on master help sign in
Add support for incrementing year and month
95cfeedJason Rodney Hansen
Cargo.lock+10−1
@@ -325,6 +325,15 @@ dependencies = [
325 "regex",
325 "regex",
326]
326]
327
327
328+[[package]]
329+name = "gregorian"
330+version = "0.2.1"
331+source = "registry+https://github.com/rust-lang/crates.io-index"
332+checksum = "3452972f2c995c38dc9b84f09fe14f1ca462a0580642fe6756fed58fdef29050"
333+dependencies = [
334+ "libc",
335+]
336+
328[[package]]
337[[package]]
329name = "grep-matcher"
338name = "grep-matcher"
330version = "0.1.5"
339version = "0.1.5"
@@ -369,8 +378,8 @@ name = "helix-core"
369version = "0.5.0"
378version = "0.5.0"
370dependencies = [
379dependencies = [
371 "arc-swap",
380 "arc-swap",
372− "chrono",
373 "etcetera",
381 "etcetera",
382+ "gregorian",
374 "helix-syntax",
383 "helix-syntax",
375 "log",
384 "log",
376 "once_cell",
385 "once_cell",
helix-core/Cargo.toml+1−1
@@ -36,7 +36,7 @@ similar = "2.1"
36
36
37etcetera = "0.3"
37etcetera = "0.3"
38
38
39−chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
39+gregorian = "0.2.1"
40
40
41[dev-dependencies]
41[dev-dependencies]
42quickcheck = { version = "1", default-features = false }
42quickcheck = { version = "1", default-features = false }
helix-core/src/date.rs+247−85
@@ -1,70 +1,124 @@
1−use chrono::{Duration, NaiveDate};
1+use gregorian::{Date, DateResultExt};
2+use regex::Regex;
2
3
3use std::borrow::Cow;
4use std::borrow::Cow;
4
5
5use ropey::RopeSlice;
6use ropey::RopeSlice;
6
7
7−use crate::{
8− textobject::{textobject_word, TextObject},
9− Range, Tendril,
8+use crate::{Range, Tendril};
10−};
9+
10+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
11+struct Format {
12+ regex: &'static str,
13+ separator: char,
14+}
11
15
12// Only support formats that aren't region specific.
16// Only support formats that aren't region specific.
13−static FORMATS: &[&str] = &["%Y-%m-%d", "%Y/%m/%d"];
17+static FORMATS: &[Format] = &[
18+ Format {
19+ regex: r"(\d{4})-(\d{2})-(\d{2})",
20+ separator: '-',
21+ },
22+ Format {
23+ regex: r"(\d{4})/(\d{2})/(\d{2})",
24+ separator: '/',
25+ },
26+];
14
27
15−// We don't want to parse ambiguous dates like 10/11/12 or 7/8/10.
16−// They must be YYYY-mm-dd or YYYY/mm/dd.
17−// So 2021-01-05 works, but 2021-1-5 doesn't.
18const DATE_LENGTH: usize = 10;
28const DATE_LENGTH: usize = 10;
19
29
30+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
31+enum DateField {
32+ Year,
33+ Month,
34+ Day,
35+}
36+
20#[derive(Debug, PartialEq, Eq)]
37#[derive(Debug, PartialEq, Eq)]
21pub struct DateIncrementor {
38pub struct DateIncrementor {
22− pub date: NaiveDate,
39+ pub date: Date,
23 pub range: Range,
40 pub range: Range,
41+
42+ field: DateField,
24− pub format: &'static str,
43+ format: Format,
25}
44}
26
45
27impl DateIncrementor {
46impl DateIncrementor {
28 pub fn from_range(text: RopeSlice, range: Range) -> Option<DateIncrementor> {
47 pub fn from_range(text: RopeSlice, range: Range) -> Option<DateIncrementor> {
29− // Don't increment if the cursor is one right of the date text.
48+ let from = range.from().saturating_sub(DATE_LENGTH);
30− if text.char(range.from()).is_whitespace() {
49+ let to = (range.from() + DATE_LENGTH).min(text.len_chars());
31− return None;
50+ let (from_in_text, to_in_text) = (range.from() - from, range.to() - from);
32− }
51+ let text: Cow<str> = text.slice(from..to).into();
33
52
34− let range = textobject_word(text, range, TextObject::Inside, 1, true);
53+ FORMATS.iter().find_map(|&format| {
35− let text: Cow<str> = text.slice(range.from()..range.to()).into();
54+ let re = Regex::new(format.regex).ok()?;
55+ let captures = re.captures(&text)?;
36
56
37− let first = text.chars().next()?;
57+ let date = captures.get(0)?;
38− let last = text.chars().next_back()?;
58+ let offset = range.from() - from_in_text;
59+ let range = Range::new(date.start() + offset, date.end() + offset);
39
60
40− // Allow date strings in quotes.
61+ let year = captures.get(1)?;
41− let (range, text) = if first == last && (first == '"' || first == '\'') {
62+ let month = captures.get(2)?;
42− (
63+ let day = captures.get(3)?;
43− Range::new(range.from() + 1, range.to() - 1),
44− Cow::from(&text[1..text.len() - 1]),
45− )
46− } else {
47− (range, text)
48− };
49
64
50− if text.len() != DATE_LENGTH {
65+ let year_range = year.range();
51− return None;
66+ let month_range = month.range();
52− }
67+ let day_range = day.range();
53
68
54− FORMATS.iter().find_map(|format| {
69+ let to_inclusive = if to_in_text > from_in_text {
55− NaiveDate::parse_from_str(&text, format)
70+ to_in_text - 1
56− .ok()
71+ } else {
72+ to_in_text
73+ };
74+ let field = if year_range.contains(&from_in_text) && year_range.contains(&to_inclusive)
75+ {
76+ DateField::Year
77+ } else if month_range.contains(&from_in_text) && month_range.contains(&to_inclusive) {
78+ DateField::Month
79+ } else if day_range.contains(&from_in_text) && day_range.contains(&to_inclusive) {
80+ DateField::Day
81+ } else {
82+ return None;
83+ };
84+
85+ let year: i16 = year.as_str().parse().ok()?;
86+ let month: u8 = month.as_str().parse().ok()?;
87+ let day: u8 = day.as_str().parse().ok()?;
88+
89+ let date = Date::new(year, month, day).ok()?;
90+
57− .map(|date| DateIncrementor {
91+ Some(DateIncrementor {
58− date,
92+ date,
93+ field,
59− range,
94+ range,
60− format,
95+ format,
61− })
96+ })
62 })
97 })
63 }
98 }
64
99
65 pub fn incremented_text(&self, amount: i64) -> Tendril {
100 pub fn incremented_text(&self, amount: i64) -> Tendril {
66− let incremented_date = self.date + Duration::days(amount);
101+ let date = match self.field {
67− incremented_date.format(self.format).to_string().into()
102+ DateField::Year => self
103+ .date
104+ .add_years(amount.try_into().unwrap_or(0))
105+ .or_next_valid(),
106+ DateField::Month => self
107+ .date
108+ .add_months(amount.try_into().unwrap_or(0))
109+ .or_prev_valid(),
110+ DateField::Day => self.date.add_days(amount.try_into().unwrap_or(0)),
111+ };
112+
113+ format!(
114+ "{:04}{}{:02}{}{:02}",
115+ date.year(),
116+ self.format.separator,
117+ date.month().to_number(),
118+ self.format.separator,
119+ date.day()
120+ )
121+ .into()
68 }
122 }
69}
123}
70
124
@@ -74,43 +128,144 @@ mod test {
74 use crate::Rope;
128 use crate::Rope;
75
129
76 #[test]
130 #[test]
77− fn test_date_dashes() {
131+ fn test_create_incrementor_for_year_with_dashes() {
78 let rope = Rope::from_str("2021-11-15");
132 let rope = Rope::from_str("2021-11-15");
133+
134+ for head in 0..=3 {
79− let range = Range::point(0);
135+ let range = Range::point(head);
136+ assert_eq!(
137+ DateIncrementor::from_range(rope.slice(..), range),
138+ Some(DateIncrementor {
139+ date: Date::new(2021, 11, 15).unwrap(),
140+ range: Range::new(0, 10),
141+ field: DateField::Year,
142+ format: FORMATS[0],
143+ })
144+ );
145+ }
146+ }
147+
148+ #[test]
149+ fn test_create_incrementor_for_month_with_dashes() {
150+ let rope = Rope::from_str("2021-11-15");
151+
152+ for head in 5..=6 {
153+ let range = Range::point(head);
80− assert_eq!(
154+ assert_eq!(
81− DateIncrementor::from_range(rope.slice(..), range),
155+ DateIncrementor::from_range(rope.slice(..), range),
82− Some(DateIncrementor {
156+ Some(DateIncrementor {
83− date: NaiveDate::from_ymd(2021, 11, 15),
157+ date: Date::new(2021, 11, 15).unwrap(),
84− range: Range::new(0, 10),
158+ range: Range::new(0, 10),
159+ field: DateField::Month,
85− format: "%Y-%m-%d",
160+ format: FORMATS[0],
161+ })
162+ );
163+ }
86− })
164+ }
87− );
165+
166+ #[test]
167+ fn test_create_incrementor_for_day_with_dashes() {
168+ let rope = Rope::from_str("2021-11-15");
169+
170+ for head in 8..=9 {
171+ let range = Range::point(head);
172+ assert_eq!(
173+ DateIncrementor::from_range(rope.slice(..), range),
174+ Some(DateIncrementor {
175+ date: Date::new(2021, 11, 15).unwrap(),
176+ range: Range::new(0, 10),
177+ field: DateField::Day,
178+ format: FORMATS[0],
179+ })
180+ );
181+ }
88 }
182 }
89
183
90 #[test]
184 #[test]
91− fn test_date_slashes() {
185+ fn test_try_create_incrementor_on_dashes() {
186+ let rope = Rope::from_str("2021-11-15");
187+
188+ for head in &[4, 7] {
189+ let range = Range::point(*head);
190+ assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None,);
191+ }
192+ }
193+
194+ #[test]
195+ fn test_create_incrementor_for_year_with_slashes() {
92 let rope = Rope::from_str("2021/11/15");
196 let rope = Rope::from_str("2021/11/15");
197+
198+ for head in 0..=3 {
93− let range = Range::point(0);
199+ let range = Range::point(head);
200+ assert_eq!(
201+ DateIncrementor::from_range(rope.slice(..), range),
202+ Some(DateIncrementor {
203+ date: Date::new(2021, 11, 15).unwrap(),
204+ range: Range::new(0, 10),
205+ field: DateField::Year,
206+ format: FORMATS[1],
207+ })
208+ );
209+ }
210+ }
211+
212+ #[test]
213+ fn test_create_incrementor_for_month_with_slashes() {
214+ let rope = Rope::from_str("2021/11/15");
215+
216+ for head in 5..=6 {
217+ let range = Range::point(head);
94− assert_eq!(
218+ assert_eq!(
95− DateIncrementor::from_range(rope.slice(..), range),
219+ DateIncrementor::from_range(rope.slice(..), range),
96− Some(DateIncrementor {
220+ Some(DateIncrementor {
97− date: NaiveDate::from_ymd(2021, 11, 15),
221+ date: Date::new(2021, 11, 15).unwrap(),
98− range: Range::new(0, 10),
222+ range: Range::new(0, 10),
223+ field: DateField::Month,
99− format: "%Y/%m/%d",
224+ format: FORMATS[1],
225+ })
226+ );
227+ }
228+ }
229+
230+ #[test]
231+ fn test_create_incrementor_for_day_with_slashes() {
232+ let rope = Rope::from_str("2021/11/15");
233+
234+ for head in 8..=9 {
235+ let range = Range::point(head);
236+ assert_eq!(
237+ DateIncrementor::from_range(rope.slice(..), range),
238+ Some(DateIncrementor {
239+ date: Date::new(2021, 11, 15).unwrap(),
240+ range: Range::new(0, 10),
241+ field: DateField::Day,
242+ format: FORMATS[1],
243+ })
244+ );
245+ }
100− })
246+ }
101− );
247+
248+ #[test]
249+ fn test_try_create_incrementor_on_slashes() {
250+ let rope = Rope::from_str("2021/11/15");
251+
252+ for head in &[4, 7] {
253+ let range = Range::point(*head);
254+ assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None,);
255+ }
102 }
256 }
103
257
104 #[test]
258 #[test]
105 fn test_date_surrounded_by_spaces() {
259 fn test_date_surrounded_by_spaces() {
106 let rope = Rope::from_str(" 2021-11-15 ");
260 let rope = Rope::from_str(" 2021-11-15 ");
107− let range = Range::point(10);
261+ let range = Range::point(3);
108 assert_eq!(
262 assert_eq!(
109 DateIncrementor::from_range(rope.slice(..), range),
263 DateIncrementor::from_range(rope.slice(..), range),
110 Some(DateIncrementor {
264 Some(DateIncrementor {
111− date: NaiveDate::from_ymd(2021, 11, 15),
265+ date: Date::new(2021, 11, 15).unwrap(),
112 range: Range::new(3, 13),
266 range: Range::new(3, 13),
267+ field: DateField::Year,
113− format: "%Y-%m-%d",
268+ format: FORMATS[0],
114 })
269 })
115 );
270 );
116 }
271 }
@@ -122,23 +277,25 @@ mod test {
122 assert_eq!(
277 assert_eq!(
123 DateIncrementor::from_range(rope.slice(..), range),
278 DateIncrementor::from_range(rope.slice(..), range),
124 Some(DateIncrementor {
279 Some(DateIncrementor {
125− date: NaiveDate::from_ymd(2021, 11, 15),
280+ date: Date::new(2021, 11, 15).unwrap(),
126 range: Range::new(8, 18),
281 range: Range::new(8, 18),
282+ field: DateField::Year,
127− format: "%Y-%m-%d",
283+ format: FORMATS[0],
128 })
284 })
129 );
285 );
130 }
286 }
131
287
132 #[test]
288 #[test]
133 fn test_date_in_double_quotes() {
289 fn test_date_in_double_quotes() {
134− let rope = Rope::from_str("date = \"2021-11-15\"");
290+ let rope = Rope::from_str("let date = \"2021-11-15\";");
135− let range = Range::point(10);
291+ let range = Range::point(12);
136 assert_eq!(
292 assert_eq!(
137 DateIncrementor::from_range(rope.slice(..), range),
293 DateIncrementor::from_range(rope.slice(..), range),
138 Some(DateIncrementor {
294 Some(DateIncrementor {
139− date: NaiveDate::from_ymd(2021, 11, 15),
295+ date: Date::new(2021, 11, 15).unwrap(),
140− range: Range::new(8, 18),
296+ range: Range::new(12, 22),
297+ field: DateField::Year,
141− format: "%Y-%m-%d",
298+ format: FORMATS[0],
142 })
299 })
143 );
300 );
144 }
301 }
@@ -189,23 +346,28 @@ mod test {
189 #[test]
346 #[test]
190 fn test_increment_dates() {
347 fn test_increment_dates() {
191 let tests = [
348 let tests = [
192− ("1980-12-21", 1, "1980-12-22"),
349+ // (original, cursor, amount, expected)
193− ("1980-12-21", -1, "1980-12-20"),
350+ ("2020-02-28", 0, 1, "2021-02-28"),
194− ("1980-12-21", 100, "1981-03-31"),
351+ ("2020-02-29", 0, 1, "2021-03-01"),
195− ("1980-12-21", -100, "1980-09-12"),
352+ ("2020-01-31", 5, 1, "2020-02-29"),
196− ("1980-12-21", 1000, "1983-09-17"),
353+ ("2020-01-20", 5, 1, "2020-02-20"),
197− ("1980-12-21", -1000, "1978-03-27"),
354+ ("2020-02-28", 8, 1, "2020-02-29"),
198− ("1980/12/21", 1, "1980/12/22"),
355+ ("2021-02-28", 8, 1, "2021-03-01"),
199− ("1980/12/21", -1, "1980/12/20"),
356+ ("2021-02-28", 0, -1, "2020-02-28"),
357+ ("2021-03-01", 0, -1, "2020-03-01"),
358+ ("2020-02-29", 5, -1, "2020-01-29"),
359+ ("2020-02-20", 5, -1, "2020-01-20"),
360+ ("2020-02-29", 8, -1, "2020-02-28"),
361+ ("2021-03-01", 8, -1, "2021-02-28"),
200− ("1980/12/21", 100, "1981/03/31"),
362+ ("1980/12/21", 8, 100, "1981/03/31"),
201− ("1980/12/21", -100, "1980/09/12"),
363+ ("1980/12/21", 8, -100, "1980/09/12"),
202− ("1980/12/21", 1000, "1983/09/17"),
364+ ("1980/12/21", 8, 1000, "1983/09/17"),
203− ("1980/12/21", -1000, "1978/03/27"),
365+ ("1980/12/21", 8, -1000, "1978/03/27"),
204 ];
366 ];
205
367
206− for (original, amount, expected) in tests {
368+ for (original, cursor, amount, expected) in tests {
207 let rope = Rope::from_str(original);
369 let rope = Rope::from_str(original);
208− let range = Range::point(0);
370+ let range = Range::point(cursor);
209 assert_eq!(
371 assert_eq!(
210 DateIncrementor::from_range(rope.slice(..), range)
372 DateIncrementor::from_range(rope.slice(..), range)
211 .unwrap()
373 .unwrap()