Skip to content
Open
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
32 changes: 28 additions & 4 deletions src/libraries/Microsoft.PowerFx.Json/FormulaValueJSON.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,41 @@ public static FormulaValue FromJson(JsonElement element, FormulaValueJsonSeriali
return FromJson(element, settings, new FormulaValueJsonSerializerWorkingData(), formulaType);
}

// // caller verified element is non-null and is of type string
// // caller verified element is non-null and is of type string
internal static FormulaValue ParseDate(JsonElement element, FormulaType targetType, Func<DateTime, FormulaValue> funcParse)
{
var strValue = element.GetString();
var strValue = element.GetString();
if (string.IsNullOrWhiteSpace(strValue))
{
return FormulaValue.NewBlank(targetType);
}

// Any exceptions will be caught at higher level.
var dateTime = element.GetDateTime();
DateTime dateTime;

try
{
dateTime = element.GetDateTime();
}
catch (FormatException)
{
// element.GetDateTime() uses a strict ISO 8601 parser that rejects valid
// date formats commonly returned by connectors (e.g. timezone offsets
// without a colon like "+0000" instead of "+00:00").
// Fall back to the more lenient DateTimeOffset.Parse.
if (DateTimeOffset.TryParse(strValue, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dto))
{
dateTime = dto.UtcDateTime;
}
else
{
return new ErrorValue(IRContext.NotInSource(targetType), new ExpressionError()
{
Message = $"Date '{strValue}' could not be parsed",
Span = new Syntax.Span(0, 0),
Kind = ErrorKind.InvalidArgument
});
}
}

var value = funcParse(dateTime);
return value;
Expand Down
42 changes: 39 additions & 3 deletions src/tests/Microsoft.PowerFx.Json.Tests.Shared/ParseJSONTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -601,14 +601,16 @@ public void ParseDates_Error()
using var doc = JsonDocument.Parse("\"123\""); // not a date
var je = doc.RootElement;

Assert.Throws<FormatException>(() =>
FormulaValueJSON.ParseDate(je, FormulaType.DateTime, (datetime) => throw new InvalidOperationException($"don't invoke this")));
var value = FormulaValueJSON.ParseDate(je, FormulaType.DateTime, (datetime) => throw new InvalidOperationException($"don't invoke this"));

var errorValue = Assert.IsType<ErrorValue>(value);
Assert.Contains("could not be parsed", errorValue.Errors[0].Message);
}

[Fact]
public void ParseDates_Value()
{
using var doc = JsonDocument.Parse("\"2024-10-02T23:13:50.123456\""); // not a date
using var doc = JsonDocument.Parse("\"2024-10-02T23:13:50.123456\"");
var je = doc.RootElement;

var value = FormulaValueJSON.ParseDate(je, FormulaType.DateTime, (datetime) => FormulaValue.New(datetime));
Expand All @@ -617,5 +619,39 @@ public void ParseDates_Value()
var dt = dtValue.GetConvertedValue(TimeZoneInfo.Local);
Assert.Equal(2024, dt.Year);
}

[Fact]
public void ParseDates_LenientTimezoneOffset()
{
// Jira-style date with timezone offset without colon (+0000 instead of +00:00)
// System.Text.Json's GetDateTime() rejects this, but DateTimeOffset.Parse handles it
using var doc = JsonDocument.Parse("\"2026-03-26T19:18:26.729+0000\"");
var je = doc.RootElement;

var value = FormulaValueJSON.ParseDate(je, FormulaType.DateTime, (datetime) => FormulaValue.New(datetime));

var dtValue = Assert.IsType<DateTimeValue>(value);
var dt = dtValue.GetConvertedValue(TimeZoneInfo.Utc);
Assert.Equal(2026, dt.Year);
Assert.Equal(3, dt.Month);
Assert.Equal(26, dt.Day);
Assert.Equal(19, dt.Hour);
Assert.Equal(18, dt.Minute);
}

[Fact]
public void ParseDates_LenientTimezoneOffset_DateOnly()
{
using var doc = JsonDocument.Parse("\"2026-03-26T19:18:26.729+0000\"");
var je = doc.RootElement;

var value = FormulaValueJSON.ParseDate(je, FormulaType.Date, (datetime) => FormulaValue.NewDateOnly(datetime.Date));

var dtValue = Assert.IsType<DateValue>(value);
var dt = dtValue.GetConvertedValue(TimeZoneInfo.Utc);
Assert.Equal(2026, dt.Year);
Assert.Equal(3, dt.Month);
Assert.Equal(26, dt.Day);
}
}
}