From 121471256070fc20206a3a2433d74c2f0067aba1 Mon Sep 17 00:00:00 2001 From: andrewhallmark <46025270+TheCakeMonster@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:59:48 +0000 Subject: [PATCH] AM-339 - Added dateAdd() and dateTimeAsEpoch() Upgraded unit tests to .NET 8 --- .../DateAddTests.cs | 115 ++++++++++++++++++ .../DateTimeAsEpochTests.cs | 21 ++++ .../PanoramicData.NCalcExtensions.Test.csproj | 14 +-- PanoramicData.NCalcExtensions/DateTimeUnit.cs | 12 ++ .../ExtendedExpression.cs | 6 + .../ExtensionFunction.cs | 2 + .../Extensions/DateAdd.cs | 60 +++++++++ .../Extensions/DateTimeAsEpoch.cs | 14 +++ .../PanoramicData.NCalcExtensions.csproj | 6 +- README.md | 40 ++++++ global.json | 2 +- 11 files changed, 281 insertions(+), 11 deletions(-) create mode 100644 PanoramicData.NCalcExtensions.Test/DateAddTests.cs create mode 100644 PanoramicData.NCalcExtensions.Test/DateTimeAsEpochTests.cs create mode 100644 PanoramicData.NCalcExtensions/DateTimeUnit.cs create mode 100644 PanoramicData.NCalcExtensions/Extensions/DateAdd.cs create mode 100644 PanoramicData.NCalcExtensions/Extensions/DateTimeAsEpoch.cs diff --git a/PanoramicData.NCalcExtensions.Test/DateAddTests.cs b/PanoramicData.NCalcExtensions.Test/DateAddTests.cs new file mode 100644 index 0000000..fd4c905 --- /dev/null +++ b/PanoramicData.NCalcExtensions.Test/DateAddTests.cs @@ -0,0 +1,115 @@ +namespace PanoramicData.NCalcExtensions.Test; + +public class DateAddTests : NCalcTest +{ + [Theory] + [InlineData("2023-12-05T05:00:01Z", 250, "milliseconds", "2023-12-05T05:00:01.250Z")] + [InlineData("2023-12-05T05:00:01Z", 250, "Milliseconds", "2023-12-05T05:00:01.250Z")] + [InlineData("2023-12-05T05:00:01Z", 1, "seconds", "2023-12-05T05:00:02Z")] + [InlineData("2023-12-05T05:00:01Z", 1, "SECONDS", "2023-12-05T05:00:02Z")] + [InlineData("2023-12-05T05:00:01Z", 1, "minutes", "2023-12-05T05:01:01Z")] + [InlineData("2023-12-05T05:00:01Z", 1, "mInUtEs", "2023-12-05T05:01:01Z")] + [InlineData("2023-12-05T05:00:01Z", 1, "hours", "2023-12-05T06:00:01Z")] + [InlineData("2023-12-05T05:00:01Z", 1, "days", "2023-12-06T05:00:01Z")] + [InlineData("2023-12-05T05:00:01Z", 1, "months", "2024-01-05T05:00:01Z")] + [InlineData("2023-12-05T05:00:01Z", 1, "years", "2024-12-05T05:00:01Z")] + public void DateAdd_ParameterisedInput_GivesExpectedOutput(string initialDateAndTime, int quantity, string units, string expectedDateAndTime) + { + var recognised = DateTime.TryParse(initialDateAndTime, out var initialDateTime); + recognised.Should().BeTrue(); + + recognised = DateTime.TryParse(expectedDateAndTime, out var expectedDateTime); + recognised.Should().BeTrue(); + + var expression = new ExtendedExpression("dateAdd(initialDateTime, quantity, units)"); + expression.Parameters.Add("units", units); + expression.Parameters.Add("quantity", quantity); + expression.Parameters.Add("initialDateTime", initialDateTime); + + var result = expression.Evaluate(); + result.Should().BeOfType(); + result.Should().Be(expectedDateTime); + } + + [Theory] + [InlineData("2023-12-05T05:00:01Z", 250, "aa")] + [InlineData("2023-12-05T05:00:01Z", 1, "nanoseconds")] + [InlineData("2023-12-05T05:00:01Z", 1, "weeks")] + public void DateAdd_Unknownunits_ThrowsFormatException(string initialDateAndTime, int quantity, string units) + { + var recognised = DateTime.TryParse(initialDateAndTime, out var initialDateTime); + recognised.Should().BeTrue(); + + var expression = new ExtendedExpression("dateAdd(initialDateTime, quantity, units)"); + expression.Parameters.Add("units", units); + expression.Parameters.Add("quantity", quantity); + expression.Parameters.Add("initialDateTime", initialDateTime); + + var action = expression.Evaluate; + action.Should().Throw(); + } + + [Fact] + public void DateAdd_SubtractionBeyondMinDateTime_ThrowsArgumentOutOfRangeException() + { + var units = "Years"; + var quantity = -1000000; + var initialDateTime = new DateTime(2023, 12, 05, 05, 00, 01); + + var expression = new ExtendedExpression("dateAdd(initialDateTime, quantity, units)"); + expression.Parameters.Add("units", units); + expression.Parameters.Add("quantity", quantity); + expression.Parameters.Add("initialDateTime", initialDateTime); + + var action = expression.Evaluate; + action.Should().Throw(); + } + + [Fact] + public void DateAdd_IncorrectunitsDataType_ThrowsFormatException() + { + var units = 1; + var quantity = 1; + var initialDateTime = new DateTime(2023, 12, 05, 05, 00, 01); + + var expression = new ExtendedExpression("dateAdd(initialDateTime, quantity, units)"); + expression.Parameters.Add("units", units); + expression.Parameters.Add("quantity", quantity); + expression.Parameters.Add("initialDateTime", initialDateTime); + + var action = expression.Evaluate; + action.Should().Throw(); + } + + [Fact] + public void DateAdd_IncorrectquantityDataType_ThrowsFormatException() + { + var units = "Hours"; + var quantity = "Hours"; + var initialDateTime = new DateTime(2023, 12, 05, 05, 00, 01); + + var expression = new ExtendedExpression("dateAdd(initialDateTime, quantity, units)"); + expression.Parameters.Add("units", units); + expression.Parameters.Add("quantity", quantity); + expression.Parameters.Add("initialDateTime", initialDateTime); + + var action = expression.Evaluate; + action.Should().Throw(); + } + + [Fact] + public void DateAdd_IncorrectDateTimeDataType_ThrowsFormatException() + { + var units = "Hours"; + var quantity = 1; + var initialDateTime = new DateTime(2023, 12, 05, 05, 00, 01).ToString(CultureInfo.InvariantCulture); + + var expression = new ExtendedExpression("dateAdd(initialDateTime, quantity, units)"); + expression.Parameters.Add("units", units); + expression.Parameters.Add("quantity", quantity); + expression.Parameters.Add("initialDateTime", initialDateTime); + + var action = expression.Evaluate; + action.Should().Throw(); + } +} diff --git a/PanoramicData.NCalcExtensions.Test/DateTimeAsEpochTests.cs b/PanoramicData.NCalcExtensions.Test/DateTimeAsEpochTests.cs new file mode 100644 index 0000000..1acbbc5 --- /dev/null +++ b/PanoramicData.NCalcExtensions.Test/DateTimeAsEpochTests.cs @@ -0,0 +1,21 @@ +namespace PanoramicData.NCalcExtensions.Test; + +public class DateTimeAsEpochTests : NCalcTest +{ + [Fact] + public void DateTimeAsEpoch_ValidParameters_CalculatesCorrectValue() + { + var result = Test("dateTimeAsEpoch('20190702T000000', 'yyyyMMddTHHmmssK')"); + const long expectedDateTimeEpoch = 1562025600; + Assert.Equal(expectedDateTimeEpoch, result); + } + + [Fact] + public void DateTimeAsEpoch_ExpressionWithSquareBrackets_SuccessfullyInsertsParameter() + { + var expression = new ExtendedExpression("1 > dateTimeAsEpoch([connectMagic.systemItem.sys_updated_on], 'yyyy-MM-dd HH:mm:ss')"); + expression.Parameters.Add("connectMagic.systemItem.sys_updated_on", "2018-01-01 01:01:01"); + var result = expression.Evaluate(); + Assert.Equal(false, result); + } +} diff --git a/PanoramicData.NCalcExtensions.Test/PanoramicData.NCalcExtensions.Test.csproj b/PanoramicData.NCalcExtensions.Test/PanoramicData.NCalcExtensions.Test.csproj index ac6e0a7..a48ecf4 100644 --- a/PanoramicData.NCalcExtensions.Test/PanoramicData.NCalcExtensions.Test.csproj +++ b/PanoramicData.NCalcExtensions.Test/PanoramicData.NCalcExtensions.Test.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 latest false enable @@ -10,19 +10,19 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/PanoramicData.NCalcExtensions/DateTimeUnit.cs b/PanoramicData.NCalcExtensions/DateTimeUnit.cs new file mode 100644 index 0000000..f0b07ea --- /dev/null +++ b/PanoramicData.NCalcExtensions/DateTimeUnit.cs @@ -0,0 +1,12 @@ +namespace PanoramicData.NCalcExtensions; + +public enum DateTimeUnit +{ + Milliseconds, + Seconds, + Minutes, + Hours, + Days, + Months, + Years, +} diff --git a/PanoramicData.NCalcExtensions/ExtendedExpression.cs b/PanoramicData.NCalcExtensions/ExtendedExpression.cs index 652d2af..b3060bd 100644 --- a/PanoramicData.NCalcExtensions/ExtendedExpression.cs +++ b/PanoramicData.NCalcExtensions/ExtendedExpression.cs @@ -89,9 +89,15 @@ internal void Extend(string functionName, FunctionArgs functionArgs) case ExtensionFunction.Count: Count.Evaluate(functionArgs); return; + case ExtensionFunction.DateAdd: + DateAddMethods.Evaluate(functionArgs); + return; case ExtensionFunction.DateTime: DateTimeMethods.Evaluate(functionArgs); return; + case ExtensionFunction.DateTimeAsEpoch: + DateTimeAsEpoch.Evaluate(functionArgs); + return; case ExtensionFunction.DateTimeAsEpochMs: DateTimeAsEpochMs.Evaluate(functionArgs); return; diff --git a/PanoramicData.NCalcExtensions/ExtensionFunction.cs b/PanoramicData.NCalcExtensions/ExtensionFunction.cs index 9c3ea98..c2b83ed 100644 --- a/PanoramicData.NCalcExtensions/ExtensionFunction.cs +++ b/PanoramicData.NCalcExtensions/ExtensionFunction.cs @@ -13,7 +13,9 @@ public static class ExtensionFunction public const string Contains = "contains"; public const string Convert = "convert"; public const string Count = "count"; + public const string DateAdd = "dateAdd"; public const string DateTime = "dateTime"; + public const string DateTimeAsEpoch = "dateTimeAsEpoch"; public const string DateTimeAsEpochMs = "dateTimeAsEpochMs"; public const string Dictionary = "dictionary"; public const string Distinct = "distinct"; diff --git a/PanoramicData.NCalcExtensions/Extensions/DateAdd.cs b/PanoramicData.NCalcExtensions/Extensions/DateAdd.cs new file mode 100644 index 0000000..487cc77 --- /dev/null +++ b/PanoramicData.NCalcExtensions/Extensions/DateAdd.cs @@ -0,0 +1,60 @@ +namespace PanoramicData.NCalcExtensions.Extensions; + +/// +/// Methods for dates and time maths +/// +internal static class DateAddMethods +{ + internal static void Evaluate(FunctionArgs functionArgs) + { + if (functionArgs.Parameters.Length < 3) + { + throw new FormatException($"{ExtensionFunction.DateTime} function - you must pass 3 parameters."); + } + + // The date and time + if (functionArgs.Parameters[0].Evaluate() is not DateTime initialDateTime) + { + throw new FormatException($"{ExtensionFunction.DateAdd} function - The first argument should be a DateTime"); + } + + // The quantity + if (functionArgs.Parameters[1].Evaluate() is not int quantity) + { + throw new FormatException($"{ExtensionFunction.DateAdd} function - The second argument should be an integer"); + } + + // The units + if (functionArgs.Parameters[2].Evaluate() is not string unitsString) + { + throw new FormatException($"{ExtensionFunction.DateAdd} function - The third argument should be a string, e.g. 'days'"); + } + if (!Enum.TryParse(unitsString, true, out DateTimeUnit units)) + { + throw new FormatException($"{ExtensionFunction.DateAdd} function - The third argument is not a recognised unit, e.g. 'days'"); + } + + functionArgs.Result = Add(initialDateTime, quantity, units); + } + + /// + /// Perform the addition, returning the sum of the DateTime and the period + /// + /// The date and time that acts as the basis for the sum + /// The quantity of the units to be added + /// The units used to define the period to be added + /// The original DateTime plus the duration specified + /// The requested interval was not recognised + private static DateTime Add(DateTime initialDateTime, int quantity, DateTimeUnit units) + => units switch + { + DateTimeUnit.Milliseconds => initialDateTime.AddMilliseconds(quantity), + DateTimeUnit.Seconds => initialDateTime.AddSeconds(quantity), + DateTimeUnit.Minutes => initialDateTime.AddMinutes(quantity), + DateTimeUnit.Hours => initialDateTime.AddHours(quantity), + DateTimeUnit.Days => initialDateTime.AddDays(quantity), + DateTimeUnit.Months => initialDateTime.AddMonths(quantity), + DateTimeUnit.Years => initialDateTime.AddYears(quantity), + _ => throw new FormatException($"{ExtensionFunction.DateAdd} function - The requested units were not recognised") + }; +} diff --git a/PanoramicData.NCalcExtensions/Extensions/DateTimeAsEpoch.cs b/PanoramicData.NCalcExtensions/Extensions/DateTimeAsEpoch.cs new file mode 100644 index 0000000..2c1cf22 --- /dev/null +++ b/PanoramicData.NCalcExtensions/Extensions/DateTimeAsEpoch.cs @@ -0,0 +1,14 @@ +namespace PanoramicData.NCalcExtensions.Extensions; + +internal static class DateTimeAsEpoch +{ + internal static void Evaluate(FunctionArgs functionArgs) + { + var dateTimeOffset = DateTimeOffset.ParseExact( + functionArgs.Parameters[0].Evaluate() as string, // Input date as string + functionArgs.Parameters[1].Evaluate() as string, + CultureInfo.InvariantCulture.DateTimeFormat, + DateTimeStyles.AssumeUniversal); + functionArgs.Result = dateTimeOffset.ToUnixTimeSeconds(); + } +} \ No newline at end of file diff --git a/PanoramicData.NCalcExtensions/PanoramicData.NCalcExtensions.csproj b/PanoramicData.NCalcExtensions/PanoramicData.NCalcExtensions.csproj index d754e35..8d91a23 100644 --- a/PanoramicData.NCalcExtensions/PanoramicData.NCalcExtensions.csproj +++ b/PanoramicData.NCalcExtensions/PanoramicData.NCalcExtensions.csproj @@ -24,7 +24,7 @@ true portable - Added support for minValue() and maxValue(). + Added support for dateAdd() and dateTimeAsEpoch(). snupkg @@ -57,11 +57,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/README.md b/README.md index 218ffbd..807c6f1 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,9 @@ The NCalc documentation can be found [here (source code)](https://github.com/skl | [contains()](#contains) | Determines whether one string contains another. | | [convert()](#convert) | Converts the output of parameter 1 into the result of parameter 2. | | [count()](#count) | Counts the number of items. Optionally, only count those that match a lambda. | +| [dateAdd()](#dateAdd) | Add a specified interval to a DateTime. | | [dateTime()](#dateTime) | Return the DateTime in the specified format as a string, with an optional offset. | +| [dateTimeAsEpoch()](#datetimeasepoch) | Parses the input DateTime and outputs as seconds since the Epoch (1970-01-01T00:00Z). | | [dateTimeAsEpochMs()](#datetimeasepochms) | Parses the input DateTime and outputs as milliseconds since the Epoch (1970-01-01T00:00Z). | | [dictionary()](#dictionary) | Builds a Dictionary\ from the parameters provided. | | [distinct()](#distinct) | Returns only distinct items from the input. | @@ -294,6 +296,30 @@ Counts the number of items. Optionally, only count those that match a lambda. --- +### dateAdd() + +#### Purpose +Add a specified period to a DateTime. +The following units are supported: + * Years + * Months + * Days + * Hours + * Minutes + * Seconds + * Milliseconds + +#### Parameters + * intialDateTime - A DateTime to which to add the period specified + * quantity - The integer number of the units to be added + * units - A string representing the units used to specify the period to be added + +#### Examples + * dateAdd(toDateTime('2019-03-05 05:09', 'yyyy-MM-dd HH:mm'), -90, 'days') : 90 days before (2018-12-05 05:09:00) + * dateAdd(toDateTime('2019-03-05 01:03:05', 'yyyy-MM-dd HH:mm:ss'), 2, 'hours') : 2 hours later (2019-03-05 03:03:05) + +--- + ### dateTime() #### Purpose @@ -313,6 +339,20 @@ Return the DateTime in the specified format as a string, with an optional offset --- +### dateTimeAsEpoch() + +#### Purpose +Parses the input DateTime and outputs as seconds since the Epoch (1970-01-01T00:00Z). + +#### Parameters + * input date string + * format + +#### Examples + * dateTimeAsEpoch('20190702T000000', 'yyyyMMddTHHmmssK') : 1562025600 + +--- + ### dateTimeAsEpochMs() #### Purpose diff --git a/global.json b/global.json index 20a9854..aa8f2b3 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.307", + "version": "8.0.100", "rollForward": "latestFeature" } } \ No newline at end of file