diff --git a/.husky/pre-commit b/.husky/pre-commit index 8bb9487318..7bd9d45a25 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -19,4 +19,5 @@ ## or put your custom commands ------------------- #echo 'Husky.Net is awesome!' -dotnet husky run --name fantomas-format-staged-files \ No newline at end of file +dotnet husky run --name fantomas-format-staged-files +dotnet husky run --name ruff-format-staged-files diff --git a/.husky/task-runner.json b/.husky/task-runner.json index 32487dd697..22c86c7cc8 100644 --- a/.husky/task-runner.json +++ b/.husky/task-runner.json @@ -1,11 +1,32 @@ { - "tasks": [ - { - "name": "fantomas-format-staged-files", - "group": "pre-commit-operations", - "command": "dotnet", - "args": ["fantomas", "${staged}"], - "include": ["**/*.fs", "**/*.fsx", "**/*.fsi"] - } - ] + "tasks": [ + { + "name": "fantomas-format-staged-files", + "group": "pre-commit-operations", + "command": "dotnet", + "args": [ + "fantomas", + "${staged}" + ], + "include": [ + "**/*.fs", + "**/*.fsx", + "**/*.fsi" + ] + }, + { + "name": "ruff-format-staged-files", + "group": "pre-commit-operations", + "command": "poetry", + "args": [ + "run", + "ruff", + "format", + "${staged}" + ], + "include": [ + "**/*.py" + ] + } + ] } diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 60c5654f94..49d500c20d 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -12,6 +12,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Updated FCS to [fce0cf00585c12174fa3e51e4fc34afe784b9b4e](https://github.com/dotnet/fsharp/commits/fce0cf00585c12174fa3e51e4fc34afe784b9b4e) (by @ncave) +### Added + +#### Python + +* [GH-3645](https://github.com/fable-compiler/Fable/pull/3645) Add `TimeSpan.Parse` and `TimeSpan.TryParse` support to Python (by @MangelMaxime) + ## 4.7.0 - 2023-12-06 ### Added diff --git a/src/fable-library-py/fable_library/date_offset.py b/src/fable-library-py/fable_library/date_offset.py index c5b63e495c..97ac6e184f 100644 --- a/src/fable-library-py/fable_library/date_offset.py +++ b/src/fable-library-py/fable_library/date_offset.py @@ -6,10 +6,12 @@ from .time_span import TimeSpan from .types import FSharpRef + def timedelta_total_microseconds(td: timedelta) -> int: # timedelta doesn't expose total_microseconds # so we need to calculate it ourselves - return td.days * (24*3600) + td.seconds * 10**6 + td.microseconds + return td.days * (24 * 3600) + td.seconds * 10**6 + td.microseconds + def add(d: datetime, ts: timedelta) -> datetime: return d + ts @@ -67,7 +69,7 @@ def op_subtraction(x: datetime, y: datetime | TimeSpan) -> datetime | TimeSpan: if isinstance(y, TimeSpan): return x - timedelta(microseconds=time_span.total_microseconds(y)) - return time_span.create(0,0,0,0,0,timedelta_total_microseconds(x - y)) + return time_span.create(0, 0, 0, 0, 0, timedelta_total_microseconds(x - y)) def min_value() -> datetime: diff --git a/src/fable-library-py/fable_library/time_span.py b/src/fable-library-py/fable_library/time_span.py index ece50c617b..b7cea2b812 100644 --- a/src/fable-library-py/fable_library/time_span.py +++ b/src/fable-library-py/fable_library/time_span.py @@ -1,8 +1,10 @@ from __future__ import annotations +import re from math import ceil, floor, fmod from typing import Any +from .types import FSharpRef from .util import pad_left_and_right_with_zeros, pad_with_zeros @@ -72,11 +74,13 @@ def total_hours(ts: TimeSpan) -> float: def total_days(ts: TimeSpan) -> float: return ts / 864000000000 + def from_microseconds(micros: float) -> TimeSpan: - return create(0,0,0,0,0,micros) + return create(0, 0, 0, 0, 0, micros) + def from_milliseconds(msecs: int) -> TimeSpan: - return create(0,0,0,0,msecs) + return create(0, 0, 0, 0, msecs) def from_ticks(ticks: int) -> TimeSpan: @@ -178,6 +182,78 @@ def to_string(ts: TimeSpan, format: str = "c", _provider: Any | None = None) -> return f"{sign}{day_str}{hour_str}:{pad_with_zeros(m, 2)}:{pad_with_zeros(s, 2)}{ms_str}" +_time_span_parse_regex = re.compile( + r"^(-?)((\d+)\.)?(?:0*)([0-9]|0[0-9]|1[0-9]|2[0-3]):(?:0*)([0-5][0-9]|[0-9])(:(?:0*)([0-5][0-9]|[0-9]))?\.?(\d+)?$" +) + + +# Second argument is to not crash when using provides CultureInfo.InvariantCulture +def parse(string: str, _: Any | None = None) -> TimeSpan: + first_dot = string.find(".") + first_colon = string.find(":") + if first_dot == -1 and first_colon == -1: + # There is only a day ex: 4 + # parse as int + try: + d = int(string) + except Exception: + raise Exception("String '%s' was not recognized as a valid TimeSpan." % string) + return from_days(d) + if first_colon > 0: # process time part + r = _time_span_parse_regex.match(string) + if r is not None and r.group(4) is not None and r.group(5) is not None: + d = 0 + ms = 0 + s = 0 + sign = -1 if r.group(1) == "-" else 1 + h = int(r.group(4)) + m = int(r.group(5)) + if r.group(3) is not None: + d = int(r.group(3)) + if r.group(7) is not None: + s = int(r.group(7)) + if r.group(8) is not None: + # Depending on the number of decimals passed, we need to adapt the numbers + g_8: str = r.group(8) + match len(g_8): + case 1: + ms = int(g_8) * 100 + case 2: + ms = int(g_8) * 10 + case 3: + ms = int(g_8) + case 4: + ms = int(g_8) / 10 + case 5: + ms = int(g_8) / 100 + case 6: + ms = int(g_8) / 1000 + case 7: + ms = int(g_8) / 10000 + case _: + raise Exception("String '%s' was not recognized as a valid TimeSpan." % string) + return multiply(create(d, h, m, s, ms), sign) + raise Exception("String '%s' was not recognized as a valid TimeSpan." % string) + + +def try_parse( + string: str, def_value_or_format_provider: FSharpRef[TimeSpan] | Any, def_value: FSharpRef[TimeSpan] | None = None +) -> bool: + # Find where the out_value is + out_value: FSharpRef[TimeSpan] = def_value_or_format_provider + + # If we have 3 arguments, it means that the second argument is the format provider + if def_value is not None: + out_value = def_value + + try: + out_value.contents = parse(string) + except Exception: + return False + + return True + + __all__ = [ "create", "total_microseconds", @@ -202,4 +278,6 @@ def to_string(ts: TimeSpan, format: str = "c", _provider: Any | None = None) -> "subtract", "divide", "multiply", + "parse", + "try_parse", ] diff --git a/tests/Python/TestTimeSpan.fs b/tests/Python/TestTimeSpan.fs index 172e72cb59..48da9de4a6 100644 --- a/tests/Python/TestTimeSpan.fs +++ b/tests/Python/TestTimeSpan.fs @@ -233,3 +233,442 @@ let ``test TimeSpan implementation coherence`` () = // test_float 2200. 11. 200. // test_float -3000. 1.5 -2000. // test_float 0. 1000. 0. + + + // ************************************************** + // Test Cases From : + // https://docs.microsoft.com/en-us/dotnet/api/system.timespan.tryparse + // + // ************************************************** + // + // String to Parse TimeSpan + // --------------- --------------------- + // 0 00:00:00 + // 14 14.00:00:00 + // 1:2:3 01:02:03 + // 0:0:0.250 00:00:00.2500000 + // 10.20:30:40.050 10.20:30:40.0050000 + // 99.23:59:59.9990000 99.23:59:59.9999000 + // 0023:0059:0059.0009 23:59:59.0009000 + // 23:0:0 23:00:00 + // 24:0:0 Parse operation failed. (actually not true, it's parsed to 24 days...) + // 0:59:0 00:59:00 + // 0:60:0 Parse operation failed. + // 0:0:59 00:00:59 + // 0:0:60 Parse operation failed. + // 10: Parse operation failed. + // 10:0 10:00:00 + // :10 Parse operation failed. + // 0:10 00:10:00 + // 10:20: Parse operation failed. + // 10:20:0 10:20:00 + // .123 Parse operation failed. + // 0.12:00 12:00:00 + // 10. Parse operation failed. + // 10.12 Parse operation failed. + // 10.12:00 10.12:00:00 + +[] +let ``test TimeSpan 0 parse works`` () = + let actual = TimeSpan.Parse("0") + let expected = TimeSpan(0, 0, 0, 0, 0) + equal actual expected + +[] +let ``test TimeSpan 14 parse works`` () = + let actual = TimeSpan.Parse("14") + let expected = TimeSpan(14, 0, 0, 0, 0) + equal actual expected + +[] +let ``test TimeSpan 1:2:3 parse works`` () = + let actual = TimeSpan.Parse("1:2:3", CultureInfo.InvariantCulture) + let expected = TimeSpan(0, 1, 2, 3, 0) + equal actual expected + +[] +let ``test TimeSpan 0:0:0.250 parse works`` () = + let actual = TimeSpan.Parse("0:0:0.250") + let expected = TimeSpan(0, 0, 0, 0, 250) + equal actual expected + +[] +let ``test TimeSpan 10.20:30:40.050 parse works`` () = + let actual = TimeSpan.Parse("10.20:30:40.050") + let expected = TimeSpan(10, 20, 30, 40, 50) + equal actual expected + +[] +let ``test TimeSpan 99.23:59:59.999 parse works`` () = + let actual = TimeSpan.Parse("99.23:59:59.999") + let expected = TimeSpan(99, 23, 59, 59, 999) + equal actual expected + +[] +let ``test TimeSpan 0023:0059:0059.0099 parse works`` () = + let actual = TimeSpan.Parse("0023:0059:0059.009") + let expected = TimeSpan(00, 23, 59, 59, 9) + equal actual expected + +[] +let ``test TimeSpan 23:0:0 parse works`` () = + let actual = TimeSpan.Parse("23:0:0") + let expected = TimeSpan(0, 23, 0, 0, 0) + equal actual expected + +#if FABLE_COMPILER +// msft assumes it's 24 days... +// https://docs.microsoft.com/en-us/dotnet/api/system.timespan.parse +// or +// https://github.com/dotnet/dotnet-api-docs/blob/7f6a3882631bc008b858adfadb43cd17bbd55d49/xml/System/TimeSpan.xml#L2772 +[] +let ``test TimeSpan 24:0:0 parse fails`` () = + (fun _ -> TimeSpan.Parse("24:0:0")) + |> Util.throwsError "String '24:0:0' was not recognized as a valid TimeSpan." + +[] +let ``test TimeSpan 24:0:0 TryParse fails`` () = + let status, _ = TimeSpan.TryParse("24:0:0", CultureInfo.InvariantCulture) + equal status false +#endif + +[] +let ``test TimeSpan 0:0:59 parse works`` () = + let actual = TimeSpan.Parse("0:0:59") + let expected = TimeSpan(0, 0, 0, 59, 0) + equal actual expected + +#if FABLE_COMPILER // temporary, TODO: fix test to be backwards compatible +[] +let ``test TimeSpan 0:60:0 parse fails`` () = + (fun _ -> TimeSpan.Parse("0:60:0")) +#if FABLE_COMPILER + |> Util.throwsError "String '0:60:0' was not recognized as a valid TimeSpan." +#else + |> Util.throwsError "The TimeSpan string '0:60:0' could not be parsed because at least one of the numeric components is out of range or contains too many digits." +#endif +#endif + +#if FABLE_COMPILER // temporary, TODO: fix test to be backwards compatible +[] +let ``test TimeSpan 10: parse fails`` () = + (fun _ -> TimeSpan.Parse("10:")) + |> Util.throwsError "String '10:' was not recognized as a valid TimeSpan." +#endif + +[] +let ``test TimeSpan 10:0 parse works`` () = + let actual = TimeSpan.Parse("10:0") + let expected = TimeSpan(0, 10, 0, 0, 0) + equal actual expected + +#if FABLE_COMPILER // temporary, TODO: fix test to be backwards compatible +[] +let ``test TimeSpan 10:20: parse fails`` () = + (fun _ -> TimeSpan.Parse("10:20:")) + |> Util.throwsError "String '10:20:' was not recognized as a valid TimeSpan." +#endif + +[] +let ``test TimeSpan 10:20:0 parse works`` () = + let actual = TimeSpan.Parse("10:20:0") + let expected = TimeSpan(0, 10, 20, 0, 0) + equal actual expected + + +#if FABLE_COMPILER // temporary, TODO: fix test to be backwards compatible +[] +let ``test TimeSpan .123 parse fails`` () = + (fun _ -> TimeSpan.Parse(".123")) + |> Util.throwsError "String '.123' was not recognized as a valid TimeSpan." +#endif + +[] +let ``test TimeSpan 0.12:00 parse works`` () = + let actual = TimeSpan.Parse("0.12:00") + let expected = TimeSpan(0, 12, 00, 0, 0) + equal actual expected + +#if FABLE_COMPILER // temporary, TODO: fix test to be backwards compatible +[] +let ``test TimeSpan 10. parse fails`` () = + (fun _ -> TimeSpan.Parse("10.")) + |> Util.throwsError "String '10.' was not recognized as a valid TimeSpan." +#endif + +#if FABLE_COMPILER // temporary, TODO: fix test to be backwards compatible +[] +let ``test TimeSpan 10.12 parse fails`` () = + (fun _ -> TimeSpan.Parse("10.12")) + |> Util.throwsError "String '10.12' was not recognized as a valid TimeSpan." +#endif + +[] +let ``test TimeSpan 10.12:00 parse works`` () = + let actual = TimeSpan.Parse("10.12:00") + let expected = TimeSpan(10, 12, 00, 0, 0) + equal actual expected + +[] +let ``test TimeSpan 0 TryParse works`` () = + let status, actual = TimeSpan.TryParse("0") + let expected = TimeSpan(0, 0, 0, 0, 0) + equal status true + equal actual expected + +[] +let ``test TimeSpan 14 TryParse works`` () = + let status, actual = TimeSpan.TryParse("14") + let expected = TimeSpan(14, 0, 0, 0, 0) + equal status true + equal actual expected + +[] +let ``test TimeSpan 1:2:3 TryParse works`` () = + let status, actual = TimeSpan.TryParse("1:2:3") + let expected = TimeSpan(0, 1, 2, 3, 0) + equal status true + equal actual expected + +[] +let ``test TimeSpan 0:0:0.250 TryParse works`` () = + let status, actual = TimeSpan.TryParse("0:0:0.250") + let expected = TimeSpan(0, 0, 0, 0, 250) + equal status true + equal actual expected + +[] +let ``test TimeSpan 10.20:30:40.050 TryParse works`` () = + let status, actual = TimeSpan.TryParse("10.20:30:40.050") + let expected = TimeSpan(10, 20, 30, 40, 50) + equal status true + equal actual expected + +[] +let ``test TimeSpan 99.23:59:59.999 TryParse works`` () = + let status, actual = TimeSpan.TryParse("99.23:59:59.999") + let expected = TimeSpan(99, 23, 59, 59, 999) + equal status true + equal actual expected + +[] +let ``test TimeSpan 0023:0059:0059.009 TryParse works`` () = + let status, actual = TimeSpan.TryParse("0023:0059:0059.009") + let expected = TimeSpan(00, 23, 59, 59, 9) + equal status true + equal actual expected + +[] +let ``test TimeSpan 23:0:0 TryParse works`` () = + let status, actual = TimeSpan.TryParse("23:0:0") + let expected = TimeSpan(0, 23, 0, 0, 0) + equal status true + equal actual expected + +[] +let ``test TimeSpan 0:0:59 TryParse works`` () = + let status, actual = TimeSpan.TryParse("0:0:59") + let expected = TimeSpan(0, 0, 0, 59, 0) + equal status true + equal actual expected + +[] +let ``test TimeSpan 0:60:0 TryParse fails`` () = + let status, _ = TimeSpan.TryParse("0:60:0") + equal status false + +[] +let ``test TimeSpan 10: TryParse fails`` () = + let status, _ = TimeSpan.TryParse("10:") + equal status false +[] +let ``test TimeSpan 10:0 TryParse works`` () = + let status, actual = TimeSpan.TryParse("10:0") + let expected = TimeSpan(0, 10, 0, 0, 0) + equal status true + equal actual expected + +[] +let ``test TimeSpan 10:20: TryParse fails`` () = + let status, _ = TimeSpan.TryParse("10:20:") + equal status false + +[] +let ``test TimeSpan 10:20:0 TryParse works`` () = + let status, actual = TimeSpan.TryParse("10:20:0") + let expected = TimeSpan(0, 10, 20, 0, 0) + equal status true + equal actual expected + +[] +let ``test TimeSpan .123 TryParse fails`` () = + let status, _ = TimeSpan.TryParse(".123") + equal status false + +[] +let ``test TimeSpan 0.12:00 TryParse works`` () = + let status, actual = TimeSpan.TryParse("0.12:00") + let expected = TimeSpan(0, 12, 00, 0, 0) + equal status true + equal actual expected + +[] +let ``test TimeSpan 10. TryParse fails`` () = + let status, _ = TimeSpan.TryParse("10.") + equal status false + +[] +let ``test TimeSpan 10.12 TryParse fails`` () = + let status, _ = TimeSpan.TryParse("10.12") + equal status false + +[] +let ``test TimeSpan 10.12:00 TryParse works`` () = + let status, actual = TimeSpan.TryParse("10.12:00") + let expected = TimeSpan(10, 12, 00, 0, 0) + equal status true + equal actual expected + +[] +let ``test TimeSpan 00:00:00.1 Parse handle correctly the milliseconds`` () = + let actual = TimeSpan.Parse("00:00:00.1").TotalMilliseconds + let expected = 100. + equal actual expected + +[] +let ``test TimeSpan 00:00:00.12 Parse handle correctly the milliseconds`` () = + let actual = TimeSpan.Parse("00:00:00.12").TotalMilliseconds + let expected = 120. + equal actual expected + +[] +let ``test TimeSpan 00:00:00.123 Parse handle correctly the milliseconds`` () = + let actual = TimeSpan.Parse("00:00:00.123").TotalMilliseconds + let expected = 123. + equal actual expected + +[] +let ``test TimeSpan 00:00:00.1234 Parse handle correctly the milliseconds`` () = + let actual = TimeSpan.Parse("00:00:00.1234").TotalMilliseconds + let expected = 123.4 + equal actual expected + +[] +let ``test TimeSpan 00:00:00.12345 Parse handle correctly the milliseconds`` () = + let actual = TimeSpan.Parse("00:00:00.12345").TotalMilliseconds + let expected = 123.45 + equal actual expected + +[] +let ``test TimeSpan 00:00:00.123456 Parse handle correctly the milliseconds`` () = + let actual = TimeSpan.Parse("00:00:00.123456").TotalMilliseconds + let expected = 123.456 + equal actual expected + +[] +let ``test TimeSpan 00:00:00.0034567 Parse handle correctly the milliseconds`` () = + let actual = TimeSpan.Parse("00:00:00.0034567").TotalMilliseconds + let expected = 3.4567 + equal actual expected + +[] +let ``test TimeSpan Parse work with negative TimeSpan`` () = + let actual = TimeSpan.Parse("-2:00:00") + equal actual.TotalMilliseconds -7200000.0 + +[] +let ``test TimeSpan 1.23:45:06.789 TotalMilliseconds works`` () = + let actual = TimeSpan.Parse("1.23:45:06.789").TotalMilliseconds + let expected = 171906789.0 + equal actual expected + +[] +let ``test TimeSpan 1.23:45:06.789 TotalSeconds works`` () = + let actual = TimeSpan.Parse("1.23:45:06.789").TotalSeconds + let expected = 171906.789 + equal actual expected + +[] +let ``test TimeSpan 1.23:45:06.789 TotalMinutes works`` () = + let actual = TimeSpan.Parse("1.23:45:06.789").TotalMinutes + let expected = 2865.11315 + equal actual expected + +[] +let ``test TimeSpan 1.23:45:06.789 TotalHours works`` () = + let actual = TimeSpan.Parse("1.23:45:06.789").TotalHours + let expected = 47.75188583333333 + equal actual expected + +[] +let ``test TimeSpan 1.23:45:06.789 TotalDays works`` () = + let actual = TimeSpan.Parse("1.23:45:06.789").TotalDays + let expected = 1.9896619097222221 + equal actual expected + +[] +let ``test TimeSpan TotalSeconds & friends work`` () = + let ts = TimeSpan.FromDays(0.005277777778) + ts.TotalMilliseconds |> equal 456000.0 + ts.TotalSeconds |> equal 456. + ts.TotalMinutes |> equal 7.6 + +[] +let ``test TimeSpan.Milliseconds works with positive TimeSpan`` () = + let actual = TimeSpan.Parse("1.23:45:06.789").Milliseconds + let expected = 789 + equal actual expected + +[] +let ``test TimeSpan.Milliseconds works with negative TimeSpan`` () = + let actual = TimeSpan.Parse("-1.23:45:06.78999").Milliseconds + let expected = -789 + equal actual expected + +[] +let ``test TimeSpan.Seconds works with positive TimeSpan`` () = + let actual = TimeSpan.Parse("1.23:45:06.789").Seconds + let expected = 6 + equal actual expected + +[] +let ``test TimeSpan.Seconds works with negative TimeSpan`` () = + let actual = TimeSpan.Parse("-1.23:45:06.78999").Seconds + let expected = -6 + equal actual expected + +[] +let ``test TimeSpan.Minutes works with positive TimeSpan`` () = + let actual = TimeSpan.Parse("1.23:45:06.789").Minutes + let expected = 45 + equal actual expected + +[] +let ``test TimeSpan.Minutes works with negative TimeSpan`` () = + let actual = TimeSpan.Parse("-1.23:45:06.78999").Minutes + let expected = -45 + equal actual expected + +[] +let ``test TimeSpan.Hours works with positive TimeSpan`` () = + let actual = TimeSpan.Parse("1.23:45:06.789").Hours + let expected = 23 + equal actual expected + +[] +let ``test TimeSpan.Hours works with negative TimeSpan`` () = + let actual = TimeSpan.Parse("-1.23:45:06.78999").Hours + let expected = -23 + equal actual expected + +[] +let ``test TimeSpan.Days works with positive TimeSpan`` () = + let actual = TimeSpan.Parse("1.23:45:06.789").Days + let expected = 1 + equal actual expected + +[] +let ``test TimeSpan.Days works with negative TimeSpan`` () = + let actual = TimeSpan.Parse("-1.23:45:06.78999").Days + let expected = -1 + equal actual expected