diff options
Diffstat (limited to 'helix-core/src/increment')
-rw-r--r-- | helix-core/src/increment/date_time.rs | 490 | ||||
-rw-r--r-- | helix-core/src/increment/mod.rs | 8 | ||||
-rw-r--r-- | helix-core/src/increment/number.rs | 507 |
3 files changed, 1005 insertions, 0 deletions
diff --git a/helix-core/src/increment/date_time.rs b/helix-core/src/increment/date_time.rs new file mode 100644 index 00000000..e3cfe107 --- /dev/null +++ b/helix-core/src/increment/date_time.rs @@ -0,0 +1,490 @@ +use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; +use once_cell::sync::Lazy; +use regex::Regex; +use ropey::RopeSlice; + +use std::borrow::Cow; +use std::cmp; + +use super::Increment; +use crate::{Range, Tendril}; + +#[derive(Debug, PartialEq, Eq)] +pub struct DateTimeIncrementor { + date_time: NaiveDateTime, + range: Range, + fmt: &'static str, + field: DateField, +} + +impl DateTimeIncrementor { + pub fn from_range(text: RopeSlice, range: Range) -> Option<DateTimeIncrementor> { + let range = if range.is_empty() { + if range.anchor < text.len_chars() { + // Treat empty range as a cursor range. + range.put_cursor(text, range.anchor + 1, true) + } else { + // The range is empty and at the end of the text. + return None; + } + } else { + range + }; + + FORMATS.iter().find_map(|format| { + let from = range.from().saturating_sub(format.max_len); + let to = (range.from() + format.max_len).min(text.len_chars()); + + let (from_in_text, to_in_text) = (range.from() - from, range.to() - from); + let text: Cow<str> = text.slice(from..to).into(); + + let captures = format.regex.captures(&text)?; + if captures.len() - 1 != format.fields.len() { + return None; + } + + let date_time = captures.get(0)?; + let offset = range.from() - from_in_text; + let range = Range::new(date_time.start() + offset, date_time.end() + offset); + + let field = captures + .iter() + .skip(1) + .enumerate() + .find_map(|(i, capture)| { + let capture = capture?; + let capture_range = capture.range(); + + if capture_range.contains(&from_in_text) + && capture_range.contains(&(to_in_text - 1)) + { + Some(format.fields[i]) + } else { + None + } + })?; + + let has_date = format.fields.iter().any(|f| f.unit.is_date()); + let has_time = format.fields.iter().any(|f| f.unit.is_time()); + + let date_time = &text[date_time.start()..date_time.end()]; + let date_time = match (has_date, has_time) { + (true, true) => NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?, + (true, false) => { + let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?; + + date.and_hms(0, 0, 0) + } + (false, true) => { + let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?; + + NaiveDate::from_ymd(0, 1, 1).and_time(time) + } + (false, false) => return None, + }; + + Some(DateTimeIncrementor { + date_time, + range, + fmt: format.fmt, + field, + }) + }) + } +} + +impl Increment for DateTimeIncrementor { + fn increment(&self, amount: i64) -> (Range, Tendril) { + let date_time = match self.field.unit { + DateUnit::Years => add_years(self.date_time, amount), + DateUnit::Months => add_months(self.date_time, amount), + DateUnit::Days => add_duration(self.date_time, Duration::days(amount)), + DateUnit::Hours => add_duration(self.date_time, Duration::hours(amount)), + DateUnit::Minutes => add_duration(self.date_time, Duration::minutes(amount)), + DateUnit::Seconds => add_duration(self.date_time, Duration::seconds(amount)), + DateUnit::AmPm => toggle_am_pm(self.date_time), + } + .unwrap_or(self.date_time); + + (self.range, date_time.format(self.fmt).to_string().into()) + } +} + +static FORMATS: Lazy<Vec<Format>> = Lazy::new(|| { + vec![ + Format::new("%Y-%m-%d %H:%M:%S"), // 2021-11-24 07:12:23 + Format::new("%Y/%m/%d %H:%M:%S"), // 2021/11/24 07:12:23 + Format::new("%Y-%m-%d %H:%M"), // 2021-11-24 07:12 + Format::new("%Y/%m/%d %H:%M"), // 2021/11/24 07:12 + Format::new("%Y-%m-%d"), // 2021-11-24 + Format::new("%Y/%m/%d"), // 2021/11/24 + Format::new("%a %b %d %Y"), // Wed Nov 24 2021 + Format::new("%d-%b-%Y"), // 24-Nov-2021 + Format::new("%Y %b %d"), // 2021 Nov 24 + Format::new("%b %d, %Y"), // Nov 24, 2021 + Format::new("%-I:%M:%S %P"), // 7:21:53 am + Format::new("%-I:%M %P"), // 7:21 am + Format::new("%-I:%M:%S %p"), // 7:21:53 AM + Format::new("%-I:%M %p"), // 7:21 AM + Format::new("%H:%M:%S"), // 23:24:23 + Format::new("%H:%M"), // 23:24 + ] +}); + +#[derive(Debug)] +struct Format { + fmt: &'static str, + fields: Vec<DateField>, + regex: Regex, + max_len: usize, +} + +impl Format { + fn new(fmt: &'static str) -> Self { + let mut remaining = fmt; + let mut fields = Vec::new(); + let mut regex = String::new(); + let mut max_len = 0; + + while let Some(i) = remaining.find('%') { + let after = &remaining[i + 1..]; + let mut chars = after.chars(); + let c = chars.next().unwrap(); + + let spec_len = if c == '-' { + 1 + chars.next().unwrap().len_utf8() + } else { + c.len_utf8() + }; + + let specifier = &after[..spec_len]; + let field = DateField::from_specifier(specifier).unwrap(); + fields.push(field); + max_len += field.max_len + remaining[..i].len(); + regex += &remaining[..i]; + regex += &format!("({})", field.regex); + remaining = &after[spec_len..]; + } + + let regex = Regex::new(®ex).unwrap(); + + Self { + fmt, + fields, + regex, + max_len, + } + } +} + +impl PartialEq for Format { + fn eq(&self, other: &Self) -> bool { + self.fmt == other.fmt && self.fields == other.fields && self.max_len == other.max_len + } +} + +impl Eq for Format {} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +struct DateField { + regex: &'static str, + unit: DateUnit, + max_len: usize, +} + +impl DateField { + fn from_specifier(specifier: &str) -> Option<Self> { + match specifier { + "Y" => Some(DateField { + regex: r"\d{4}", + unit: DateUnit::Years, + max_len: 5, + }), + "y" => Some(DateField { + regex: r"\d\d", + unit: DateUnit::Years, + max_len: 2, + }), + "m" => Some(DateField { + regex: r"[0-1]\d", + unit: DateUnit::Months, + max_len: 2, + }), + "d" => Some(DateField { + regex: r"[0-3]\d", + unit: DateUnit::Days, + max_len: 2, + }), + "-d" => Some(DateField { + regex: r"[1-3]?\d", + unit: DateUnit::Days, + max_len: 2, + }), + "a" => Some(DateField { + regex: r"Sun|Mon|Tue|Wed|Thu|Fri|Sat", + unit: DateUnit::Days, + max_len: 3, + }), + "A" => Some(DateField { + regex: r"Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday", + unit: DateUnit::Days, + max_len: 9, + }), + "b" | "h" => Some(DateField { + regex: r"Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec", + unit: DateUnit::Months, + max_len: 3, + }), + "B" => Some(DateField { + regex: r"January|February|March|April|May|June|July|August|September|October|November|December", + unit: DateUnit::Months, + max_len: 9, + }), + "H" => Some(DateField { + regex: r"[0-2]\d", + unit: DateUnit::Hours, + max_len: 2, + }), + "M" => Some(DateField { + regex: r"[0-5]\d", + unit: DateUnit::Minutes, + max_len: 2, + }), + "S" => Some(DateField { + regex: r"[0-5]\d", + unit: DateUnit::Seconds, + max_len: 2, + }), + "I" => Some(DateField { + regex: r"[0-1]\d", + unit: DateUnit::Hours, + max_len: 2, + }), + "-I" => Some(DateField { + regex: r"1?\d", + unit: DateUnit::Hours, + max_len: 2, + }), + "P" => Some(DateField { + regex: r"am|pm", + unit: DateUnit::AmPm, + max_len: 2, + }), + "p" => Some(DateField { + regex: r"AM|PM", + unit: DateUnit::AmPm, + max_len: 2, + }), + _ => None, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum DateUnit { + Years, + Months, + Days, + Hours, + Minutes, + Seconds, + AmPm, +} + +impl DateUnit { + fn is_date(self) -> bool { + matches!(self, DateUnit::Years | DateUnit::Months | DateUnit::Days) + } + + fn is_time(self) -> bool { + matches!( + self, + DateUnit::Hours | DateUnit::Minutes | DateUnit::Seconds + ) + } +} + +fn ndays_in_month(year: i32, month: u32) -> u32 { + // The first day of the next month... + let (y, m) = if month == 12 { + (year + 1, 1) + } else { + (year, month + 1) + }; + let d = NaiveDate::from_ymd(y, m, 1); + + // ...is preceded by the last day of the original month. + d.pred().day() +} + +fn add_months(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> { + let month = (date_time.month0() as i64).checked_add(amount)?; + let year = date_time.year() + i32::try_from(month / 12).ok()?; + let year = if month.is_negative() { year - 1 } else { year }; + + // Normalize month + let month = month % 12; + let month = if month.is_negative() { + month + 12 + } else { + month + } as u32 + + 1; + + let day = cmp::min(date_time.day(), ndays_in_month(year, month)); + + Some(NaiveDate::from_ymd(year, month, day).and_time(date_time.time())) +} + +fn add_years(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> { + let year = i32::try_from((date_time.year() as i64).checked_add(amount)?).ok()?; + let ndays = ndays_in_month(year, date_time.month()); + + if date_time.day() > ndays { + let d = NaiveDate::from_ymd(year, date_time.month(), ndays); + Some(d.succ().and_time(date_time.time())) + } else { + date_time.with_year(year) + } +} + +fn add_duration(date_time: NaiveDateTime, duration: Duration) -> Option<NaiveDateTime> { + date_time.checked_add_signed(duration) +} + +fn toggle_am_pm(date_time: NaiveDateTime) -> Option<NaiveDateTime> { + if date_time.hour() < 12 { + add_duration(date_time, Duration::hours(12)) + } else { + add_duration(date_time, Duration::hours(-12)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Rope; + + #[test] + fn test_increment_date_times() { + let tests = [ + // (original, cursor, amount, expected) + ("2020-02-28", 0, 1, "2021-02-28"), + ("2020-02-29", 0, 1, "2021-03-01"), + ("2020-01-31", 5, 1, "2020-02-29"), + ("2020-01-20", 5, 1, "2020-02-20"), + ("2021-01-01", 5, -1, "2020-12-01"), + ("2021-01-31", 5, -2, "2020-11-30"), + ("2020-02-28", 8, 1, "2020-02-29"), + ("2021-02-28", 8, 1, "2021-03-01"), + ("2021-02-28", 0, -1, "2020-02-28"), + ("2021-03-01", 0, -1, "2020-03-01"), + ("2020-02-29", 5, -1, "2020-01-29"), + ("2020-02-20", 5, -1, "2020-01-20"), + ("2020-02-29", 8, -1, "2020-02-28"), + ("2021-03-01", 8, -1, "2021-02-28"), + ("1980/12/21", 8, 100, "1981/03/31"), + ("1980/12/21", 8, -100, "1980/09/12"), + ("1980/12/21", 8, 1000, "1983/09/17"), + ("1980/12/21", 8, -1000, "1978/03/27"), + ("2021-11-24 07:12:23", 0, 1, "2022-11-24 07:12:23"), + ("2021-11-24 07:12:23", 5, 1, "2021-12-24 07:12:23"), + ("2021-11-24 07:12:23", 8, 1, "2021-11-25 07:12:23"), + ("2021-11-24 07:12:23", 11, 1, "2021-11-24 08:12:23"), + ("2021-11-24 07:12:23", 14, 1, "2021-11-24 07:13:23"), + ("2021-11-24 07:12:23", 17, 1, "2021-11-24 07:12:24"), + ("2021/11/24 07:12:23", 0, 1, "2022/11/24 07:12:23"), + ("2021/11/24 07:12:23", 5, 1, "2021/12/24 07:12:23"), + ("2021/11/24 07:12:23", 8, 1, "2021/11/25 07:12:23"), + ("2021/11/24 07:12:23", 11, 1, "2021/11/24 08:12:23"), + ("2021/11/24 07:12:23", 14, 1, "2021/11/24 07:13:23"), + ("2021/11/24 07:12:23", 17, 1, "2021/11/24 07:12:24"), + ("2021-11-24 07:12", 0, 1, "2022-11-24 07:12"), + ("2021-11-24 07:12", 5, 1, "2021-12-24 07:12"), + ("2021-11-24 07:12", 8, 1, "2021-11-25 07:12"), + ("2021-11-24 07:12", 11, 1, "2021-11-24 08:12"), + ("2021-11-24 07:12", 14, 1, "2021-11-24 07:13"), + ("2021/11/24 07:12", 0, 1, "2022/11/24 07:12"), + ("2021/11/24 07:12", 5, 1, "2021/12/24 07:12"), + ("2021/11/24 07:12", 8, 1, "2021/11/25 07:12"), + ("2021/11/24 07:12", 11, 1, "2021/11/24 08:12"), + ("2021/11/24 07:12", 14, 1, "2021/11/24 07:13"), + ("Wed Nov 24 2021", 0, 1, "Thu Nov 25 2021"), + ("Wed Nov 24 2021", 4, 1, "Fri Dec 24 2021"), + ("Wed Nov 24 2021", 8, 1, "Thu Nov 25 2021"), + ("Wed Nov 24 2021", 11, 1, "Thu Nov 24 2022"), + ("24-Nov-2021", 0, 1, "25-Nov-2021"), + ("24-Nov-2021", 3, 1, "24-Dec-2021"), + ("24-Nov-2021", 7, 1, "24-Nov-2022"), + ("2021 Nov 24", 0, 1, "2022 Nov 24"), + ("2021 Nov 24", 5, 1, "2021 Dec 24"), + ("2021 Nov 24", 9, 1, "2021 Nov 25"), + ("Nov 24, 2021", 0, 1, "Dec 24, 2021"), + ("Nov 24, 2021", 4, 1, "Nov 25, 2021"), + ("Nov 24, 2021", 8, 1, "Nov 24, 2022"), + ("7:21:53 am", 0, 1, "8:21:53 am"), + ("7:21:53 am", 3, 1, "7:22:53 am"), + ("7:21:53 am", 5, 1, "7:21:54 am"), + ("7:21:53 am", 8, 1, "7:21:53 pm"), + ("7:21:53 AM", 0, 1, "8:21:53 AM"), + ("7:21:53 AM", 3, 1, "7:22:53 AM"), + ("7:21:53 AM", 5, 1, "7:21:54 AM"), + ("7:21:53 AM", 8, 1, "7:21:53 PM"), + ("7:21 am", 0, 1, "8:21 am"), + ("7:21 am", 3, 1, "7:22 am"), + ("7:21 am", 5, 1, "7:21 pm"), + ("7:21 AM", 0, 1, "8:21 AM"), + ("7:21 AM", 3, 1, "7:22 AM"), + ("7:21 AM", 5, 1, "7:21 PM"), + ("23:24:23", 1, 1, "00:24:23"), + ("23:24:23", 3, 1, "23:25:23"), + ("23:24:23", 6, 1, "23:24:24"), + ("23:24", 1, 1, "00:24"), + ("23:24", 3, 1, "23:25"), + ]; + + for (original, cursor, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::new(cursor, cursor + 1); + assert_eq!( + DateTimeIncrementor::from_range(rope.slice(..), range) + .unwrap() + .increment(amount) + .1, + expected.into() + ); + } + } + + #[test] + fn test_invalid_date_times() { + let tests = [ + "0000-00-00", + "1980-2-21", + "1980-12-1", + "12345", + "2020-02-30", + "1999-12-32", + "19-12-32", + "1-2-3", + "0000/00/00", + "1980/2/21", + "1980/12/1", + "12345", + "2020/02/30", + "1999/12/32", + "19/12/32", + "1/2/3", + "123:456:789", + "11:61", + "2021-55-12 08:12:54", + ]; + + for invalid in tests { + let rope = Rope::from_str(invalid); + let range = Range::new(0, 1); + + assert_eq!(DateTimeIncrementor::from_range(rope.slice(..), range), None) + } + } +} diff --git a/helix-core/src/increment/mod.rs b/helix-core/src/increment/mod.rs new file mode 100644 index 00000000..f5945774 --- /dev/null +++ b/helix-core/src/increment/mod.rs @@ -0,0 +1,8 @@ +pub mod date_time; +pub mod number; + +use crate::{Range, Tendril}; + +pub trait Increment { + fn increment(&self, amount: i64) -> (Range, Tendril); +} diff --git a/helix-core/src/increment/number.rs b/helix-core/src/increment/number.rs new file mode 100644 index 00000000..a19b7e75 --- /dev/null +++ b/helix-core/src/increment/number.rs @@ -0,0 +1,507 @@ +use std::borrow::Cow; + +use ropey::RopeSlice; + +use super::Increment; + +use crate::{ + textobject::{textobject_word, TextObject}, + Range, Tendril, +}; + +#[derive(Debug, PartialEq, Eq)] +pub struct NumberIncrementor<'a> { + value: i64, + radix: u32, + range: Range, + + text: RopeSlice<'a>, +} + +impl<'a> NumberIncrementor<'a> { + /// Return information about number under rang if there is one. + pub fn from_range(text: RopeSlice, range: Range) -> Option<NumberIncrementor> { + // If the cursor is on the minus sign of a number we want to get the word textobject to the + // right of it. + let range = if range.to() < text.len_chars() + && range.to() - range.from() <= 1 + && text.char(range.from()) == '-' + { + Range::new(range.from() + 1, range.to() + 1) + } else { + range + }; + + let range = textobject_word(text, range, TextObject::Inside, 1, false); + + // If there is a minus sign to the left of the word object, we want to include it in the range. + let range = if range.from() > 0 && text.char(range.from() - 1) == '-' { + range.extend(range.from() - 1, range.from()) + } else { + range + }; + + let word: String = text + .slice(range.from()..range.to()) + .chars() + .filter(|&c| c != '_') + .collect(); + let (radix, prefixed) = if word.starts_with("0x") { + (16, true) + } else if word.starts_with("0o") { + (8, true) + } else if word.starts_with("0b") { + (2, true) + } else { + (10, false) + }; + + let number = if prefixed { &word[2..] } else { &word }; + + let value = i128::from_str_radix(number, radix).ok()?; + if (value.is_positive() && value.leading_zeros() < 64) + || (value.is_negative() && value.leading_ones() < 64) + { + return None; + } + + let value = value as i64; + Some(NumberIncrementor { + range, + value, + radix, + text, + }) + } +} + +impl<'a> Increment for NumberIncrementor<'a> { + fn increment(&self, amount: i64) -> (Range, Tendril) { + let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into(); + let old_length = old_text.len(); + let new_value = self.value.wrapping_add(amount); + + // Get separator indexes from right to left. + let separator_rtl_indexes: Vec<usize> = old_text + .chars() + .rev() + .enumerate() + .filter_map(|(i, c)| if c == '_' { Some(i) } else { None }) + .collect(); + + let format_length = if self.radix == 10 { + match (self.value.is_negative(), new_value.is_negative()) { + (true, false) => old_length - 1, + (false, true) => old_length + 1, + _ => old_text.len(), + } + } else { + old_text.len() - 2 + } - separator_rtl_indexes.len(); + + let mut new_text = match self.radix { + 2 => format!("0b{:01$b}", new_value, format_length), + 8 => format!("0o{:01$o}", new_value, format_length), + 10 if old_text.starts_with('0') || old_text.starts_with("-0") => { + format!("{:01$}", new_value, format_length) + } + 10 => format!("{}", new_value), + 16 => { + let (lower_count, upper_count): (usize, usize) = + old_text.chars().skip(2).fold((0, 0), |(lower, upper), c| { + ( + lower + c.is_ascii_lowercase().then(|| 1).unwrap_or(0), + upper + c.is_ascii_uppercase().then(|| 1).unwrap_or(0), + ) + }); + if upper_count > lower_count { + format!("0x{:01$X}", new_value, format_length) + } else { + format!("0x{:01$x}", new_value, format_length) + } + } + _ => unimplemented!("radix not supported: {}", self.radix), + }; + + // Add separators from original number. + for &rtl_index in &separator_rtl_indexes { + if rtl_index < new_text.len() { + let new_index = new_text.len() - rtl_index; + new_text.insert(new_index, '_'); + } + } + + // Add in additional separators if necessary. + if new_text.len() > old_length && !separator_rtl_indexes.is_empty() { + let spacing = match separator_rtl_indexes.as_slice() { + [.., b, a] => a - b - 1, + _ => separator_rtl_indexes[0], + }; + + let prefix_length = if self.radix == 10 { 0 } else { 2 }; + if let Some(mut index) = new_text.find('_') { + while index - prefix_length > spacing { + index -= spacing; + new_text.insert(index, '_'); + } + } + } + + (self.range, new_text.into()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Rope; + + #[test] + fn test_decimal_at_point() { + let rope = Rope::from_str("Test text 12345 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 15), + value: 12345, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_uppercase_hexadecimal_at_point() { + let rope = Rope::from_str("Test text 0x123ABCDEF more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 21), + value: 0x123ABCDEF, + radix: 16, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_lowercase_hexadecimal_at_point() { + let rope = Rope::from_str("Test text 0xfa3b4e more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 18), + value: 0xfa3b4e, + radix: 16, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_octal_at_point() { + let rope = Rope::from_str("Test text 0o1074312 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 19), + value: 0o1074312, + radix: 8, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_binary_at_point() { + let rope = Rope::from_str("Test text 0b10111010010101 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 26), + value: 0b10111010010101, + radix: 2, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_negative_decimal_at_point() { + let rope = Rope::from_str("Test text -54321 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 16), + value: -54321, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_decimal_with_leading_zeroes_at_point() { + let rope = Rope::from_str("Test text 000045326 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 19), + value: 45326, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_negative_decimal_cursor_on_minus_sign() { + let rope = Rope::from_str("Test text -54321 more text."); + let range = Range::point(10); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 16), + value: -54321, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_number_under_range_start_of_rope() { + let rope = Rope::from_str("100"); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(0, 3), + value: 100, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_number_under_range_end_of_rope() { + let rope = Rope::from_str("100"); + let range = Range::point(2); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(0, 3), + value: 100, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_number_surrounded_by_punctuation() { + let rope = Rope::from_str(",100;"); + let range = Range::point(1); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(1, 4), + value: 100, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_not_a_number_point() { + let rope = Rope::from_str("Test text 45326 more text."); + let range = Range::point(6); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_number_too_large_at_point() { + let rope = Rope::from_str("Test text 0xFFFFFFFFFFFFFFFFF more text."); + let range = Range::point(12); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_number_cursor_one_right_of_number() { + let rope = Rope::from_str("100 "); + let range = Range::point(3); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_number_cursor_one_left_of_number() { + let rope = Rope::from_str(" 100"); + let range = Range::point(0); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_increment_basic_decimal_numbers() { + let tests = [ + ("100", 1, "101"), + ("100", -1, "99"), + ("99", 1, "100"), + ("100", 1000, "1100"), + ("100", -1000, "-900"), + ("-1", 1, "0"), + ("-1", 2, "1"), + ("1", -1, "0"), + ("1", -2, "-1"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .increment(amount) + .1, + expected.into() + ); + } + } + + #[test] + fn test_increment_basic_hexadedimal_numbers() { + let tests = [ + ("0x0100", 1, "0x0101"), + ("0x0100", -1, "0x00ff"), + ("0x0001", -1, "0x0000"), + ("0x0000", -1, "0xffffffffffffffff"), + ("0xffffffffffffffff", 1, "0x0000000000000000"), + ("0xffffffffffffffff", 2, "0x0000000000000001"), + ("0xffffffffffffffff", -1, "0xfffffffffffffffe"), + ("0xABCDEF1234567890", 1, "0xABCDEF1234567891"), + ("0xabcdef1234567890", 1, "0xabcdef1234567891"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .increment(amount) + .1, + expected.into() + ); + } + } + + #[test] + fn test_increment_basic_octal_numbers() { + let tests = [ + ("0o0107", 1, "0o0110"), + ("0o0110", -1, "0o0107"), + ("0o0001", -1, "0o0000"), + ("0o7777", 1, "0o10000"), + ("0o1000", -1, "0o0777"), + ("0o0107", 10, "0o0121"), + ("0o0000", -1, "0o1777777777777777777777"), + ("0o1777777777777777777777", 1, "0o0000000000000000000000"), + ("0o1777777777777777777777", 2, "0o0000000000000000000001"), + ("0o1777777777777777777777", -1, "0o1777777777777777777776"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .increment(amount) + .1, + expected.into() + ); + } + } + + #[test] + fn test_increment_basic_binary_numbers() { + let tests = [ + ("0b00000100", 1, "0b00000101"), + ("0b00000100", -1, "0b00000011"), + ("0b00000100", 2, "0b00000110"), + ("0b00000100", -2, "0b00000010"), + ("0b00000001", -1, "0b00000000"), + ("0b00111111", 10, "0b01001001"), + ("0b11111111", 1, "0b100000000"), + ("0b10000000", -1, "0b01111111"), + ( + "0b0000", + -1, + "0b1111111111111111111111111111111111111111111111111111111111111111", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + 1, + "0b0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + 2, + "0b0000000000000000000000000000000000000000000000000000000000000001", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + -1, + "0b1111111111111111111111111111111111111111111111111111111111111110", + ), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .increment(amount) + .1, + expected.into() + ); + } + } + + #[test] + fn test_increment_with_separators() { + let tests = [ + ("999_999", 1, "1_000_000"), + ("1_000_000", -1, "999_999"), + ("-999_999", -1, "-1_000_000"), + ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000", -1, "0xffff_ffff_ffff_ffff"), + ("0x0000_0000_0000", -1, "0xffff_ffff_ffff_ffff"), + ("0b01111111_11111111", 1, "0b10000000_00000000"), + ("0b11111111_11111111", 1, "0b1_00000000_00000000"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .increment(amount) + .1, + expected.into() + ); + } + } +} |