Skip to content

Commit

Permalink
AM-339 - Added dateAdd() and dateTimeAsEpoch()
Browse files Browse the repository at this point in the history
Upgraded unit tests to .NET 8
  • Loading branch information
TheCakeMonster committed Dec 5, 2023
1 parent d96c1fb commit 1214712
Show file tree
Hide file tree
Showing 11 changed files with 281 additions and 11 deletions.
115 changes: 115 additions & 0 deletions PanoramicData.NCalcExtensions.Test/DateAddTests.cs
Original file line number Diff line number Diff line change
@@ -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<DateTime>();
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<FormatException>();
}

[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<ArgumentOutOfRangeException>();
}

[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<FormatException>();
}

[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<FormatException>();
}

[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<FormatException>();
}
}
21 changes: 21 additions & 0 deletions PanoramicData.NCalcExtensions.Test/DateTimeAsEpochTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
Expand All @@ -10,19 +10,19 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="FluentAssertions.Analyzers" Version="0.17.2">
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="FluentAssertions.Analyzers" Version="0.26.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Meraki.Api" Version="1.30.26" />
<PackageReference Include="Microsoft.Net.Compilers.Toolset" Version="4.4.0">
<PackageReference Include="Microsoft.Net.Compilers.Toolset" Version="4.8.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
12 changes: 12 additions & 0 deletions PanoramicData.NCalcExtensions/DateTimeUnit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace PanoramicData.NCalcExtensions;

public enum DateTimeUnit
{
Milliseconds,
Seconds,
Minutes,
Hours,
Days,
Months,
Years,
}
6 changes: 6 additions & 0 deletions PanoramicData.NCalcExtensions/ExtendedExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions PanoramicData.NCalcExtensions/ExtensionFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
60 changes: 60 additions & 0 deletions PanoramicData.NCalcExtensions/Extensions/DateAdd.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
namespace PanoramicData.NCalcExtensions.Extensions;

/// <summary>
/// Methods for dates and time maths
/// </summary>
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);
}

/// <summary>
/// Perform the addition, returning the sum of the DateTime and the period
/// </summary>
/// <param name="initialDateTime">The date and time that acts as the basis for the sum</param>
/// <param name="quantity">The quantity of the units to be added</param>
/// <param name="units">The units used to define the period to be added</param>
/// <returns>The original DateTime plus the duration specified</returns>
/// <exception cref="FormatException">The requested interval was not recognised</exception>
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")
};
}
14 changes: 14 additions & 0 deletions PanoramicData.NCalcExtensions/Extensions/DateTimeAsEpoch.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<EmbedAllSources>true</EmbedAllSources>
<DebugType>portable</DebugType>

<PackageReleaseNotes>Added support for minValue() and maxValue().</PackageReleaseNotes>
<PackageReleaseNotes>Added support for dateAdd() and dateTimeAsEpoch().</PackageReleaseNotes>

<SymbolPackageFormat>snupkg</SymbolPackageFormat>

Expand Down Expand Up @@ -57,11 +57,11 @@

<ItemGroup>
<PackageReference Include="CoreCLR-NCalc" Version="2.2.110" />
<PackageReference Include="Nerdbank.GitVersioning" Version="3.5.119">
<PackageReference Include="Nerdbank.GitVersioning" Version="3.6.133">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="TimeZoneConverter" Version="6.1.0" />

</ItemGroup>
Expand Down
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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\<string, object?\> from the parameters provided. |
| [distinct()](#distinct) | Returns only distinct items from the input. |
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "7.0.307",
"version": "8.0.100",
"rollForward": "latestFeature"
}
}

0 comments on commit 1214712

Please sign in to comment.