diff --git a/src/libraries/Microsoft.PowerFx.Json/FormulaValueJSON.cs b/src/libraries/Microsoft.PowerFx.Json/FormulaValueJSON.cs index aa705a3af5..2e4497621b 100644 --- a/src/libraries/Microsoft.PowerFx.Json/FormulaValueJSON.cs +++ b/src/libraries/Microsoft.PowerFx.Json/FormulaValueJSON.cs @@ -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 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; diff --git a/src/tests/Microsoft.PowerFx.Json.Tests.Shared/ParseJSONTests.cs b/src/tests/Microsoft.PowerFx.Json.Tests.Shared/ParseJSONTests.cs index d6bd56018a..1552c418a0 100644 --- a/src/tests/Microsoft.PowerFx.Json.Tests.Shared/ParseJSONTests.cs +++ b/src/tests/Microsoft.PowerFx.Json.Tests.Shared/ParseJSONTests.cs @@ -601,14 +601,16 @@ public void ParseDates_Error() using var doc = JsonDocument.Parse("\"123\""); // not a date var je = doc.RootElement; - Assert.Throws(() => - 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(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)); @@ -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(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(value); + var dt = dtValue.GetConvertedValue(TimeZoneInfo.Utc); + Assert.Equal(2026, dt.Year); + Assert.Equal(3, dt.Month); + Assert.Equal(26, dt.Day); + } } }