diff --git a/src/lazy/expanded/e_expression.rs b/src/lazy/expanded/e_expression.rs index 30a9a4f4..8c1becc5 100644 --- a/src/lazy/expanded/e_expression.rs +++ b/src/lazy/expanded/e_expression.rs @@ -11,7 +11,7 @@ use crate::lazy::expanded::macro_evaluator::{ AnnotateExpansion, ConditionalExpansion, DeltaExpansion, EExpressionArgGroup, ExprGroupExpansion, FlattenExpansion, IsExhaustedIterator, MacroExpansion, MacroExpansionKind, MacroExpr, MacroExprArgsIterator, MakeDecimalExpansion, MakeFieldExpansion, MakeStructExpansion, - MakeTextExpansion, RawEExpression, RepeatExpansion, SumExpansion, TemplateExpansion, + MakeTextExpansion, MakeTimestampExpansion, RawEExpression, RepeatExpansion, SumExpansion, TemplateExpansion, ValueExpr, }; use crate::lazy::expanded::macro_table::{MacroKind, MacroRef}; @@ -129,6 +129,9 @@ impl<'top, D: Decoder> EExpression<'top, D> { MacroKind::MakeStruct => { MacroExpansionKind::MakeStruct(MakeStructExpansion::new(arguments)) } + MacroKind::MakeTimestamp => { + MacroExpansionKind::MakeTimestamp(MakeTimestampExpansion::new(arguments)) + } MacroKind::MakeField => { MacroExpansionKind::MakeField(MakeFieldExpansion::new(arguments)) } diff --git a/src/lazy/expanded/macro_evaluator.rs b/src/lazy/expanded/macro_evaluator.rs index b42fe05f..d95e35ce 100644 --- a/src/lazy/expanded/macro_evaluator.rs +++ b/src/lazy/expanded/macro_evaluator.rs @@ -39,6 +39,7 @@ use crate::{ LazyExpandedField, LazyExpandedFieldName, LazyExpandedStruct, LazyStruct, LazyValue, Span, SymbolRef, ValueRef }; +use crate::types::{HasDay, HasFractionalSeconds, HasHour, HasMinute, HasMonth, HasOffset, HasSeconds, HasYear, Timestamp, TimestampBuilder}; pub trait IsExhaustedIterator<'top, D: Decoder>: Copy + Clone + Debug + Iterator>> @@ -510,6 +511,7 @@ pub enum MacroExpansionKind<'top, D: Decoder> { MakeString(MakeTextExpansion<'top, D>), MakeSymbol(MakeTextExpansion<'top, D>), MakeStruct(MakeStructExpansion<'top, D>), + MakeTimestamp(MakeTimestampExpansion<'top, D>), MakeField(MakeFieldExpansion<'top, D>), Annotate(AnnotateExpansion<'top, D>), Flatten(FlattenExpansion<'top, D>), @@ -592,6 +594,7 @@ impl<'top, D: Decoder> MacroExpansion<'top, D> { MakeString(expansion) | MakeSymbol(expansion) => expansion.make_text_value(context), MakeField(make_field_expansion) => make_field_expansion.next(context, environment), MakeStruct(make_struct_expansion) => make_struct_expansion.next(context, environment), + MakeTimestamp(make_timestamp_expansion) => make_timestamp_expansion.next(context, environment), Annotate(annotate_expansion) => annotate_expansion.next(context, environment), Flatten(flatten_expansion) => flatten_expansion.next(), Conditional(cardinality_test_expansion) => cardinality_test_expansion.next(environment), @@ -615,6 +618,7 @@ impl Debug for MacroExpansion<'_, D> { MacroExpansionKind::MakeSymbol(_) => "make_symbol", MacroExpansionKind::MakeField(_) => "make_field", MacroExpansionKind::MakeStruct(_) => "make_struct", + MacroExpansionKind::MakeTimestamp(_) => "make_timestamp", MacroExpansionKind::Annotate(_) => "annotate", MacroExpansionKind::Flatten(_) => "flatten", MacroExpansionKind::Delta(_) => "delta", @@ -1169,41 +1173,71 @@ impl<'top, D: Decoder> ConditionalExpansion<'top, D> { } } -// ===== Implementation of the `make_decimal` macro === +macro_rules! sysmacro_arg_info { + (enum $name:ident { + $($variant:ident = $val:expr),*, + }) => { + #[derive(PartialEq)] + enum $name { + $($variant = $val),* + } -// A simple wrapper for make_decimal's known arguments. Provides context for error reporting, and -// functionality to expand e-exp and validate integer types. -struct MakeDecimalArgument<'top, D: Decoder>(&'static str, ValueExpr<'top, D>); -impl<'top, D: Decoder> MakeDecimalArgument<'top, D> { - /// Given a [`ValueExpr`], this function will expand it into its underlying value; An - /// error is return if the value does not expand to exactly one Int. - fn get_integer(&self, env: Environment<'top, D>) -> IonResult { - let parameter = self.0; - match self.1 { + impl AsRef for $name { + fn as_ref(&self) -> &str { + match self { + $($name::$variant => stringify!($variant)),* + } + } + } + } +} + +// A simple wrapper for a system macros's known arguments. Provides context for error reporting, and +// functionality to expand e-exp and validate integer types. Can be expanded for more types as +// needed. +struct SystemMacroArgument<'top, A: AsRef, D: Decoder>(A, ValueExpr<'top, D>); +impl <'top, A: AsRef, D: Decoder> SystemMacroArgument<'top, A, D> { + /// Expands the current [`ValueExpr`] for the argument and verifies that it expands to 0 or 1 + /// value. Returns the value as a ValueRef. + fn try_get_valueref(&self, env: Environment<'top, D>) -> IonResult>> { + let argument_name= self.0.as_ref(); + + let arg = match self.1 { ValueExpr::ValueLiteral(value_literal) => { - value_literal - .read_resolved()? - .expect_int() + Some(value_literal.read_resolved()?) } ValueExpr::MacroInvocation(invocation) => { let mut evaluator = MacroEvaluator::new_with_environment(env); evaluator.push(invocation.expand()?); let int_arg = match evaluator.next()? { - None => IonResult::decoding_error(format!("`make_decimal` requires an integer {parameter} but the provided argument contained no value.")), - Some(value) => value - .read_resolved()? - .expect_int(), + None => None, + Some(value) => { + Some(value.read_resolved()?) + } }; if !evaluator.is_empty() && evaluator.next()?.is_some() { - return IonResult::decoding_error(format!("`make_decimal` requires an integer {parameter} but the provided argument contained multiple values.")); + return IonResult::decoding_error(format!("expected integer value for '{argument_name}' parameter but the provided argument contained multiple values.")); } int_arg } - } + }; + Ok(arg) + } + + /// Given a [`ValueExpr`], this function will expand it into its underlying value; An + /// error is return if the value does not expand to exactly one Int. + fn get_integer(&self, env: Environment<'top, D>) -> IonResult { + let argument_name= self.0.as_ref(); + let value_ref = self.try_get_valueref(env)?; + value_ref + .ok_or_else(|| IonError::decoding_error(format!("expected integer value for '{argument_name}' parameter but the provided argument contained no value.")))? + .expect_int() } + } +// ===== Implementation of the `make_decimal` macro === #[derive(Copy, Clone, Debug)] pub struct MakeDecimalExpansion<'top, D: Decoder> { arguments: MacroExprArgsIterator<'top, D>, @@ -1219,13 +1253,17 @@ impl<'top, D: Decoder> MakeDecimalExpansion<'top, D> { context: EncodingContextRef<'top>, environment: Environment<'top, D>, ) -> IonResult> { + #[inline(never)] + fn error_context(err: IonError) -> IonError { + IonError::decoding_error(format!("`make_decimal`: {err}")) + } // Arguments should be: (coefficient exponent) // Both coefficient and exponent should evaluate to a single integer value. let coeff_expr = self.arguments.next().ok_or(IonError::decoding_error("`make_decimal` takes 2 integer arguments; found 0 arguments"))?; - let coefficient = MakeDecimalArgument("coefficient", coeff_expr?).get_integer(environment)?; + let coefficient = SystemMacroArgument("Coefficient", coeff_expr?).get_integer(environment).map_err(error_context)?; let expo_expr = self.arguments.next().ok_or(IonError::decoding_error("`make_decimal` takes 2 integer arguments; found only 1 argument"))?; - let exponent = MakeDecimalArgument("exponent", expo_expr?).get_integer(environment)?; + let exponent = SystemMacroArgument("Exponent", expo_expr?).get_integer(environment).map_err(error_context)?; let decimal = Decimal::new(coefficient, exponent.as_i64().ok_or_else(|| IonError::decoding_error("Exponent does not fit within the range supported by this implementation."))?); @@ -1240,6 +1278,325 @@ impl<'top, D: Decoder> MakeDecimalExpansion<'top, D> { } } +// ===== Implementation of the `make_timestamp` macro ==== + +/// This wrapper wraps a [`TimestampBuilder`] and allows us to treat it as an unchanging type to +/// more easily pass it around while evaluating the make_timestamp macro arguments. +#[derive(Clone)] +#[allow(clippy::enum_variant_names)] +enum TimestampBuilderWrapper { + WithYear(TimestampBuilder), + WithMonth(TimestampBuilder), + WithDay(TimestampBuilder), + WithHour(TimestampBuilder), + WithMinute(TimestampBuilder), + WithSecond(TimestampBuilder), + WithFractionalSeconds(TimestampBuilder), + WithOffset(TimestampBuilder), +} + +sysmacro_arg_info! { enum MakeTimestampArgs { Month = 0, Day = 1, Hour = 2, Minute = 3, Second = 4, Offset = 5, } } +impl TimestampBuilderWrapper { + + fn process<'top, D: Decoder>(self, env: Environment<'top, D>, arg: &SystemMacroArgument<'top, MakeTimestampArgs, D>) -> IonResult { + use TimestampBuilderWrapper::*; + match self { + WithYear(builder) => Self::process_with_year(builder, env, arg), + WithMonth(ref builder) => self.process_with_month(builder, env, arg), + WithDay(ref builder) => self.process_with_day(builder, env, arg), + WithHour(ref builder) => self.process_with_hour(builder, env, arg), + WithMinute(ref builder) => self.process_with_minute(builder, env, arg), + WithSecond(ref _builder) => { + let Some(value) = arg.try_get_valueref(env)? else { + return Ok(self.clone()); + }; + self.process_offset(value) + } + WithFractionalSeconds(ref _builder) => { + let Some(value) = arg.try_get_valueref(env)? else { + return Ok(self.clone()); + }; + self.process_offset(value) + } + WithOffset(_) => unreachable!(), // offset is the last argument, there won't be any + // more after. + } + } + + fn build(self) -> IonResult { + use TimestampBuilderWrapper::*; + match self { + WithYear(builder) => builder.build(), + WithMonth(builder) => builder.build(), + WithDay(builder) => builder.build(), + WithHour(_builder) => IonResult::decoding_error("no value provided for 'Minute'"), + WithMinute(builder) => builder.build(), + WithSecond(builder) => builder.build(), + WithFractionalSeconds(builder) => builder.build(), + WithOffset(builder) => builder.build(), + } + } + + /// Process the next provided argument after we've added the year to the timestamp. + fn process_with_year<'top, D: Decoder>(builder: TimestampBuilder, env: Environment<'top, D>, arg: &SystemMacroArgument<'top, MakeTimestampArgs, D>) -> IonResult { + // We have a year, only option for a value is the month. + let parameter = arg.0.as_ref(); + + // Check to see if we actually have a value. + let Some(value_ref) = arg.try_get_valueref(env)? else { + return Ok(TimestampBuilderWrapper::WithYear(builder)) + }; + + // We have a value, if it is anything other than Month it is invalid. + if arg.0 != MakeTimestampArgs::Month { + return IonResult::decoding_error(format!("value provided for '{parameter}', but no month specified.")); + } + + let month_i64= value_ref + .expect_int()? + .as_i64() + .filter(|v| *v >= 1 && *v <= 12) + .ok_or_else(|| IonError::decoding_error("value provided for 'Month' is out of range [1, 12]"))?; + + let new_builder = builder.clone().with_month(month_i64 as u32); + Ok(TimestampBuilderWrapper::WithMonth(new_builder)) + } + + /// Process the next provided argument, after we have added the month to the timestamp. + fn process_with_month<'top, D: Decoder>(&self, builder: &TimestampBuilder, env: Environment<'top, D>, arg: &SystemMacroArgument<'top, MakeTimestampArgs, D>) -> IonResult { + // If we have a new value, it has to be a day, nothing else is valid. + let parameter = arg.0.as_ref(); + + // Check to see if we actually have a value. + let Some(value_ref) = arg.try_get_valueref(env)? else { + return Ok(TimestampBuilderWrapper::WithMonth(builder.clone())) + }; + + // We have a value, if it is anything other than Day then it is invalid. + if arg.0 != MakeTimestampArgs::Day { + return IonResult::decoding_error(format!("value provided for '{parameter}', but no day specified.")); + } + + let day_i64 = value_ref + .expect_int()? + .as_i64() + .filter(|v| *v >= 1 && *v <= 31) + // Spec says 1 indexed day for the given month; does this mean we accept any integer + // >=1? or does the spec expect us to validate that the day is appropriate for the + // month and year? + .ok_or_else(|| IonError::decoding_error("value provided for 'Day' parameter is out of range [1, 31]"))?; + + let new_builder = builder.clone().with_day(day_i64 as u32); + Ok(TimestampBuilderWrapper::WithDay(new_builder)) + } + + /// Process the next provided argument, after we have added the day to the timestamp. + fn process_with_day<'top, D: Decoder>(&self, builder: &TimestampBuilder, env: Environment<'top, D>, arg: &SystemMacroArgument<'top, MakeTimestampArgs, D>) -> IonResult { + // We have a day, and a new value.. the only valid option is hour. + let parameter = arg.0.as_ref(); + + // Check to see if we actually have a value. + let Some(value_ref) = arg.try_get_valueref(env)? else { + return Ok(TimestampBuilderWrapper::WithDay(builder.clone())); + }; + + // We have a value, if it is anything other than Hour then it is invalid. + if arg.0 != MakeTimestampArgs::Hour { + return IonResult::decoding_error(format!("value provided for '{parameter}', but no hour specified.")); + } + + let hour_i64 = value_ref + .expect_int()? + .as_i64() + .filter(|v| *v >= 0 && *v <= 23) + .ok_or_else(|| IonError::decoding_error("value provided for 'Hour' is out of range [0, 23]"))?; + + let new_builder = builder.clone().with_hour(hour_i64 as u32); + Ok(TimestampBuilderWrapper::WithHour(new_builder)) + } + + /// Process the next provided argument after we have added the hour to the timestamp. + fn process_with_hour<'top, D: Decoder>(&self, builder: &TimestampBuilder, env: Environment<'top, D>, arg: &SystemMacroArgument<'top, MakeTimestampArgs, D>) -> IonResult { + // We have an hour, the only valid argument is Minute. + let parameter_name = arg.0.as_ref(); + + // Check if we have a value. + let Some(value_ref) = arg.try_get_valueref(env)? else { + return Ok(TimestampBuilderWrapper::WithHour(builder.clone())); + }; + + if arg.0 != MakeTimestampArgs::Minute { + return IonResult::decoding_error(format!("value provided for '{parameter_name}', but no minute specified.")); + } + + let minute_i64 = value_ref + .expect_int()? + .as_i64() + .filter(|v| *v >= 0 && *v < 60) + .ok_or_else(|| IonError::decoding_error("value provided for 'Minute' is out of range [1, 59]"))?; + + let new_builder = builder.clone().with_minute(minute_i64 as u32); + Ok(TimestampBuilderWrapper::WithMinute(new_builder)) + } + + /// Process the next provided argument after we have added the minute to the timestamp. + fn process_with_minute<'top, D: Decoder>(&self, builder: &TimestampBuilder, env: Environment<'top, D>, arg: &SystemMacroArgument<'top, MakeTimestampArgs, D>) -> IonResult { + // We have a minute, we have 2 options for args now: Seconds, and Offset. + let parameter_name = arg.0.as_ref(); + + // Check if we have a value. + let Some(value_ref) = arg.try_get_valueref(env)? else { + return Ok(self.clone()); + // return Ok(TimestampBuilderWrapper::WithMinute(builder.clone())); + }; + + if arg.0 == MakeTimestampArgs::Second { + self.process_second(builder, value_ref) + } else if arg.0 == MakeTimestampArgs::Offset { + self.process_offset(value_ref) + } else { + return IonResult::decoding_error(format!("value provided for '{parameter_name}', but no value for 'second' specified.")); + } + } + + /// Process the value for the timestamp's second field. The second can be provided as either an + /// Int or a Decimal with sub-second precision. + fn process_second<'top, D: Decoder>(&self, builder: &TimestampBuilder, value: ValueRef<'top, D>) -> IonResult { + use crate::IonType; + + let builder_wrapper= match value.ion_type() { + IonType::Decimal => { + let upper_limit = Decimal::new(60, 1); + let second_dec = value.expect_decimal()?; + + if second_dec.is_less_than_zero() || second_dec >= upper_limit { + IonResult::decoding_error("value provided for 'Second' is out of range [0, 59]")?; + } + + let whole_seconds = second_dec.trunc(); + let fractional_seconds = second_dec.fract(); + + // The whole value should be between 0 and 60, tested above. + let whole_seconds_i64 = whole_seconds + .coefficient() + .as_int() + .and_then(|v| v.as_i64()) + .unwrap(); // Tested above that the whole decimal is < 60 and > 0. + // Correct with the exponent jic the value is normalized to 5x10^1 or something. + let whole_seconds_i64 = whole_seconds_i64 * 10i64.pow(whole_seconds.exponent() as u32); + + let builder = builder.clone().with_second(whole_seconds_i64 as u32); + + let builder = builder.with_fractional_seconds(fractional_seconds); + + TimestampBuilderWrapper::WithFractionalSeconds(builder) + } + IonType::Int => { + let second_i64 = value + .expect_int()? + .as_i64() + .filter(|v| *v >= 0 && *v < 60) + .ok_or_else(|| IonError::decoding_error("value provided for 'Second' is out of range [0, 59]"))?; + TimestampBuilderWrapper::WithSecond(builder.clone().with_second(second_i64 as u32)) + } + _ => IonResult::decoding_error("value provided for 'Second' is an unexpected type; should be an integer or decimal")?, + }; + + Ok(builder_wrapper) + } + + /// Process the provided ValueRef as the offset parameter for the timestamp. + fn process_offset<'top, D: Decoder>(&self, value: ValueRef<'top, D>) -> IonResult { + let offset = value + .expect_int()? + .as_i64() + .filter(|v| *v >= -720 && *v <= 840) + .ok_or_else(|| IonError::decoding_error("value provided for 'Offset' is out of range [-840, 720]"))?; + + let new_builder = match self { + Self::WithMinute(builder) => builder.clone().with_offset(offset as i32), + Self::WithSecond(builder) => builder.clone().with_offset(offset as i32), + Self::WithFractionalSeconds(builder) => builder.clone().with_offset(offset as i32), + _ => return IonResult::decoding_error("Invalid state while building timestamp; tried to set field 'Offset' without setting time"), + }; + + Ok(TimestampBuilderWrapper::WithOffset(new_builder)) + } +} + + +#[derive(Copy, Clone, Debug)] +pub struct MakeTimestampExpansion<'top, D: Decoder> { + arguments: MacroExprArgsIterator<'top, D>, +} + +impl<'top, D: Decoder> MakeTimestampExpansion<'top, D> { + + pub fn new(arguments: MacroExprArgsIterator<'top, D>) -> Self { + Self { arguments } + } + + fn next( + &mut self, + context: EncodingContextRef<'top>, + environment: Environment<'top, D>, + ) -> IonResult> { + // make_timestamp (year month? day? hour? minute? second? offset_minutes?) + use crate::types::Timestamp; + + #[inline(never)] + fn error_context(err: IonError) -> IonError { + IonError::decoding_error(format!("`make_timestamp`: {err}")) + } + + // Year is required, so we have to ensure that it is available. Our arguments iterator will + // always have an item for each defined parameter, even if it is not present at the callsite. + // But we still check here, JIC that ever changes. + let year_expr = self.arguments + .next() + .ok_or(IonError::decoding_error("`make_timestamp` takes 1 to 7 arguments; found 0 arguments"))?; + let year = SystemMacroArgument("year", year_expr?) + .get_integer(environment) + .map_err(error_context)?; + + // Validate year range. + let year_i64 = year + .as_i64() + .filter(|v| *v >= 1 && *v <= 9999) + .ok_or_else(|| IonError::decoding_error("`make_timestamp`: value provided for 'year' parameter is out of range [1, 9999]"))?; + + // Now that we know that Year is provided, we can evaluate all of the arguments. + // TimestampBuilderWrapper handles the tracking of state, and which arguments need to be + // present and which are optional. + let args= [ + SystemMacroArgument(MakeTimestampArgs::Month, self.arguments.next().unwrap()?), + SystemMacroArgument(MakeTimestampArgs::Day, self.arguments.next().unwrap()?), + SystemMacroArgument(MakeTimestampArgs::Hour, self.arguments.next().unwrap()?), + SystemMacroArgument(MakeTimestampArgs::Minute, self.arguments.next().unwrap()?), + SystemMacroArgument(MakeTimestampArgs::Second, self.arguments.next().unwrap()?), + SystemMacroArgument(MakeTimestampArgs::Offset, self.arguments.next().unwrap()?), + ]; + + let builder = args + .iter() + .try_fold(TimestampBuilderWrapper::WithYear(Timestamp::with_year(year_i64 as u32)), |builder, arg| { + builder.process(environment, arg) + }) + .map_err(error_context)?; + + let timestamp = builder.build()?; + + let value_ref = context + .allocator() + .alloc_with(|| ValueRef::Timestamp(timestamp)); + let lazy_expanded_value = LazyExpandedValue::from_constructed(context, &[], value_ref); + + Ok(MacroExpansionStep::FinalStep(Some( + ValueExpr::ValueLiteral(lazy_expanded_value) + ))) + } +} + // ===== Implementation of the `make_field` macro ===== #[derive(Copy, Clone, Debug)] @@ -1885,6 +2242,7 @@ impl<'top> TemplateExpansion<'top> { #[cfg(test)] mod tests { use crate::{v1_1, ElementReader, Int, IonResult, MacroTable, Reader, Sequence}; + use rstest::*; /// Reads `input` and `expected` and asserts that their output is Ion-equivalent. fn stream_eq<'data>(input: &'data str, expected: &'data str) -> IonResult<()> { @@ -3406,6 +3764,62 @@ mod tests { Ok(()) } + #[test] + fn make_timestamp_eexp() -> IonResult<()> { + stream_eq( + r#" + (:make_timestamp 2025) + (:make_timestamp 2025 5) + (:make_timestamp 2025 5 2) + (:make_timestamp 2025 5 2 1 3) + (:make_timestamp 2025 5 2 1 3 5) + (:make_timestamp 2025 5 2 1 3 1.25) + (:make_timestamp 2025 5 2 1 3 10.00) + (:make_timestamp 2025 5 2 1 3 1.25 8) + (:make_timestamp 2025 5 2 1 3 (:none) 8) + (:make_timestamp 2025 5 2 1 3 5d1) + (:make_timestamp 2025 5 2 1 3 5 8) + "#, + r#" + 2025T + 2025-05T + 2025-05-02T + 2025-05-02T01:03Z + 2025-05-02T01:03:05Z + 2025-05-02T01:03:01.25Z + 2025-05-02T01:03:10.00Z + 2025-05-02T01:03:01.25+00:08 + 2025-05-02T01:03+00:08 + 2025-05-02T01:03:50Z + 2025-05-02T01:03:05+00:08 + "#, + ) + } + + #[rstest] + #[case("(:make_timestamp)", "no year specified")] + #[case("(:make_timestamp 2025 (:none) 2)", "month empty, day provided")] + #[case("(:make_timestamp 2025 5 2 1)", "no minute provided")] + #[case("(:make_timestamp 2025 5 2 (:none) (:none) (:none) 5)", "offset provided with no minute")] + #[case("(:make_timestamp 2025 5 2 (:none) (:none) 4", "second provided with no minute")] + #[case("(:make_timestamp 2025 100000)", "year out of range")] + #[case("(:make_timestamp 2025 1 2 1 70)", "minute out of range")] + #[case("(:make_timestamp 2025 1 2 1 40 -1)", "second out of range")] + #[case("(:make_timestamp asdf)", "invalid type for year")] + #[case("(:make_timestamp 2025 asdf)", "invalid type for month")] + #[case("(:make_timestamp 2025 1 asdf)", "invalid type for day")] + #[case("(:make_timestamp 2025 1 2 asdf 4)", "invalid type for hour")] + #[case("(:make_timestamp 2025 1 2 3 asdf)", "invalid type for minute")] + #[case("(:make_timestamp 2025 1 2 3 4 asdf)", "invalid type for second")] + #[case("(:make_timestamp 2025 1 2 3 4 5 asdf)", "invalid type for offset")] + fn make_timestamp_errors(#[case] source: &str, #[case] message: &str) -> IonResult<()> { + let mut actual_reader = Reader::new(v1_1::Text, source)?; + actual_reader + .read_all_elements() + .expect_err(message); + Ok(()) + } + #[test] fn sum_eexp_arg_non_int() -> IonResult<()> { // Test non-integer in first parameter diff --git a/src/lazy/expanded/macro_table.rs b/src/lazy/expanded/macro_table.rs index f911cc4d..670794ec 100644 --- a/src/lazy/expanded/macro_table.rs +++ b/src/lazy/expanded/macro_table.rs @@ -360,6 +360,7 @@ pub enum MacroKind { MakeSymbol, MakeField, MakeStruct, + MakeTimestamp, Annotate, Flatten, Template(TemplateBody), @@ -633,7 +634,7 @@ impl MacroTable { builtin( "make_timestamp", "(year month? day? hour? minute? second? offset_minutes?)", - MacroKind::ToDo, + MacroKind::MakeTimestamp, ExpansionAnalysis::single_application_value(IonType::Timestamp), ), builtin( diff --git a/src/lazy/expanded/template.rs b/src/lazy/expanded/template.rs index 62bd524b..4f74c130 100644 --- a/src/lazy/expanded/template.rs +++ b/src/lazy/expanded/template.rs @@ -4,8 +4,8 @@ use crate::lazy::expanded::compiler::ExpansionAnalysis; use crate::lazy::expanded::macro_evaluator::{ AnnotateExpansion, ConditionalExpansion, DeltaExpansion, ExprGroupExpansion, FlattenExpansion, MakeDecimalExpansion, MacroEvaluator, MacroExpansion, MacroExpansionKind, MacroExpr, - MacroExprArgsIterator, MakeFieldExpansion, MakeStructExpansion, MakeTextExpansion, RepeatExpansion, - SumExpansion, TemplateExpansion, ValueExpr, + MacroExprArgsIterator, MakeFieldExpansion, MakeStructExpansion, MakeTextExpansion, MakeTimestampExpansion, + RepeatExpansion, SumExpansion, TemplateExpansion, ValueExpr, }; use crate::lazy::expanded::macro_table::{MacroDef, MacroKind, MacroRef}; use crate::lazy::expanded::r#struct::FieldExpr; @@ -1364,6 +1364,9 @@ impl<'top, D: Decoder> TemplateMacroInvocation<'top, D> { MacroKind::MakeDecimal => { MacroExpansionKind::MakeDecimal(MakeDecimalExpansion::new(arguments)) } + MacroKind::MakeTimestamp => { + MacroExpansionKind::MakeTimestamp(MakeTimestampExpansion::new(arguments)) + } MacroKind::MakeString => { MacroExpansionKind::MakeString(MakeTextExpansion::string_maker(arguments)) } diff --git a/src/types/decimal/mod.rs b/src/types/decimal/mod.rs index 1f54d33d..5dbaad02 100644 --- a/src/types/decimal/mod.rs +++ b/src/types/decimal/mod.rs @@ -1,6 +1,7 @@ //! Types related to [`Decimal`], the in-memory representation of an Ion decimal value. use std::cmp::Ordering; +use std::ops::Sub; use crate::decimal::coefficient::{Coefficient, Sign}; use crate::ion_data::{IonDataHash, IonDataOrd, IonEq}; @@ -244,6 +245,30 @@ impl Decimal { scaled_coefficient *= 10u128.pow(exponent_delta as u32); UInt::from(scaled_coefficient).cmp(&d2.coefficient().magnitude()) } + + /// Returns the integer part of `self`. This means that non-integer numbers are always + /// truncated towards zero. + pub fn trunc(&self) -> Decimal { + if self.exponent >= 0 { + *self + } else { + let mut coeff = self.coefficient().as_int().unwrap(); + coeff.data /= 10i128.pow(self.exponent.unsigned_abs() as u32); + Decimal::new(coeff, 0) + } + } + + /// Returns the fractional part of `self`. Values with no fractional component will return + /// zero. + pub fn fract(&self) -> Decimal { + if self.exponent >= 0 { + Decimal::ZERO + } else { + let mut coeff = self.coefficient().as_int().unwrap(); + coeff.data %= 10i128.pow(self.exponent.unsigned_abs() as u32); + Decimal::new(coeff, self.exponent) + } + } } impl PartialEq for Decimal { @@ -302,6 +327,27 @@ impl Ord for Decimal { } } +impl Sub for Decimal { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + // Scale the larger value up so that both coefficients are the same scaling factor apart + // and we can do integer arithmetic. + let mut lhs_int = self.coefficient().as_int().unwrap(); + let mut rhs_int = rhs.coefficient().as_int().unwrap(); + + let exp = if self.exponent > rhs.exponent { + lhs_int.data *= 10i128.pow((self.exponent - rhs.exponent) as u32); + rhs.exponent + } else { + rhs_int.data *= 10i128.pow((rhs.exponent - self.exponent) as u32); + self.exponent + }; + let new_coeff = lhs_int - rhs_int; + Decimal::new(new_coeff, exp) + } +} + macro_rules! impl_decimal_from_unsigned_primitive_integer { ($($t:ty),*) => ($( impl From<$t> for Decimal { @@ -759,4 +805,31 @@ mod decimal_tests { ) { assert_eq!(Decimal::new(coefficient, 0), expected); } + + #[rstest] + #[case(Decimal::new(1, 0), Decimal::new(1, 0), Decimal::new(0, 0))] + #[case(Decimal::new(-1, 0), Decimal::new(1, 0), Decimal::new(-2, 0))] + #[case(Decimal::new(1, -5), Decimal::new(1, 0), Decimal::new(-99999, -5))] + #[case(Decimal::new(1, 0), Decimal::new(1, 0), Decimal::new(0, 0))] + #[case(Decimal::new(-1, 0), Decimal::new(-2, 0), Decimal::new(1, 0))] + fn decimal_sub(#[case] lhs: Decimal, #[case] rhs: Decimal, #[case] expected: Decimal) { + assert_eq!(lhs - rhs, expected); + } + + #[rstest] + #[case(Decimal::new(1, 0), Decimal::new(1, 0))] + #[case(Decimal::new(15, -1), Decimal::new(1, 0))] + #[case(Decimal::new(105, -1), Decimal::new(10, 0))] + fn decimal_trunc(#[case] value: Decimal, #[case] expected: Decimal) { + assert_eq!(value.trunc(), expected); + } + + #[rstest] + #[case(Decimal::new(1, 0), Decimal::new(0, 0))] + #[case(Decimal::new(15, -1), Decimal::new(5, -1))] + #[case(Decimal::new(105, -1), Decimal::new(5, -1))] + fn decimal_fract(#[case] value: Decimal, #[case] expected: Decimal) { + assert_eq!(value.fract(), expected); + } + } diff --git a/src/types/mod.rs b/src/types/mod.rs index f700cb7b..f068d0a0 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -29,7 +29,7 @@ pub use r#struct::Struct; pub use sexp::SExp; pub use string::Str; pub use symbol::Symbol; -pub use timestamp::{HasMinute, Mantissa, Timestamp, TimestampBuilder, TimestampPrecision}; +pub use timestamp::{HasDay, HasFractionalSeconds, HasHour, HasMinute, HasMonth, HasOffset, HasSeconds, HasYear, Mantissa, Timestamp, TimestampBuilder, TimestampPrecision}; use crate::ion_data::{IonDataHash, IonDataOrd}; use std::cmp::Ordering;