Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@v3

- name: Test
run: dotnet test --configuration Release
run: dotnet test -c Release

- name: Test - Compiled
run: dotnet test --configuration Release /p:Compiled=true
4 changes: 1 addition & 3 deletions Fluid.Tests/Fluid.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net5.0;net6.0;net7.0</TargetFrameworks>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<DefineConstants Condition="'$(Compiled)' == 'true'">$(DefineConstants);COMPILED</DefineConstants>
<!-- Ignore: The target framework '...' is out of support and will not receive security updates in the future -->
<NoWarn>NETSDK1138</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
28 changes: 25 additions & 3 deletions Fluid.Tests/MiscFiltersTests.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using System;
using Fluid.Filters;
using Fluid.Values;
using System;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Fluid.Filters;
using Fluid.Values;
using TimeZoneConverter;
using Xunit;

namespace Fluid.Tests
Expand Down Expand Up @@ -302,6 +303,27 @@ public async Task ChangeTimeZone(string initialDateTime, string timeZone, string
Assert.Equal(expected, ((DateTimeOffset)result.ToObjectValue()).ToString("yyyy-MM-ddTHH:mm:ssK"));
}

[Theory]
[InlineData("2022-12-13T21:02:18.399+00:00", "utc", "2022-12-13T21:02:18.399+00:00")]
[InlineData("2022-12-13T21:02:18.399+00:00", "America/New_York", "2022-12-13T21:02:18.399+00:00")]
[InlineData("2022-12-13T21:02:18.399+00:00", "Australia/Adelaide", "2022-12-13T21:02:18.399+00:00")]
[InlineData("2022-12-13T21:02:18.399", "utc", "2022-12-13T21:02:18.399+00:00")]
[InlineData("2022-12-13T21:02:18.399", "America/New_York", "2022-12-13T21:02:18.399-05:00")]
[InlineData("2022-12-13T21:02:18.399", "Australia/Adelaide", "2022-12-13T21:02:18.399+10:30")]
[InlineData("2022-12-13T21:02:18.399+01:00", "utc", "2022-12-13T21:02:18.399+01:00")] // Parsed as UTC+1, converted to UTC
public async Task DateFilterUsesContextTimezone(string initialDateTime, string timeZone, string expected)
{
// - When a TZ is provided in the source string, the resulting DateTimeOffset uses it
// - When no TZ is provided, we assume the local offset (context.TimeZone)

var input = new StringValue(initialDateTime);
var context = new TemplateContext { TimeZone = TZConvert.GetTimeZoneInfo(timeZone) };

var date = await MiscFilters.Date(input, new FilterArguments(new StringValue(RoundTripDateTimePattern)), context);

Assert.Equal(expected, date.ToStringValue());
}

[Theory]
[InlineData("2020-05-18T02:13:09+00:00", "America/New_York", "%l:%M%P", "10:13pm")]
[InlineData("2020-05-18T02:13:09+00:00", "Europe/London", "%l:%M%P", "3:13am")]
Expand Down
56 changes: 42 additions & 14 deletions Fluid/Values/FluidValueExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
using TimeZoneConverter;

