@@ -0,0 +1,217 @@
1+use chrono::{Duration, NaiveDate};
2+
3+use std::borrow::Cow;
4+
5+use ropey::RopeSlice;
6+
7+use crate::{
8+ textobject::{textobject_word, TextObject},
9+ Range, Tendril,
10+};
11+
12+// Only support formats that aren't region specific.
13+static FORMATS: &[&str] = &["%Y-%m-%d", "%Y/%m/%d"];
14+
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.
18+const DATE_LENGTH: usize = 10;
19+
20+#[derive(Debug, PartialEq, Eq)]
21+pub struct DateIncrementor {
22+ pub date: NaiveDate,
23+ pub range: Range,
24+ pub format: &'static str,
25+}
26+
27+impl DateIncrementor {
28+ 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+ }
33+
34+ let range = textobject_word(text, range, TextObject::Inside, 1, true);
35+ let text: Cow<str> = text.slice(range.from()..range.to()).into();
36+
37+ let first = text.chars().next()?;
38+ let last = text.chars().next_back()?;
39+
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+ };
49+
50+ if text.len() != DATE_LENGTH {
51+ return None;
52+ }
53+
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+ })
62+ })
63+ }
64+
65+ 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()
68+ }
69+}
70+
71+#[cfg(test)]
72+mod test {
73+ use super::*;
74+ use crate::Rope;
75+
76+ #[test]
77+ fn test_date_dashes() {
78+ 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+ );
88+ }
89+
90+ #[test]
91+ fn test_date_slashes() {
92+ 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+ );
102+ }
103+
104+ #[test]
105+ fn test_date_surrounded_by_spaces() {
106+ let rope = Rope::from_str(" 2021-11-15 ");
107+ let range = Range::point(10);
108+ assert_eq!(
109+ DateIncrementor::from_range(rope.slice(..), range),
110+ Some(DateIncrementor {
111+ date: NaiveDate::from_ymd(2021, 11, 15),
112+ range: Range::new(3, 13),
113+ format: "%Y-%m-%d",
114+ })
115+ );
116+ }
117+
118+ #[test]
119+ fn test_date_in_single_quotes() {
120+ let rope = Rope::from_str("date = '2021-11-15'");
121+ let range = Range::point(10);
122+ assert_eq!(
123+ DateIncrementor::from_range(rope.slice(..), range),
124+ Some(DateIncrementor {
125+ date: NaiveDate::from_ymd(2021, 11, 15),
126+ range: Range::new(8, 18),
127+ format: "%Y-%m-%d",
128+ })
129+ );
130+ }
131+
132+ #[test]
133+ fn test_date_in_double_quotes() {
134+ let rope = Rope::from_str("date = \"2021-11-15\"");
135+ let range = Range::point(10);
136+ assert_eq!(
137+ DateIncrementor::from_range(rope.slice(..), range),
138+ Some(DateIncrementor {
139+ date: NaiveDate::from_ymd(2021, 11, 15),
140+ range: Range::new(8, 18),
141+ format: "%Y-%m-%d",
142+ })
143+ );
144+ }
145+
146+ #[test]
147+ fn test_date_cursor_one_right_of_date() {
148+ let rope = Rope::from_str("2021-11-15 ");
149+ let range = Range::point(10);
150+ assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None);
151+ }
152+
153+ #[test]
154+ fn test_date_cursor_one_left_of_number() {
155+ let rope = Rope::from_str(" 2021-11-15");
156+ let range = Range::point(0);
157+ assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None);
158+ }
159+
160+ #[test]
161+ fn test_invalid_dates() {
162+ let tests = [
163+ "0000-00-00",
164+ "1980-2-21",
165+ "1980-12-1",
166+ "12345",
167+ "2020-02-30",
168+ "1999-12-32",
169+ "19-12-32",
170+ "1-2-3",
171+ "0000/00/00",
172+ "1980/2/21",
173+ "1980/12/1",
174+ "12345",
175+ "2020/02/30",
176+ "1999/12/32",
177+ "19/12/32",
178+ "1/2/3",
179+ ];
180+
181+ for invalid in tests {
182+ let rope = Rope::from_str(invalid);
183+ let range = Range::point(0);
184+
185+ assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None);
186+ }
187+ }
188+
189+ #[test]
190+ fn test_increment_dates() {
191+ 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"),
204+ ];
205+
206+ for (original, amount, expected) in tests {
207+ let rope = Rope::from_str(original);
208+ let range = Range::point(0);
209+ assert_eq!(
210+ DateIncrementor::from_range(rope.slice(..), range)
211+ .unwrap()
212+ .incremented_text(amount),
213+ expected.into()
214+ );
215+ }
216+ }
217+}