diff --git a/core/src/ast.rs b/core/src/ast.rs index 091ae145..7de5116b 100644 --- a/core/src/ast.rs +++ b/core/src/ast.rs @@ -867,9 +867,9 @@ fn resolve_builtin_identifier( ]), "print" => Value::BuiltInFunction(BuiltInFunction::Print), "println" => Value::BuiltInFunction(BuiltInFunction::Println), - "today" => Value::Date(crate::date::Date::today(context)?), - "tomorrow" => Value::Date(crate::date::Date::today(context)?.next()), - "yesterday" => Value::Date(crate::date::Date::today(context)?.prev()), + "today" => Value::Date(crate::date::Date::today(context, int)?), + "tomorrow" => Value::Date(crate::date::Date::today(context, int)?.next()?), + "yesterday" => Value::Date(crate::date::Date::today(context, int)?.prev()?), "trans" => Value::String(Cow::Borrowed("🏳️‍⚧️")), _ => return Err(FendError::IdentifierNotFound(ident.clone())), }) diff --git a/core/src/date.rs b/core/src/date.rs index 33207c09..3f33b8ff 100644 --- a/core/src/date.rs +++ b/core/src/date.rs @@ -11,6 +11,7 @@ pub(crate) use day_of_week::DayOfWeek; pub(crate) use month::Month; use year::Year; +use crate::interrupt::test_int; use crate::{Interrupt, error::FendError, ident::Ident, result::FResult, value::Value}; #[derive(Copy, Clone, Eq, PartialEq)] @@ -21,36 +22,31 @@ pub(crate) struct Date { } impl Date { - pub(crate) fn today(context: &crate::Context) -> FResult { + pub(crate) fn today(context: &crate::Context, int: &I) -> FResult { let Some(current_time_info) = &context.current_time else { return Err(FendError::UnableToGetCurrentDate); }; - let mut ms_since_epoch: i64 = current_time_info.elapsed_unix_time_ms.try_into().unwrap(); - ms_since_epoch -= current_time_info.timezone_offset_secs * 1000; - let mut days = ms_since_epoch / 86_400_000; // no leap seconds - let mut year = Year::new(1970); - while days >= year.number_of_days().into() { - year = year.next(); - days -= i64::from(year.number_of_days()); - } - let mut month = Month::January; - while days >= month.number_of_days(year).into() { - month = month.next(); - days -= i64::from(month.number_of_days(year)); - } - Ok(Self { - year, - month, - day: Day::new(days.try_into().unwrap()), - }) + let mut seconds_since_epoch = current_time_info.elapsed_unix_time_ms / 1000; + seconds_since_epoch = seconds_since_epoch + .checked_sub_signed(current_time_info.timezone_offset_secs) + .ok_or(FendError::ValueTooLarge)?; + let days = seconds_since_epoch / (60 * 60 * 24); // no leap seconds + + let date = Self { + year: Year::new(1970), + day: Day::new(1), + month: Month::January, + }; + + date.add_days(days.try_into().map_err(|_| FendError::ValueTooLarge)?, int) } fn day_of_week(self) -> DayOfWeek { - let d1 = (1 - + 5 * ((self.year.value() - 1) % 4) - + 4 * ((self.year.value() - 1) % 100) - + 6 * ((self.year.value() - 1) % 400)) - % 7; + let d1 = ((1 + + 5 * ((i64::from(self.year.value()) - 1) % 4) + + 4 * ((i64::from(self.year.value()) - 1) % 100) + + 6 * ((i64::from(self.year.value()) - 1) % 400)) + % 7) as i32; let ms = match self.month { Month::January => (0, 0), Month::February => (3, 3), @@ -63,7 +59,7 @@ impl Date { Month::October => (0, 1), }; let m = if self.year.is_leap_year() { ms.1 } else { ms.0 }; - match (d1 + m + i32::from(self.day.value() - 1)) % 7 { + match ((d1 + m + i32::from(self.day.value() - 1)) % 7 + 7) % 7 { 0 => DayOfWeek::Sunday, 1 => DayOfWeek::Monday, 2 => DayOfWeek::Tuesday, @@ -75,65 +71,66 @@ impl Date { } } - pub(crate) fn next(self) -> Self { + pub(crate) fn next(self) -> FResult { if self.day.value() < Month::number_of_days(self.month, self.year) { - Self { + Ok(Self { day: Day::new(self.day.value() + 1), month: self.month, year: self.year, - } + }) } else if self.month == Month::December { - Self { + Ok(Self { day: Day::new(1), month: Month::January, - year: self.year.next(), - } + year: self.year.next()?, + }) } else { - Self { + Ok(Self { day: Day::new(1), month: self.month.next(), year: self.year, - } + }) } } - pub(crate) fn prev(self) -> Self { + pub(crate) fn prev(self) -> FResult { if self.day.value() > 1 { - Self { + Ok(Self { day: Day::new(self.day.value() - 1), month: self.month, year: self.year, - } + }) } else if self.month == Month::January { - Self { + Ok(Self { day: Day::new(31), month: Month::December, - year: self.year.prev(), - } + year: self.year.prev()?, + }) } else { let month = self.month.prev(); - Self { + Ok(Self { day: Day::new(Month::number_of_days(month, self.year)), month, year: self.year, - } + }) } } pub(crate) fn diff_months(self, mut months: i64) -> FResult { let mut result = self; - while months >= 12 { - result.year = result.year.next(); - months -= 12; - } - while months <= -12 { - result.year = result.year.prev(); - months += 12; + if months >= 12 { + let years = months / 12; + months %= 12; + result.year = result.year.add(years)?; + } else if months <= -12 { + let years = (months / 12).unsigned_abs(); + months %= 12; + result.year = result.year.sub(years)?; } while months > 0 { if result.month == Month::December { result.month = Month::January; - result.year = result.year.next(); + result.year = result.year.next()?; } else { result.month = result.month.next(); } @@ -142,7 +139,7 @@ impl Date { while months < 0 { if result.month == Month::January { result.month = Month::December; - result.year = result.year.prev(); + result.year = result.year.prev()?; } else { result.month = result.month.prev(); } @@ -154,7 +151,7 @@ impl Date { let mut after = result; if after.month == Month::December { after.month = Month::January; - after.year = after.year.next(); + after.year = after.year.next()?; } else { after.month = after.month.next(); } @@ -197,38 +194,110 @@ impl Date { }) } + fn add_days(self, mut num_days: usize, int: &I) -> FResult { + let mut result = self; + + // make sure to be before 29th February to make skipping years work + while num_days > 0 && result.month != Month::January { + result = result.next()?; + num_days -= 1; + } + + while let days_in_year = result.year.number_of_days().into() + && num_days >= days_in_year + { + num_days -= days_in_year; + + result.year = result.year.next()?; + + test_int(int)?; + } + + for _ in 0..num_days { + test_int(int)?; + result = result.next()?; + } + + Ok(result) + } + pub(crate) fn add(self, rhs: Value, int: &I) -> FResult { let rhs = rhs.expect_num()?; if rhs.unit_equal_to("day", int)? { let num_days = rhs.try_as_usize_unit(int)?; - let mut result = self; - for _ in 0..num_days { - result = result.next(); - } + + let result = self.add_days(num_days, int)?; + + Ok(Value::Date(result)) + } else if rhs.unit_equal_to("week", int)? { + let num_weeks = rhs.try_as_usize_unit(int)?; + + let num_days = num_weeks.checked_mul(7).ok_or(FendError::ValueTooLarge)?; + + let result = self.add_days(num_days, int)?; + + debug_assert_eq!(self.day_of_week(), result.day_of_week()); + + Ok(Value::Date(result)) + } else if rhs.unit_equal_to("month", int)? { + let num_months = rhs.try_as_usize_unit(int)?; + let result = + self.diff_months(i64::try_from(num_months).map_err(|_| FendError::ValueTooLarge)?)?; + Ok(Value::Date(result)) + } else if rhs.unit_equal_to("year", int)? { + let num_years = rhs.try_as_usize_unit(int)?; + let num_months = num_years.checked_mul(12).ok_or(FendError::ValueTooLarge)?; + let result = + self.diff_months(i64::try_from(num_months).map_err(|_| FendError::ValueTooLarge)?)?; Ok(Value::Date(result)) } else { Err(FendError::ExpectedANumber) } } + fn sub_days(self, mut num_days: usize, int: &I) -> FResult { + let mut result = self; + + // make sure to be after 29th February to make skipping years work + while num_days > 0 && result.month.as_u8() < Month::March.as_u8() { + result = result.prev()?; + num_days -= 1; + } + + while let days_in_year = result.year.number_of_days().into() + && num_days >= days_in_year + { + num_days -= days_in_year; + + result.year = result.year.prev()?; + + test_int(int)?; + } + + for _ in 0..num_days { + result = result.prev()?; + test_int(int)?; + } + + Ok(result) + } + pub(crate) fn sub(self, rhs: Value, int: &I) -> FResult { let rhs = rhs.expect_num()?; if rhs.unit_equal_to("day", int)? { let num_days = rhs.try_as_usize_unit(int)?; - let mut result = self; - for _ in 0..num_days { - result = result.prev(); - } + + let result = self.sub_days(num_days, int)?; + Ok(Value::Date(result)) } else if rhs.unit_equal_to("week", int)? { let num_weeks = rhs.try_as_usize_unit(int)?; - let mut result = self; - for _ in 0..num_weeks { - for _ in 0..7 { - result = result.prev(); - } - } + + let num_days = num_weeks.checked_mul(7).ok_or(FendError::ValueTooLarge)?; + + let result = self.sub_days(num_days, int)?; + Ok(Value::Date(result)) } else if rhs.unit_equal_to("month", int)? { let num_months = rhs.try_as_usize_unit(int)?; @@ -237,7 +306,7 @@ impl Date { Ok(Value::Date(result)) } else if rhs.unit_equal_to("year", int)? { let num_years = rhs.try_as_usize_unit(int)?; - let num_months = num_years * 12; + let num_months = num_years.checked_mul(12).ok_or(FendError::ValueTooLarge)?; let result = self .diff_months(-i64::try_from(num_months).map_err(|_| FendError::ValueTooLarge)?)?; Ok(Value::Date(result)) diff --git a/core/src/date/month.rs b/core/src/date/month.rs index 1173c5ed..6692d755 100644 --- a/core/src/date/month.rs +++ b/core/src/date/month.rs @@ -98,7 +98,7 @@ impl Month { .map_err(|_| FendError::DeserializationError("month is out of range")) } - fn as_u8(self) -> u8 { + pub(crate) fn as_u8(self) -> u8 { match self { Self::January => 1, Self::February => 2, diff --git a/core/src/date/year.rs b/core/src/date/year.rs index ea67acf8..bfde71fc 100644 --- a/core/src/date/year.rs +++ b/core/src/date/year.rs @@ -1,5 +1,7 @@ use std::{convert, fmt, io}; +use crate::format::DisplayDebug; +use crate::num::RangeBound; use crate::{ error::FendError, result::FResult, @@ -10,30 +12,92 @@ use crate::{ pub(crate) struct Year(i32); impl Year { - pub(crate) fn new(year: i32) -> Self { + pub(crate) const MAX: Self = Self::new(i32::MAX); + pub(crate) const MIN: Self = Self::new(i32::MIN); + + pub(crate) const fn new(year: i32) -> Self { assert!(year != 0, "year 0 is invalid"); Self(year) } #[inline] - pub(crate) fn value(self) -> i32 { + pub(crate) const fn value(self) -> i32 { self.0 } - pub(crate) fn next(self) -> Self { - if self.value() == -1 { + pub(crate) fn out_of_range_error(value: impl DisplayDebug + 'static) -> FendError { + FendError::OutOfRange { + value: Box::new(value), + range: crate::num::Range { + start: RangeBound::Closed(Box::new(Self::MIN)), + end: RangeBound::Closed(Box::new(Self::MAX)), + }, + } + } + + pub(crate) fn add( + self, + value: impl TryInto + Copy + DisplayDebug + 'static, + ) -> FResult { + let value = value.try_into().map_err(|_| FendError::ValueTooLarge)?; + + let new_year = self + .value() + .checked_add_unsigned(value) + .ok_or(FendError::ValueTooLarge)?; + + Ok(if new_year == 0 { Self::new(1) } else { - Self::new(self.value() + 1) - } + match (self.value().is_positive(), new_year.is_positive()) { + (true, true) | (false, false) => Self::new(new_year), + (false, true) => Self::new(new_year).next()?, // add one year because 0 isn't valid. + (true, false) => unreachable!("Year can't have become negative"), + } + }) + } + + pub(crate) fn next(self) -> FResult { + Ok(if self.value() == -1 { + Self::new(1) + } else { + Self::new( + self.value().checked_add(1).ok_or_else(|| { + Self::out_of_range_error(const { Self::MAX.value() as i64 + 1 }) + })?, + ) + }) } - pub(crate) fn prev(self) -> Self { - if self.value() == 1 { + pub(crate) fn sub(self, value: impl TryInto + Copy + 'static) -> FResult { + let value = value.try_into().map_err(|_| FendError::ValueTooLarge)?; + + let new_year = self + .value() + .checked_sub_unsigned(value) + .ok_or(FendError::ValueTooLarge)?; + + Ok(if new_year == 0 { Self::new(-1) } else { - Self::new(self.value() - 1) - } + match (self.value().is_positive(), new_year.is_positive()) { + (true, true) | (false, false) => Self::new(new_year), + (true, false) => Self::new(new_year).prev()?, // sub one year because 0 isn't valid. + (false, true) => unreachable!("Year can't have become positive"), + } + }) + } + + pub(crate) fn prev(self) -> FResult { + Ok(if self.value() == 1 { + Self::new(-1) + } else { + Self::new( + self.value().checked_sub(1).ok_or_else(|| { + Self::out_of_range_error(const { Self::MIN.value() as i64 - 1 }) + })?, + ) + }) } pub(crate) fn is_leap_year(self) -> bool { @@ -77,18 +141,15 @@ impl convert::TryFrom for Year { impl fmt::Debug for Year { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.value() < 0 { - write!(f, "{} BC", -self.0) - } else { - write!(f, "{}", self.0) - } + fmt::Display::fmt(self, f) } } impl fmt::Display for Year { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.value() < 0 { - write!(f, "{} BC", -self.0) + // cast to bigger int to fix this for Self::MIN + write!(f, "{} BC", -i64::from(self.0)) } else { write!(f, "{}", self.0) } @@ -108,5 +169,6 @@ mod tests { #[test] fn negative_year_string() { assert_eq!(Year::new(-823).to_string(), "823 BC"); + assert_eq!(Year::MIN.to_string(), "2147483648 BC"); } } diff --git a/core/src/lib.rs b/core/src/lib.rs index e77d4731..845f9d80 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -385,12 +385,11 @@ impl Context { /// /// The second argument (`tz_offset_secs`) is the current time zone /// offset to UTC, in seconds. - pub fn set_current_time_v1(&mut self, _ms_since_1970: u64, _tz_offset_secs: i64) { - // self.current_time = Some(CurrentTimeInfo { - // elapsed_unix_time_ms: ms_since_1970, - // timezone_offset_secs: tz_offset_secs, - // }); - self.current_time = None; + pub fn set_current_time_v1(&mut self, ms_since_1970: u64, tz_offset_secs: i64) { + self.current_time = Some(CurrentTimeInfo { + elapsed_unix_time_ms: ms_since_1970, + timezone_offset_secs: tz_offset_secs, + }); } /// Define the units `C` and `F` as coulomb and farad instead of degrees diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 4ac63ae0..407e5843 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -5186,7 +5186,6 @@ fn unicode_escape_aaa_uppercase() { } #[test] -#[ignore] fn today() { let mut context = Context::new(); context.set_current_time_v1(1617517099000, 0); @@ -5197,14 +5196,20 @@ fn today() { } #[test] -#[ignore] fn today_with_tz() { let mut context = Context::new(); - context.set_current_time_v1(1619943083155, 43200); + context.set_current_time_v1(1619943083155, 28800); assert_eq!( evaluate("today", &mut context).unwrap().get_main_result(), "Sunday, 2 May 2021" ); + + let mut context = Context::new(); + context.set_current_time_v1(1619943083155, 43200); + assert_eq!( + evaluate("today", &mut context).unwrap().get_main_result(), + "Saturday, 1 May 2021" + ); } #[test] @@ -5767,13 +5772,68 @@ fn permutation_test() { test_eval("10 permute 3", "720"); } -// ERROR +#[test] +fn test_date_adding_many_days_works() { + test_eval_simple("@2024-02-29 + 365 days", "Friday, 28 February 2025"); + test_eval_simple("@2024-02-29 + 366 days", "Saturday, 1 March 2025"); + test_eval_simple("@2025-02-28 + 365 days", "Saturday, 28 February 2026"); + test_eval_simple("@2023-02-28 + 365 days", "Wednesday, 28 February 2024"); + test_eval_simple("@2023-02-28 + 366 days", "Thursday, 29 February 2024"); + test_eval_simple("@2023-03-01 + 365 days", "Thursday, 29 February 2024"); + test_eval_simple("@2023-03-01 + 366 days", "Friday, 1 March 2024"); +} + +#[test] +#[cfg_attr(not(target_pointer_width = "64"), ignore = "Needs at least 64 bits.")] +fn test_date_adding_really_many_days_works() { + test_eval_simple( + "@1970-01-01 + 1_000_000_000 days", + "Wednesday, 3 January 2739877", + ); + test_eval_simple( + "@1970-01-01 + 100_000_000_000 days", + "Tuesday, 13 September 273792670", + ); +} + +#[test] +fn test_date_literal_addition() { + test_eval_simple("@1970-01-01 + 52 weeks", "Thursday, 31 December 1970"); // not leap year + test_eval_simple("@2020-01-01 + 52 weeks", "Wednesday, 30 December 2020"); // leap year + test_eval_simple("@1970-01-01 + 200 weeks", "Thursday, 1 November 1973"); + test_eval_simple("@2004-01-01 + 100 weeks", "Thursday, 1 December 2005"); + test_eval_simple("@2005-01-01 + 100 weeks", "Saturday, 2 December 2006"); + test_eval_simple("@1970-01-01 + 40 month", "Tuesday, 1 May 1973"); + test_eval_simple("@2004-01-01 + 200 weeks", "Thursday, 1 November 2007"); + test_eval_simple("@2004-01-01 + 40 month", "Tuesday, 1 May 2007"); + test_eval_simple("@2004-01-01 + 4 years", "Tuesday, 1 January 2008"); +} + +#[test] +fn test_date_literal_addition_and_subtraction() { + test_eval( + "@2020-01-01 + 1000 weeks - 1000 weeks == @2020-01-01", + "true", + ); + test_eval( + "@2020-01-01 + 1000 weeks - 7000 days == @2020-01-01", + "true", + ); + test_eval( + "@2020-01-01 - 1000 weeks + 7000 days == @2020-01-01", + "true", + ); + test_eval("@2020-01-01 - 100 month + 100 month == @2020-01-01", "true"); + test_eval("@2020-01-01 + 100 month - 100 month == @2020-01-01", "true"); + test_eval("@2020-01-01 - 1200 month + 100 year == @2020-01-01", "true"); + test_eval("@2020-01-01 + 1200 month - 100 year == @2020-01-01", "true"); +} + #[test] fn date_literals() { test_eval_simple("@1970-01-01", "Thursday, 1 January 1970"); } -// ERROR #[test] fn date_literal_subtraction() { test_eval_simple("@2022-11-29 - 2 days", "Sunday, 27 November 2022"); @@ -5783,6 +5843,10 @@ fn date_literal_subtraction() { test_eval_simple("@2022-03-01 - 1 month", "Tuesday, 1 February 2022"); test_eval_simple("@2020-02-28 - 1 year", "Thursday, 28 February 2019"); + test_eval_simple("@2020-02-28 - 365 days", "Thursday, 28 February 2019"); + test_eval_simple("@2020-03-28 - 365 days", "Friday, 29 March 2019"); // leap year + test_eval_simple("@2019-03-28 - 365 days", "Wednesday, 28 March 2018"); // not leap year + expect_error( "@2020-02-29 - 1 year", "February 29, 2019 does not exist, did you mean Thursday, 28 February 2019 or Friday, 1 March 2019?".into(), @@ -5792,6 +5856,20 @@ fn date_literal_subtraction() { "February 29, 2019 does not exist, did you mean Thursday, 28 February 2019 or Friday, 1 March 2019?".into(), ); test_eval_simple("@2020-08-01 - 1 year", "Thursday, 1 August 2019"); + + test_eval_simple("@2000-01-01 - 1999 year", "Monday, 1 January 1"); + test_eval_simple("@2000-01-01 - 2000 year", "Saturday, 1 January 1 BC"); + test_eval_simple("@2000-01-01 - 2001 year", "Friday, 1 January 2 BC"); + + test_eval_simple("@2000-02-01 - (1999 * 12 + 1) month", "Monday, 1 January 1"); + test_eval_simple( + "@2000-03-01 - (2000 * 12 + 2) month", + "Saturday, 1 January 1 BC", + ); + test_eval_simple( + "@2000-04-01 - (2001 * 12 + 3) month", + "Friday, 1 January 2 BC", + ); } #[test]