namespace Fluid.Values
{
Expand All @@ -8,10 +9,13 @@ public static class FluidValueExtensions
private const string Now = "now";
private const string Today = "today";

// The K specifier is optional when used in TryParseExact, so
// if a TZ is not specified, it will still match

private static readonly string[] DefaultFormats = {
"yyyy-MM-ddTHH:mm:ss.FFF",
"yyyy-MM-ddTHH:mm:ss",
"yyyy-MM-ddTHH:mm",
"yyyy-MM-ddTHH:mm:ss.FFFK",
"yyyy-MM-ddTHH:mm:ssK",
"yyyy-MM-ddTHH:mmK",
"yyyy-MM-dd",
"yyyy-MM",
"yyyy"
Expand Down Expand Up @@ -53,6 +57,8 @@ public static bool TryGetDateTimeInput(this FluidValue input, TemplateContext co

if (input.Type == FluidValues.String)
{
var timeZoneProvided = false;

var stringValue = input.ToStringValue();

if (stringValue == Now || stringValue == Today)
Expand All @@ -63,30 +69,52 @@ public static bool TryGetDateTimeInput(this FluidValue input, TemplateContext co
{
var success = true;

if (!DateTime.TryParseExact(stringValue, DefaultFormats, context.CultureInfo, DateTimeStyles.None, out var dateTime))
// Use DateTimeOffset.Parse to extract the TZ if it's specified.
// We then verify if a TZ was set in the source string by using DateTime.Parse's Kind which will return Unspecified if not set.

if (!DateTimeOffset.TryParseExact(stringValue, DefaultFormats, context.CultureInfo, DateTimeStyles.AssumeUniversal, out result))
{
if (!DateTime.TryParseExact(stringValue, SecondaryFormats, context.CultureInfo, DateTimeStyles.None, out dateTime))
if (!DateTimeOffset.TryParseExact(stringValue, SecondaryFormats, context.CultureInfo, DateTimeStyles.AssumeUniversal, out result))
{
if (!DateTime.TryParse(stringValue, context.CultureInfo, DateTimeStyles.None, out dateTime))
if (!DateTimeOffset.TryParse(stringValue, context.CultureInfo, DateTimeStyles.AssumeUniversal, out result))
{
if (!DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.None, out dateTime))
if (!DateTimeOffset.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result))
{
success = false;
}
else
{
timeZoneProvided = DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.None, out var timeZoneDateTime) && timeZoneDateTime.Kind != DateTimeKind.Unspecified;
}
}
else
{
timeZoneProvided = DateTime.TryParse(stringValue, context.CultureInfo, DateTimeStyles.None, out var timeZoneDateTime) && timeZoneDateTime.Kind != DateTimeKind.Unspecified;
}
}
else
{
timeZoneProvided = DateTime.TryParseExact(stringValue, SecondaryFormats, context.CultureInfo, DateTimeStyles.None, out var timeZoneDateTime) && timeZoneDateTime.Kind != DateTimeKind.Unspecified;
}
}
else
{
timeZoneProvided = DateTime.TryParseExact(stringValue, DefaultFormats, context.CultureInfo, DateTimeStyles.None, out var timeZoneDateTime) && timeZoneDateTime.Kind != DateTimeKind.Unspecified;
}

// If no timezone is specified, assume local using the configured timezone
if (success)
{
if (dateTime.Kind == DateTimeKind.Unspecified)
{
result = new DateTimeOffset(dateTime, context.TimeZone.GetUtcOffset(dateTime));
}
else
// If no timezone is specified in the source string, only use the date time part of the result
if (!timeZoneProvided)
{
result = new DateTimeOffset(dateTime);
// A timezone is represented as a UTC offset, but this can vary based on daylight saving times.
// Hence we don't use context.TimeZone.BaseUtcOffset which is fixed, but TimeZone.GetUtcOffset
// to get the actual timezone offset at the moment of the parsed date and time

var dateTime = result.DateTime;
var offset = context.TimeZone.GetUtcOffset(dateTime);

result = new DateTimeOffset(dateTime, offset);
}
}

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,10 +372,12 @@ Tuesday, August 1, 2017

### System time zone

`TemplateOptions` and `TemplateContext` provides a property to define a default time zone to use when parsing date and times. The default value is the current system's time zone.
When dates and times are parsed and don't specify a time zone, the default one is assumed. Setting a custom one can also prevent different environments (data centers) from
`TemplateOptions` and `TemplateContext` provides a property to define a default time zone to use when parsing date and times. The default value is the current system's time zone. Setting a custom one can also prevent different environments (data centers) from
generating different results.

- When dates and times are parsed and don't specify a time zone, the configured one is assumed.
- When a time zone is provided in the source string, the resulting date time uses it.

> Note: The `date` filter conforms to the Ruby date and time formats https://ruby-doc.org/core-3.0.0/Time.html#method-i-strftime. To use the .NET standard date formats, use the `format_date` filter.

#### Source
Expand Down