Skip to content

Commit

Permalink
Fix TimeValue function, add test (#2731)
Browse files Browse the repository at this point in the history
Fixes #2674 .

The TimeValue function has a few issues, which are fixed by this change:
- Better validation (so that calls such as `TimeValue("1")` fail, as it
does in Excel
- Support for AM/PM designators (`TimeValue("6:00 PM")`), as it is
supported in Excel
- Support for wrapping hours (`Value(TimeValue("27:00:00")) =
Value(TimeValue("3:00:00"))`), as it is done in Excel
  • Loading branch information
CarlosFigueiraMSFT authored Nov 7, 2024
1 parent c5a3aea commit f891601
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 19 deletions.
8 changes: 7 additions & 1 deletion releasenotes/releasenotes-1.3.0-rc.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
`IsType(UntypedObject, Type)`\
`AsType(UntypedObject, Type)`

## Updated function behaviors:
- TimeValue function (https://github.com/microsoft/Power-Fx/pull/2731)
- Support for am/pm designators: `TimeValue("6:00pm")` now works (used to return an error)
- Better validation: `TimeValue("1")` would return a time value (equivalent to `Time(0,0,0)`), now returns an error
- Better support for wrapping times around the 24-hour mark: `TimeValue("27:00:00")` now returns the same as `Time(3,0,0)`, consistent with Excel's behavior.

## Other:
- Untyped object
- Read a field from an untyped object by index (https://github.com/microsoft/Power-Fx/pull/2555):
Expand All @@ -39,4 +45,4 @@
- Setting an untyped object via deep mutation is now supported (https://github.com/microsoft/Power-Fx/pull/2548):
`Set(untypedObject.Field, 99)`
`Set(Index(untypedObject, 1).Field, 99) // Reference field by name`
`Set(Index(Index(untypedObject, 1), 1), 99) // Reference field by index`
`Set(Index(Index(untypedObject, 1), 1), 99) // Reference field by index`
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.PowerFx.Core.IR;
using Microsoft.PowerFx.Core.Utils;
Expand Down Expand Up @@ -697,29 +698,50 @@ public static FormulaValue DateTimeParse(FormattingInfo formatInfo, IRContext ir
}
}

private static readonly Regex TimeValueRegex = new Regex(@"(?<hours>\d{1,2})\:(?<minutes>\d{2})(\:(?<seconds>\d{2})(\.(?<milliseconds>\d{1,3}))?)?");

public static FormulaValue TimeParse(EvalVisitor runner, EvalVisitorContext context, IRContext irContext, StringValue[] args)
{
var str = args[0].Value;

// culture will have Cultural info in-case one was passed in argument else it will have the default one.
CultureInfo culture = runner.CultureInfo;
if (args.Length > 1)
var dateTimeResult = DateTimeParse(runner, context, IRContext.NotInSource(FormulaType.DateTime), args);
if (dateTimeResult is DateTimeValue dateTimeValue)
{
var languageCode = args[1].Value;
if (!TextFormatUtils.TryGetCulture(languageCode, out culture))
{
return CommonErrors.BadLanguageCode(irContext, languageCode);
}
var dt = dateTimeValue.GetConvertedValue(runner.TimeZoneInfo);
var time = new TimeSpan(0, dt.Hour, dt.Minute, dt.Second, dt.Millisecond);
return new TimeValue(irContext, time);
}

if (TimeSpan.TryParse(str, runner.CultureInfo, out var result))
if (dateTimeResult is BlankValue)
{
return new TimeValue(irContext, result);
return dateTimeResult;
}
else

// Error; trying time-only options
var match = TimeValueRegex.Match(args[0].Value);
if (match.Success)
{
return CommonErrors.InvalidDateTimeParsingError(irContext);
var hours = int.Parse(match.Groups["hours"].Value, CultureInfo.InvariantCulture) % 24;
var minutes = int.Parse(match.Groups["minutes"].Value, CultureInfo.InvariantCulture);
var secondsText = match.Groups["seconds"]?.Value;
var seconds = string.IsNullOrEmpty(secondsText) ? 0 : int.Parse(secondsText, CultureInfo.InvariantCulture);
var millisecondsText = match.Groups["milliseconds"]?.Value;
int milliseconds = 0;
if (!string.IsNullOrEmpty(millisecondsText))
{
milliseconds = int.Parse(millisecondsText, CultureInfo.InvariantCulture);
if (millisecondsText.Length == 1)
{
milliseconds *= 100; // 12:34:56.7 === 12:34:56.700
}
else if (millisecondsText.Length == 2)
{
milliseconds *= 10; // 12:34:56.78 === 12:34:56.780
}
}

return new TimeValue(irContext, new TimeSpan(0, hours, minutes, seconds, milliseconds));
}

return dateTimeResult;
}

// Returns the number of minutes between UTC and either local or defined time zone
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
>> TimeValue("12:00:00")
Time(12,0,0,0)

>> TimeValue("27:00:00")
Time(3,0,0,0)

// TimeValue only returns values between [00:00:00.000, 23:59:59.999)
>> Value(TimeValue("27:00:00"))
0.125

>> TimeValue("6:00")
Time(6,0,0,0)

>> TimeValue("6:00 PM")
Time(18,0,0,0)

// Date portion is ignored
>> Value(TimeValue("10/12/2024 6:00:00 AM"))
0.25

>> TimeValue("29 Feb 2008 9:21:33 AM")
Time(9,21,33,0)

>> TimeValue("12:34:56.7")
Time(12,34,56,700)

>> TimeValue("12:34:56.78")
Time(12,34,56,780)

>> TimeValue("12:34:56.789")
Time(12,34,56,789)

// Extra digits on milliseconds are truncated
>> TimeValue("11:22:33.4449999")
Time(11,22,33,444)

>> TimeValue("6:01:02")
Time(6,1,2,0)

>> TimeValue("14:18")
Time(14,18,0,0)

>> TimeValue("24:11:11")
Time(0,11,11,0)

>> TimeValue("Not a time")
Error({Kind:ErrorKind.InvalidArgument})

>> TimeValue("One PM")
Error({Kind:ErrorKind.InvalidArgument})

>> TimeValue("1234")
Error({Kind:ErrorKind.InvalidArgument})

>> TimeValue("20241106073000")
Error({Kind:ErrorKind.InvalidArgument})
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,11 @@ Date(2000,1,10)
Time(0,0,0,0)

//Time-String
>> Switch("Case1","Case2",Time(6,30,30),"Case1","1")
Time(0,0,0,0)
>> Switch("Case1","Case2",Time(6,30,30),"Case1","12:34:56")
Time(12,34,56,0)

>> Switch("Case1","Case2",Time(6,30,30),"Case1","200")
Time(0,0,0,0)
>> Switch("Case1","Case2",Time(6,30,30),"Case1","6:00")
Time(6,0,0,0)

>> Switch("Case1","Case2",Time(6,30,30),"Case1","AB$%^")
Error({Kind:ErrorKind.InvalidArgument})
Expand Down

0 comments on commit f891601

Please sign in to comment.