@@ -1,70 +1,124 @@
1−use chrono::{Duration, NaiveDate};
1+use gregorian::{Date, DateResultExt};
2+use regex::Regex;
23
34use std::borrow::Cow;
45
56use ropey::RopeSlice;
67
7−use crate::{
8− textobject::{textobject_word, TextObject},
9− Range, Tendril,
10−};
8+use crate::{Range, Tendril};
9+
10+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
11+struct Format {
12+ regex: &'static str,
13+ separator: char,
14+}
1115
1216// 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+];
1427
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.
1828const DATE_LENGTH: usize = 10;
1929
30+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
31+enum DateField {
32+ Year,
33+ Month,
34+ Day,
35+}
36+
2037#[derive(Debug, PartialEq, Eq)]
2138pub struct DateIncrementor {
22− pub date: NaiveDate,
39+ pub date: Date,
2340 pub range: Range,
24− pub format: &'static str,
41+
42+ field: DateField,
43+ format: Format,
2544}
2645
2746impl DateIncrementor {
2847 pub fn from_range(text: RopeSlice, range: Range) -> Option<DateIncrementor> {
29− // Don't increment if the cursor is one right of the date text.
30− if text.char(range.from()).is_whitespace() {
31− return None;
32− }
48+ let from = range.from().saturating_sub(DATE_LENGTH);
49+ let to = (range.from() + DATE_LENGTH).min(text.len_chars());
50+ let (from_in_text, to_in_text) = (range.from() - from, range.to() - from);
51+ let text: Cow<str> = text.slice(from..to).into();
3352
34− let range = textobject_word(text, range, TextObject::Inside, 1, true);
35− let text: Cow<str> = text.slice(range.from()..range.to()).into();
53+ FORMATS.iter().find_map(|&format| {
54+ let re = Regex::new(format.regex).ok()?;
55+ let captures = re.captures(&text)?;
3656
37− let first = text.chars().next()?;
38− let last = text.chars().next_back()?;
57+ let date = captures.get(0)?;
58+ let offset = range.from() - from_in_text;
59+ let range = Range::new(date.start() + offset, date.end() + offset);
3960
40− // Allow date strings in quotes.
41− let (range, text) = if first == last && (first == '"' || first == '\'') {
42− (
43− Range::new(range.from() + 1, range.to() - 1),
44− Cow::from(&text[1..text.len() - 1]),
45− )
46− } else {
47− (range, text)
48− };
61+ let year = captures.get(1)?;
62+ let month = captures.get(2)?;
63+ let day = captures.get(3)?;
4964
50− if text.len() != DATE_LENGTH {
51− return None;
52− }
65+ let year_range = year.range();
66+ let month_range = month.range();
67+ let day_range = day.range();
5368
54− FORMATS.iter().find_map(|format| {
55− NaiveDate::parse_from_str(&text, format)
56− .ok()
57− .map(|date| DateIncrementor {
58− date,
59− range,
60− format,
61− })
69+ let to_inclusive = if to_in_text > from_in_text {
70+ to_in_text - 1
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+
91+ Some(DateIncrementor {
92+ date,
93+ field,
94+ range,
95+ format,
96+ })
6297 })
6398 }
6499
65100 pub fn incremented_text(&self, amount: i64) -> Tendril {
66− let incremented_date = self.date + Duration::days(amount);
67− incremented_date.format(self.format).to_string().into()
101+ let date = match self.field {
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()
68122 }
69123}
70124
@@ -74,43 +128,144 @@ mod test {
74128 use crate::Rope;
75129
76130 #[test]
77− fn test_date_dashes() {
131+ fn test_create_incrementor_for_year_with_dashes() {
78132 let rope = Rope::from_str("2021-11-15");
79− let range = Range::point(0);
80− assert_eq!(
81− DateIncrementor::from_range(rope.slice(..), range),
82− Some(DateIncrementor {
83− date: NaiveDate::from_ymd(2021, 11, 15),
84− range: Range::new(0, 10),
85− format: "%Y-%m-%d",
86− })
87− );
133+
134+ for head in 0..=3 {
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);
154+ assert_eq!(
155+ DateIncrementor::from_range(rope.slice(..), range),
156+ Some(DateIncrementor {
157+ date: Date::new(2021, 11, 15).unwrap(),
158+ range: Range::new(0, 10),
159+ field: DateField::Month,
160+ format: FORMATS[0],
161+ })
162+ );
163+ }
164+ }
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+ }
88182 }
89183
90184 #[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() {
92196 let rope = Rope::from_str("2021/11/15");
93− let range = Range::point(0);
94− assert_eq!(
95− DateIncrementor::from_range(rope.slice(..), range),
96− Some(DateIncrementor {
97− date: NaiveDate::from_ymd(2021, 11, 15),
98− range: Range::new(0, 10),
99− format: "%Y/%m/%d",
100− })
101− );
197+
198+ for head in 0..=3 {
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);
218+ assert_eq!(
219+ DateIncrementor::from_range(rope.slice(..), range),
220+ Some(DateIncrementor {
221+ date: Date::new(2021, 11, 15).unwrap(),
222+ range: Range::new(0, 10),
223+ field: DateField::Month,
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+ }
246+ }
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+ }
102256 }
103257
104258 #[test]
105259 fn test_date_surrounded_by_spaces() {
106260 let rope = Rope::from_str(" 2021-11-15 ");
107− let range = Range::point(10);
261+ let range = Range::point(3);
108262 assert_eq!(
109263 DateIncrementor::from_range(rope.slice(..), range),
110264 Some(DateIncrementor {
111− date: NaiveDate::from_ymd(2021, 11, 15),
265+ date: Date::new(2021, 11, 15).unwrap(),
112266 range: Range::new(3, 13),
113− format: "%Y-%m-%d",
267+ field: DateField::Year,
268+ format: FORMATS[0],
114269 })
115270 );
116271 }
@@ -122,23 +277,25 @@ mod test {
122277 assert_eq!(
123278 DateIncrementor::from_range(rope.slice(..), range),
124279 Some(DateIncrementor {
125− date: NaiveDate::from_ymd(2021, 11, 15),
280+ date: Date::new(2021, 11, 15).unwrap(),
126281 range: Range::new(8, 18),
127− format: "%Y-%m-%d",
282+ field: DateField::Year,
283+ format: FORMATS[0],
128284 })
129285 );
130286 }
131287
132288 #[test]
133289 fn test_date_in_double_quotes() {
134− let rope = Rope::from_str("date = \"2021-11-15\"");
135− let range = Range::point(10);
290+ let rope = Rope::from_str("let date = \"2021-11-15\";");
291+ let range = Range::point(12);
136292 assert_eq!(
137293 DateIncrementor::from_range(rope.slice(..), range),
138294 Some(DateIncrementor {
139− date: NaiveDate::from_ymd(2021, 11, 15),
140− range: Range::new(8, 18),
141− format: "%Y-%m-%d",
295+ date: Date::new(2021, 11, 15).unwrap(),
296+ range: Range::new(12, 22),
297+ field: DateField::Year,
298+ format: FORMATS[0],
142299 })
143300 );
144301 }
@@ -189,23 +346,28 @@ mod test {
189346 #[test]
190347 fn test_increment_dates() {
191348 let tests = [
192− ("1980-12-21", 1, "1980-12-22"),
193− ("1980-12-21", -1, "1980-12-20"),
194− ("1980-12-21", 100, "1981-03-31"),
195− ("1980-12-21", -100, "1980-09-12"),
196− ("1980-12-21", 1000, "1983-09-17"),
197− ("1980-12-21", -1000, "1978-03-27"),
198− ("1980/12/21", 1, "1980/12/22"),
199− ("1980/12/21", -1, "1980/12/20"),
200− ("1980/12/21", 100, "1981/03/31"),
201− ("1980/12/21", -100, "1980/09/12"),
202− ("1980/12/21", 1000, "1983/09/17"),
203− ("1980/12/21", -1000, "1978/03/27"),
349+ // (original, cursor, amount, expected)
350+ ("2020-02-28", 0, 1, "2021-02-28"),
351+ ("2020-02-29", 0, 1, "2021-03-01"),
352+ ("2020-01-31", 5, 1, "2020-02-29"),
353+ ("2020-01-20", 5, 1, "2020-02-20"),
354+ ("2020-02-28", 8, 1, "2020-02-29"),
355+ ("2021-02-28", 8, 1, "2021-03-01"),
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"),
362+ ("1980/12/21", 8, 100, "1981/03/31"),
363+ ("1980/12/21", 8, -100, "1980/09/12"),
364+ ("1980/12/21", 8, 1000, "1983/09/17"),
365+ ("1980/12/21", 8, -1000, "1978/03/27"),
204366 ];
205367
206− for (original, amount, expected) in tests {
368+ for (original, cursor, amount, expected) in tests {
207369 let rope = Rope::from_str(original);
208− let range = Range::point(0);
370+ let range = Range::point(cursor);
209371 assert_eq!(
210372 DateIncrementor::from_range(rope.slice(..), range)
211373 .unwrap()