From 17c4364b431febb805545a2d930319a4b407a2f1 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 22 Nov 2023 22:03:32 +0100 Subject: [PATCH 1/8] time_span.py has been rewritten to be represented using float --- src/Fable.Transforms/Python/Replacements.fs | 34 +--- src/fable-library-py/fable_library/date.py | 9 +- .../fable_library/date_offset.py | 27 +-- .../fable_library/time_span.py | 170 +++++++++++------- src/quicktest-py/quicktest.fsx | 89 +++++++++ 5 files changed, 227 insertions(+), 102 deletions(-) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index fa46c12cc2..57aa1b9c2e 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -5246,32 +5246,14 @@ let timeSpans ?loc = r ) |> Some - | "FromMilliseconds" -> - //TypeCast(args.Head, t) |> Some - Helper.LibCall( - com, - "time_span", - "from_milliseconds", - t, - args, - i.SignatureArgTypes, - ?thisArg = thisArg, - ?loc = r - ) - |> Some - | "get_TotalMilliseconds" -> - //TypeCast(thisArg.Value, t) |> Some - Helper.LibCall( - com, - "time_span", - "to_milliseconds", - t, - args, - i.SignatureArgTypes, - ?thisArg = thisArg, - ?loc = r - ) - |> Some + // | "FromMilliseconds" -> + // //TypeCast(args.Head, t) |> Some + // Helper.LibCall(com, "time_span", "from_milliseconds", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + // |> Some + // | "get_TotalMilliseconds" -> + // //TypeCast(thisArg.Value, t) |> Some + // Helper.LibCall(com, "time_span", "to_milliseconds", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + // |> Some | "ToString" when (args.Length = 1) -> "TimeSpan.ToString with one argument is not supported, because it depends of local culture, please add CultureInfo.InvariantCulture" |> addError com ctx.InlinePath r diff --git a/src/fable-library-py/fable_library/date.py b/src/fable-library-py/fable_library/date.py index 7995e84b13..de1fbd8de8 100644 --- a/src/fable-library-py/fable_library/date.py +++ b/src/fable-library-py/fable_library/date.py @@ -7,13 +7,18 @@ from .types import FSharpRef from .util import DateKind +from .time_span import TimeSpan, create as create_time_span formatRegExp = re.compile(r"(\w)\1*") -def op_subtraction(x: datetime, y: datetime) -> timedelta: - return x - y +def op_subtraction(x: datetime, y: datetime) -> TimeSpan: + delta = x - y + # ts.microseconds only contains the microseconds provided to the constructor + # so we need to calculate the total microseconds ourselves + deltaMicroseconds = delta.days * (24*3600) + delta.seconds * 10**6 + delta.microseconds + return create_time_span(0,0,0,0,0,deltaMicroseconds) def create( diff --git a/src/fable-library-py/fable_library/date_offset.py b/src/fable-library-py/fable_library/date_offset.py index 2c5522dd15..16742ee9f8 100644 --- a/src/fable-library-py/fable_library/date_offset.py +++ b/src/fable-library-py/fable_library/date_offset.py @@ -2,8 +2,14 @@ from datetime import datetime, timedelta, timezone +from . import time_span +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 def add(d: datetime, ts: timedelta) -> datetime: return d + ts @@ -30,17 +36,18 @@ def create( h: int, m: int, s: int, - ms: int | timedelta, - offset: timedelta | None = None, + ms: int | TimeSpan, + offset: TimeSpan | None = None, ) -> datetime: - if isinstance(ms, timedelta): - offset = ms + pythonOffset: timedelta | None = None + if isinstance(ms, TimeSpan): + pythonOffset = timedelta(microseconds=time_span.total_microseconds(ms)) ms = 0 - if offset is None: + if pythonOffset is None: return datetime(year, month, day, h, m, s, ms) - tzinfo = timezone(offset) + tzinfo = timezone(pythonOffset) return datetime(year, month, day, h, m, s, ms, tzinfo=tzinfo) @@ -56,11 +63,11 @@ def op_addition(x: datetime, y: timedelta) -> datetime: return x + y -def op_subtraction(x: datetime, y: datetime | timedelta) -> datetime | timedelta: - if isinstance(y, timedelta): - return x - y +def op_subtraction(x: datetime, y: datetime | TimeSpan) -> datetime | TimeSpan: + if isinstance(y, TimeSpan): + return x - timedelta(microseconds=time_span.total_microseconds(y)) - return 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 11397644a4..80826c693c 100644 --- a/src/fable-library-py/fable_library/time_span.py +++ b/src/fable-library-py/fable_library/time_span.py @@ -6,118 +6,155 @@ from .util import pad_left_and_right_with_zeros, pad_with_zeros -def total_seconds(ts: timedelta) -> float: - return ts.total_seconds() +# TimeSpan is represented as an int which is the Tick value +# We can recompute everything from this value +class TimeSpan(int): + pass -def total_days(ts: timedelta) -> float: - return total_seconds(ts) / 86400 +def create( + days: float = 0, + hours: float | None = None, + minutes: float | None = None, + seconds: float | None = None, + milliseconds: float | None = None, + microseconds: float | None = None, +) -> TimeSpan: + match (days, hours, minutes, seconds, milliseconds, microseconds): + # ticks constructor + case (_, None, None, None, None, None): + return TimeSpan(days) + # hours, minutes, seconds constructor + case (_, _, _, None, None, None): + print("hours, minutes, seconds constructor") + seconds = minutes + minutes = hours + hours = days + days = 0 + # others constructor follows the correct order of arguments + case _: + pass + + return TimeSpan( + days * 864000000000 + + (hours or 0) * 36000000000 + + (minutes or 0) * 600000000 + + (seconds or 0) * 10000000 + + (milliseconds or 0) * 10000 + + (microseconds or 0) * 10 + ) -def total_minutes(ts: timedelta) -> float: - return total_seconds(ts) / 60 +def total_nanoseconds(ts: TimeSpan) -> float: + # We store timespan as the Tick value so nanoseconds step is 100 + return ts * 100 -def total_hours(ts: timedelta) -> float: - return total_seconds(ts) / 3600 +def total_microseconds(ts: TimeSpan) -> float: + return ts / 10 -def from_milliseconds(msecs: int) -> timedelta: - return timedelta(milliseconds=msecs) +def total_milliseconds(ts: TimeSpan) -> float: + return ts / 10000 -def from_ticks(ticks: int) -> timedelta: - return timedelta(microseconds=ticks // 10) +def total_seconds(ts: TimeSpan) -> float: + return ts / 10000000 -def from_seconds(s: int) -> timedelta: - return timedelta(seconds=s) +def total_minutes(ts: TimeSpan) -> float: + return ts / 600000000 -def from_minutes(m: int) -> timedelta: - return timedelta(minutes=m) +def total_hours(ts: TimeSpan) -> float: + return ts / 36000000000 -def from_hours(h: int) -> timedelta: - return timedelta(hours=h) +def total_days(ts: TimeSpan) -> float: + return ts / 864000000000 +def from_microseconds(micros: float) -> TimeSpan: + return create(0,0,0,0,0,micros) -def from_days(d: int) -> timedelta: - return timedelta(days=d) +def from_milliseconds(msecs: int) -> TimeSpan: + return create(0,0,0,0,msecs) -def to_milliseconds(td: timedelta) -> int: - return int(td.total_seconds() * 1000) +def from_ticks(ticks: int) -> TimeSpan: + return create(ticks) -def days(ts: timedelta) -> int: - return int(total_seconds(ts) / 86400) +def from_seconds(s: int) -> TimeSpan: + return create(0, 0, s) -def hours(ts: timedelta) -> int: - return int(abs(total_seconds(ts)) % 86400 / 3600) +def from_minutes(m: int) -> TimeSpan: + return create(0, m, 0) -def minutes(ts: timedelta) -> int: - return int(abs(total_seconds(ts)) % 3600 / 60) +def from_hours(h: int) -> TimeSpan: + return create(h, 0, 0) -def seconds(ts: timedelta) -> int: - return int(abs(total_seconds(ts)) % 60) +def from_days(d: int) -> TimeSpan: + return create(d, 0, 0, 0) -def milliseconds(ts: timedelta) -> int: - return int(total_seconds(ts) % 1 * 1000) +def ticks(ts: TimeSpan) -> int: + return int(ts) -def ticks(ts: timedelta) -> int: - return int(total_seconds(ts) * 10000000) +def microseconds(ts: TimeSpan) -> int: + return int(ts % 10000 / 10) -def negate(ts: timedelta) -> timedelta: - return -ts +def milliseconds(ts: TimeSpan) -> int: + return int(ts % 10000000 / 10000) -def duration(ts: timedelta) -> timedelta: - if ts < timedelta(0): - return -ts +def seconds(ts: TimeSpan) -> int: + return int(ts % 600000000 / 10000000) - return ts +def minutes(ts: TimeSpan) -> int: + return int(ts % 36000000000 / 600000000) -def add(ts: timedelta, other: timedelta) -> timedelta: - return ts + other +def hours(ts: TimeSpan) -> int: + return int(ts % 864000000000 / 36000000000) -def subtract(ts: timedelta, other: timedelta) -> timedelta: - return ts - other +def days(ts: TimeSpan) -> int: + return int(ts / 864000000000) -def multiply(ts: timedelta, factor: int) -> timedelta: - return ts * factor +def negate(ts: TimeSpan) -> TimeSpan: + return TimeSpan(-ts) -def divide(ts: timedelta, divisor: int) -> timedelta: - return ts / divisor +def duration(ts: TimeSpan) -> TimeSpan: + return TimeSpan(abs(int(ts))) -def create( - d: int = 0, - h: int | None = None, - m: int | None = None, - s: int | None = None, - ms: int | None = None, -) -> timedelta: - if h is None and m is None and s is None and ms is None: - return from_ticks(d) - elif s is None and ms is None: - return timedelta(hours=d or 0, minutes=h or 0, seconds=m or 0) +def add(ts: TimeSpan, other: TimeSpan) -> TimeSpan: + return TimeSpan(ts + other) + + +def subtract(ts: TimeSpan, other: TimeSpan) -> TimeSpan: + return TimeSpan(ts - other) + - return timedelta(days=d, hours=h or 0, minutes=m or 0, seconds=s or 0, milliseconds=ms or 0) +def multiply(ts: TimeSpan, factor: float) -> TimeSpan: + # We represents TimeSpan as a Tick which can't be a float + # This also allows us + return TimeSpan(int(ts * factor)) -def to_string(ts: timedelta, format: str = "c", _provider: Any | None = None) -> str: +def divide(ts: TimeSpan, divisor: float) -> TimeSpan: + return TimeSpan(int(ts / divisor)) + + +def to_string(ts: TimeSpan, format: str = "c", _provider: Any | None = None) -> str: if format not in ["c", "g", "G"]: raise ValueError("Custom formats are not supported") @@ -126,7 +163,7 @@ def to_string(ts: timedelta, format: str = "c", _provider: Any | None = None) -> m = minutes(ts) s = seconds(ts) ms = abs(milliseconds(ts)) - sign: str = "-" if ts < timedelta(0) else "" + sign: str = "-" if ts < 0 else "" ms_str = ( "" @@ -143,9 +180,14 @@ def to_string(ts: timedelta, format: str = "c", _provider: Any | None = None) -> __all__ = [ "create", - "to_milliseconds", - "to_string", + "total_microseconds", + "total_milliseconds", + "total_seconds", + "total_minutes", + "total_hours", + "total_days", "from_ticks", + "from_microseconds", "from_milliseconds", "from_hours", "from_minutes", diff --git a/src/quicktest-py/quicktest.fsx b/src/quicktest-py/quicktest.fsx index 8185ef42dd..de39409b39 100644 --- a/src/quicktest-py/quicktest.fsx +++ b/src/quicktest-py/quicktest.fsx @@ -1,8 +1,13 @@ #r "nuget:Fable.Python" open Fable.Core +open Fable.Core.Testing open Fable.Core.PyInterop open Fable.Python.Builtins +open System + +let equal expected actual = + Assert.AreEqual(expected, actual) [] let main argv = @@ -13,4 +18,88 @@ let main argv = // use file = builtins.``open``(StringPath "data.txt") // file.read() |> printfn "File contents: %s" + let t1 = TimeSpan(10, 10, 10) + let t2 = TimeSpan(1, 1, 1, 1, 1, 1) + let t3 = TimeSpan(1, 1, 1, 1, 1) + + // printfn "%A" t1.TotalSeconds + // printfn "%A" t2.TotalSeconds + // printfn "%A" t3.TotalMilliseconds + + // printfn "%A" t3.TotalMicroseconds + // printfn "%A" t3.TotalMilliseconds + // printfn "%A" t3.TotalSeconds + // printfn "%A" t3.TotalMinutes + // printfn "%A" t3.TotalHours + // printfn "%A" t3.TotalDays + // printfn "%A" 1.042372697 + // printfn "%A" (t3.TotalDays = 1.042372697) + + // TimeSpan(10, 20, 30, 10, 10, 10).Ticks |> printfn "TotalMicroseconds: %A" + // TimeSpan(10, 20, 30, 10, 5, 12).Milliseconds |> printfn "TotalMicroseconds: %A" + // TimeSpan(10, 20, 30, 10, -5, 12).Milliseconds |> printfn "TotalMicroseconds: %A" + // TimeSpan(10, 20, 30, 10, 10, 10).Milliseconds |> printfn "Milliseconds: %A" + // TimeSpan(10, 20, 30, 10, 10, 10).Seconds |> printfn "Seconds: %A" + // TimeSpan(10, 20, 30, 10, 10, 10).Minutes |> printfn "Minutes: %A" + // TimeSpan(10, 20, 30, 10, 10, 10).Hours |> printfn "Hours: %A" + // TimeSpan(10, 20, 30, 10, 10, 10).Days |> printfn "TotalDays: %A" + + // TimeSpan(10, 0, 0, 0, 0).Divide(3.26).Ticks |> printfn "TotalMicroseconds: %A" + + // TimeSpan.FromTicks(2650306748466L).TotalSeconds |> printfn "TotalSeconds: %A" + + // TimeSpan(1).TotalNanoseconds |> printfn "TotalNanoseconds: %A" + // TimeSpan(1).Ticks |> printfn "Ticks: %A" + // TimeSpan.FromHours(3).TotalHours |> printfn "TotalHours: %A" + // TimeSpan.FromHours(3).Hours |> printfn "TotalHours: %A" + // TimeSpan(0,3,0,0,0).Hours |> printfn "TotalHours: %A" + + // TimeSpan.fro + + // TimeSpan(10, 20, 30, 10, 10, 10).TotalMicroseconds.ToString() |> printfn "TotalMicroseconds: %A" + // TimeSpan(10, 20, 30, 10, 10, 10).TotalMilliseconds.ToString() |> printfn "TotalMilliseconds: %A" + // TimeSpan(10, 20, 30, 10, 10, 10).TotalSeconds.ToString() |> printfn "TotalSeconds: %A" + // TimeSpan(10, 20, 30, 10, 10, 10).TotalMinutes.ToString() |> printfn "TotalMinutes: %A" + // TimeSpan(10, 20, 30, 10, 10, 10).TotalHours.ToString() |> printfn "TotalHours: %A" + // TimeSpan(10, 20, 30, 10, 10, 10).TotalDays.ToString() |> printfn "TotalDays: %A" + + // TimeSpan(10, 20, 30, 10, 10, 10).Ticks.ToString() |> printfn "TotalDays: %A" + + // printfn "%A" (TimeSpan(10, 20, 30, 10, 10, 10).TotalMicroseconds = 9.3781001e+11) + + // printfn "%A" 9.3781001e+11 + + // TimeSpan(10, 20, 30, 10, 10, 10).TotalDays |> printfn "%A" + // printfn "%A" 10.85428252 + + + async { + let mutable _aggregate = 0 + + let makeWork i = + async { + // check that the individual work items run sequentially and not interleaved + _aggregate <- _aggregate + i + let copyOfI = _aggregate + do! Async.Sleep 100 + equal copyOfI _aggregate + do! Async.Sleep 100 + equal copyOfI _aggregate + return i + } + let works = [ for i in 1 .. 5 -> makeWork i ] + let now = DateTimeOffset.Now + let! result = Async.Sequential works + let ``then`` = DateTimeOffset.Now + let d = ``then`` - now + if d.TotalSeconds < 0.99 then + failwithf "expected sequential operations to take 1 second or more, but took %.3f" d.TotalSeconds + result |> equal [| 1 .. 5 |] + result |> Seq.sum |> equal _aggregate + } |> Async.RunSynchronously + + + + + 0 From 6ca9911dc6e8a635131233fc4d31d68737be2631 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 22 Nov 2023 22:41:55 +0100 Subject: [PATCH 2/8] Fix negative TimeSpan support --- .../fable_library/time_span.py | 21 ++--- src/quicktest-py/quicktest.fsx | 87 +------------------ 2 files changed, 13 insertions(+), 95 deletions(-) diff --git a/src/fable-library-py/fable_library/time_span.py b/src/fable-library-py/fable_library/time_span.py index 80826c693c..a9c392cfbf 100644 --- a/src/fable-library-py/fable_library/time_span.py +++ b/src/fable-library-py/fable_library/time_span.py @@ -1,9 +1,9 @@ from __future__ import annotations -from datetime import timedelta from typing import Any from .util import pad_left_and_right_with_zeros, pad_with_zeros +from math import fmod, ceil, floor # TimeSpan is represented as an int which is the Tick value @@ -105,27 +105,28 @@ def ticks(ts: TimeSpan) -> int: def microseconds(ts: TimeSpan) -> int: - return int(ts % 10000 / 10) + return int(fmod(ts, 10000) / 10) def milliseconds(ts: TimeSpan) -> int: - return int(ts % 10000000 / 10000) + return int(fmod(ts, 10000000) / 10000) def seconds(ts: TimeSpan) -> int: - return int(ts % 600000000 / 10000000) + return int(fmod(ts, 600000000) / 10000000) def minutes(ts: TimeSpan) -> int: - return int(ts % 36000000000 / 600000000) + return int(fmod(ts, 36000000000) / 600000000) def hours(ts: TimeSpan) -> int: - return int(ts % 864000000000 / 36000000000) + return int(fmod(ts, 864000000000) / 36000000000) def days(ts: TimeSpan) -> int: - return int(ts / 864000000000) + res = ts / 864000000000 + return ceil(res) if res < 0 else floor(res) def negate(ts: TimeSpan) -> TimeSpan: @@ -159,9 +160,9 @@ def to_string(ts: TimeSpan, format: str = "c", _provider: Any | None = None) -> raise ValueError("Custom formats are not supported") d = abs(days(ts)) - h = hours(ts) - m = minutes(ts) - s = seconds(ts) + h = abs(hours(ts)) + m = abs(minutes(ts)) + s = abs(seconds(ts)) ms = abs(milliseconds(ts)) sign: str = "-" if ts < 0 else "" diff --git a/src/quicktest-py/quicktest.fsx b/src/quicktest-py/quicktest.fsx index de39409b39..974b9a53f7 100644 --- a/src/quicktest-py/quicktest.fsx +++ b/src/quicktest-py/quicktest.fsx @@ -7,7 +7,8 @@ open Fable.Python.Builtins open System let equal expected actual = - Assert.AreEqual(expected, actual) + // According the console log arguments are reversed + Assert.AreEqual(actual, expected) [] let main argv = @@ -18,88 +19,4 @@ let main argv = // use file = builtins.``open``(StringPath "data.txt") // file.read() |> printfn "File contents: %s" - let t1 = TimeSpan(10, 10, 10) - let t2 = TimeSpan(1, 1, 1, 1, 1, 1) - let t3 = TimeSpan(1, 1, 1, 1, 1) - - // printfn "%A" t1.TotalSeconds - // printfn "%A" t2.TotalSeconds - // printfn "%A" t3.TotalMilliseconds - - // printfn "%A" t3.TotalMicroseconds - // printfn "%A" t3.TotalMilliseconds - // printfn "%A" t3.TotalSeconds - // printfn "%A" t3.TotalMinutes - // printfn "%A" t3.TotalHours - // printfn "%A" t3.TotalDays - // printfn "%A" 1.042372697 - // printfn "%A" (t3.TotalDays = 1.042372697) - - // TimeSpan(10, 20, 30, 10, 10, 10).Ticks |> printfn "TotalMicroseconds: %A" - // TimeSpan(10, 20, 30, 10, 5, 12).Milliseconds |> printfn "TotalMicroseconds: %A" - // TimeSpan(10, 20, 30, 10, -5, 12).Milliseconds |> printfn "TotalMicroseconds: %A" - // TimeSpan(10, 20, 30, 10, 10, 10).Milliseconds |> printfn "Milliseconds: %A" - // TimeSpan(10, 20, 30, 10, 10, 10).Seconds |> printfn "Seconds: %A" - // TimeSpan(10, 20, 30, 10, 10, 10).Minutes |> printfn "Minutes: %A" - // TimeSpan(10, 20, 30, 10, 10, 10).Hours |> printfn "Hours: %A" - // TimeSpan(10, 20, 30, 10, 10, 10).Days |> printfn "TotalDays: %A" - - // TimeSpan(10, 0, 0, 0, 0).Divide(3.26).Ticks |> printfn "TotalMicroseconds: %A" - - // TimeSpan.FromTicks(2650306748466L).TotalSeconds |> printfn "TotalSeconds: %A" - - // TimeSpan(1).TotalNanoseconds |> printfn "TotalNanoseconds: %A" - // TimeSpan(1).Ticks |> printfn "Ticks: %A" - // TimeSpan.FromHours(3).TotalHours |> printfn "TotalHours: %A" - // TimeSpan.FromHours(3).Hours |> printfn "TotalHours: %A" - // TimeSpan(0,3,0,0,0).Hours |> printfn "TotalHours: %A" - - // TimeSpan.fro - - // TimeSpan(10, 20, 30, 10, 10, 10).TotalMicroseconds.ToString() |> printfn "TotalMicroseconds: %A" - // TimeSpan(10, 20, 30, 10, 10, 10).TotalMilliseconds.ToString() |> printfn "TotalMilliseconds: %A" - // TimeSpan(10, 20, 30, 10, 10, 10).TotalSeconds.ToString() |> printfn "TotalSeconds: %A" - // TimeSpan(10, 20, 30, 10, 10, 10).TotalMinutes.ToString() |> printfn "TotalMinutes: %A" - // TimeSpan(10, 20, 30, 10, 10, 10).TotalHours.ToString() |> printfn "TotalHours: %A" - // TimeSpan(10, 20, 30, 10, 10, 10).TotalDays.ToString() |> printfn "TotalDays: %A" - - // TimeSpan(10, 20, 30, 10, 10, 10).Ticks.ToString() |> printfn "TotalDays: %A" - - // printfn "%A" (TimeSpan(10, 20, 30, 10, 10, 10).TotalMicroseconds = 9.3781001e+11) - - // printfn "%A" 9.3781001e+11 - - // TimeSpan(10, 20, 30, 10, 10, 10).TotalDays |> printfn "%A" - // printfn "%A" 10.85428252 - - - async { - let mutable _aggregate = 0 - - let makeWork i = - async { - // check that the individual work items run sequentially and not interleaved - _aggregate <- _aggregate + i - let copyOfI = _aggregate - do! Async.Sleep 100 - equal copyOfI _aggregate - do! Async.Sleep 100 - equal copyOfI _aggregate - return i - } - let works = [ for i in 1 .. 5 -> makeWork i ] - let now = DateTimeOffset.Now - let! result = Async.Sequential works - let ``then`` = DateTimeOffset.Now - let d = ``then`` - now - if d.TotalSeconds < 0.99 then - failwithf "expected sequential operations to take 1 second or more, but took %.3f" d.TotalSeconds - result |> equal [| 1 .. 5 |] - result |> Seq.sum |> equal _aggregate - } |> Async.RunSynchronously - - - - - 0 From 53b61bd9353ef996bb6fbfe618005019bc6adb99 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 24 Nov 2023 15:17:09 +0100 Subject: [PATCH 3/8] Remove unused code --- src/Fable.Transforms/Python/Replacements.fs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 57aa1b9c2e..c9ab306dbf 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -5246,14 +5246,6 @@ let timeSpans ?loc = r ) |> Some - // | "FromMilliseconds" -> - // //TypeCast(args.Head, t) |> Some - // Helper.LibCall(com, "time_span", "from_milliseconds", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - // |> Some - // | "get_TotalMilliseconds" -> - // //TypeCast(thisArg.Value, t) |> Some - // Helper.LibCall(com, "time_span", "to_milliseconds", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - // |> Some | "ToString" when (args.Length = 1) -> "TimeSpan.ToString with one argument is not supported, because it depends of local culture, please add CultureInfo.InvariantCulture" |> addError com ctx.InlinePath r From b2b096d77a5697fdcac45c32acba06fe89d6d953 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 24 Nov 2023 15:17:41 +0100 Subject: [PATCH 4/8] Don't make Ticks constructor a special case, the Python `create` function can handle it just fine --- src/Fable.Transforms/Python/Replacements.fs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index c9ab306dbf..ed00b94354 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -5231,20 +5231,7 @@ let timeSpans // let callee = match i.callee with Some c -> c | None -> i.args.Head match i.CompiledName with | ".ctor" -> - let meth = - match args with - | [ ticks ] -> "fromTicks" - | _ -> "create" - - Helper.LibCall( - com, - "time_span", - meth, - t, - args, - i.SignatureArgTypes, - ?loc = r - ) + Helper.LibCall(com, "time_span", "create", t, args, i.SignatureArgTypes, ?loc = r) |> Some | "ToString" when (args.Length = 1) -> "TimeSpan.ToString with one argument is not supported, because it depends of local culture, please add CultureInfo.InvariantCulture" From 5658f6bdc293f6f4dc2cb5a8c5a7a17765ec50a3 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 24 Nov 2023 15:54:11 +0100 Subject: [PATCH 5/8] Remove debug print --- src/fable-library-py/fable_library/time_span.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fable-library-py/fable_library/time_span.py b/src/fable-library-py/fable_library/time_span.py index a9c392cfbf..c168412415 100644 --- a/src/fable-library-py/fable_library/time_span.py +++ b/src/fable-library-py/fable_library/time_span.py @@ -26,7 +26,6 @@ def create( return TimeSpan(days) # hours, minutes, seconds constructor case (_, _, _, None, None, None): - print("hours, minutes, seconds constructor") seconds = minutes minutes = hours hours = days From f17051f1bb726e4067a65dda3066d050a3263009 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 24 Nov 2023 15:55:02 +0100 Subject: [PATCH 6/8] Add tests to TimeSpans to check the coherence of values --- tests/Python/Fable.Tests.Python.fsproj | 2 +- tests/Python/TestTimeSpan.fs | 59 +++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index e564631241..cac5e1cb5a 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -1,7 +1,7 @@ Exe - net6.0 + net8.0 Major false false diff --git a/tests/Python/TestTimeSpan.fs b/tests/Python/TestTimeSpan.fs index 13fdb727ab..172e72cb59 100644 --- a/tests/Python/TestTimeSpan.fs +++ b/tests/Python/TestTimeSpan.fs @@ -11,12 +11,12 @@ let ``test TimeSpan.ToString() works`` () = TimeSpan.FromSeconds(12345.).ToString() |> equal "03:25:45" TimeSpan.FromDays(18.).ToString() |> equal "18.00:00:00" TimeSpan.FromMilliseconds(25.).ToString() |> equal "00:00:00.0250000" - + [] let ``test TimeSpan.ToString() works for negative TimeSpan`` () = TimeSpan.FromSeconds(-5.).ToString() |> equal "-00:00:05" TimeSpan.FromDays(-5.23).ToString() |> equal "-5.05:31:12" - + [] let ``test TimeSpan.ToString(\"c\", CultureInfo.InvariantCulture) works`` () = TimeSpan(0L).ToString("c", CultureInfo.InvariantCulture) |> equal "00:00:00" @@ -37,7 +37,7 @@ let ``test TimeSpan.ToString(\"General long\", CultureInfo.InvariantCulture) wor TimeSpan.FromSeconds(12345.).ToString("G", CultureInfo.InvariantCulture) |> equal "0:03:25:45.0000000" TimeSpan.FromDays(18.).ToString("G", CultureInfo.InvariantCulture) |> equal "18:00:00:00.0000000" TimeSpan.FromMilliseconds(25.).ToString("G", CultureInfo.InvariantCulture) |> equal "0:00:00:00.0250000" - + [] let ``test TimeSpan constructors work`` () = let t1 = TimeSpan(20000L) @@ -124,6 +124,53 @@ let ``test TimeSpan Addition works`` () = test -2000. 0. -2000. test 0. 0. 0. +[] +let ``test TimeSpan implementation coherence`` () = + TimeSpan.FromTicks(1L).Ticks |> equal 1L + TimeSpan.FromMilliseconds(1).Milliseconds |> equal 1 + TimeSpan.FromMilliseconds(1).TotalMilliseconds |> equal 1. + TimeSpan.FromSeconds(1).Seconds |> equal 1 + TimeSpan.FromSeconds(1).TotalSeconds |> equal 1. + TimeSpan.FromMinutes(1).Minutes |> equal 1 + TimeSpan.FromMinutes(1).TotalMinutes |> equal 1. + TimeSpan.FromHours(1).Hours |> equal 1 + TimeSpan.FromHours(1).TotalHours |> equal 1. + TimeSpan.FromDays(1).Days |> equal 1 + TimeSpan.FromDays(1).TotalDays |> equal 1. + + TimeSpan.FromMilliseconds(-1).Milliseconds |> equal -1 + TimeSpan.FromMilliseconds(-1).TotalMilliseconds |> equal -1. + TimeSpan.FromSeconds(-1).Seconds |> equal -1 + TimeSpan.FromSeconds(-1).TotalSeconds |> equal -1. + TimeSpan.FromMinutes(-1).Minutes |> equal -1 + TimeSpan.FromMinutes(-1).TotalMinutes |> equal -1. + TimeSpan.FromHours(-1).Hours |> equal -1 + TimeSpan.FromHours(-1).TotalHours |> equal -1. + TimeSpan.FromDays(-1).Days |> equal -1 + TimeSpan.FromDays(-1).TotalDays |> equal -1. + + TimeSpan(49, 0, 0).Days |> equal 2 + TimeSpan(0, 200, 0).Hours |> equal 3 + TimeSpan(0, 0, 300).Minutes |> equal 5 + TimeSpan(0, 0, 0, 0, 5089).Seconds |> equal 5 + TimeSpan(0, 0, 0, 0, 0, 5999).Milliseconds |> equal 5 + + let t1 = TimeSpan(10, 20, 39, 42, 57, 589) + + t1.Days |> equal 10 + t1.Hours |> equal 20 + t1.Minutes |> equal 39 + t1.Seconds |> equal 42 + t1.Milliseconds |> equal 57 + t1.Ticks |> equal 9383820575890L + t1.TotalDays |> equal 10.86090344431713 + t1.TotalHours |> equal 260.6616826636111 + t1.TotalMinutes |> equal 15639.700959816666 + t1.TotalSeconds |> equal 938382.057589 + t1.TotalMilliseconds |> equal 938382057.589 + t1.TotalMicroseconds |> equal 938382057589.0 + + //[] //[] //[] @@ -139,7 +186,7 @@ let ``test TimeSpan Addition works`` () = // let res2 = (t1 - t2).TotalMilliseconds // equal true (res1 = res2) // equal expected res1 -// +// //[] //[] //[] @@ -154,7 +201,7 @@ let ``test TimeSpan Addition works`` () = // equal res1 res2 // equal true (res1 = res2) // equal expected res1 - + //[] //let ``test TimeSpan Division works" [ @@ -185,4 +232,4 @@ let ``test TimeSpan Addition works`` () = // test_float 1000. -1. -1000. // test_float 2200. 11. 200. // test_float -3000. 1.5 -2000. -// test_float 0. 1000. 0. \ No newline at end of file +// test_float 0. 1000. 0. From c8cd132a45757329044638e4ece72447b3956470 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 24 Nov 2023 16:08:59 +0100 Subject: [PATCH 7/8] Generate an error if user try to use `get_Nanoseconds` or `get_TotalNanoseconds` + Update changelog --- src/Fable.Cli/CHANGELOG.md | 1 + src/Fable.Transforms/Python/Replacements.fs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 48f2c0c369..8c292941a8 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Fix `DateTime(..., DateTimeKind.Local).ToString("O")` (by @MangelMaxime) * Fix calling `value.ToString(CultureInfo.InvariantCulture)` (by @MangelMaxime) * Fix #3605: Fix record equality comparison to works with optional fields (by @MangelMaxime & @dbrattli) +* PR #3608: Rewrite `time_span.py` allowing for better precision by using a number representation intead of native `timedelta`. (by @MangelMaxime) ## 4.5.0 - 2023-11-07 diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index ed00b94354..31fd08804d 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -5229,9 +5229,18 @@ let timeSpans (args: Expr list) = // let callee = match i.callee with Some c -> c | None -> i.args.Head + match i.CompiledName with | ".ctor" -> - Helper.LibCall(com, "time_span", "create", t, args, i.SignatureArgTypes, ?loc = r) + Helper.LibCall( + com, + "time_span", + "create", + t, + args, + i.SignatureArgTypes, + ?loc = r + ) |> Some | "ToString" when (args.Length = 1) -> "TimeSpan.ToString with one argument is not supported, because it depends of local culture, please add CultureInfo.InvariantCulture" @@ -5259,6 +5268,8 @@ let timeSpans |> addError com ctx.InlinePath r None + | "get_Nanoseconds" + | "get_TotalNanoseconds" -> None | meth -> let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst From 0f60855afe4ea5ea4d21a34eed6438800b0d7e7a Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Sat, 25 Nov 2023 21:33:33 +0100 Subject: [PATCH 8/8] Fix variables naming --- src/fable-library-py/fable_library/date.py | 4 ++-- src/fable-library-py/fable_library/date_offset.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/fable-library-py/fable_library/date.py b/src/fable-library-py/fable_library/date.py index de1fbd8de8..dbdc88dc35 100644 --- a/src/fable-library-py/fable_library/date.py +++ b/src/fable-library-py/fable_library/date.py @@ -17,8 +17,8 @@ def op_subtraction(x: datetime, y: datetime) -> TimeSpan: delta = x - y # ts.microseconds only contains the microseconds provided to the constructor # so we need to calculate the total microseconds ourselves - deltaMicroseconds = delta.days * (24*3600) + delta.seconds * 10**6 + delta.microseconds - return create_time_span(0,0,0,0,0,deltaMicroseconds) + delta_microseconds = delta.days * (24*3600) + delta.seconds * 10**6 + delta.microseconds + return create_time_span(0,0,0,0,0,delta_microseconds) def create( diff --git a/src/fable-library-py/fable_library/date_offset.py b/src/fable-library-py/fable_library/date_offset.py index 16742ee9f8..c5b63e495c 100644 --- a/src/fable-library-py/fable_library/date_offset.py +++ b/src/fable-library-py/fable_library/date_offset.py @@ -39,15 +39,15 @@ def create( ms: int | TimeSpan, offset: TimeSpan | None = None, ) -> datetime: - pythonOffset: timedelta | None = None + python_offset: timedelta | None = None if isinstance(ms, TimeSpan): - pythonOffset = timedelta(microseconds=time_span.total_microseconds(ms)) + python_offset = timedelta(microseconds=time_span.total_microseconds(ms)) ms = 0 - if pythonOffset is None: + if python_offset is None: return datetime(year, month, day, h, m, s, ms) - tzinfo = timezone(pythonOffset) + tzinfo = timezone(python_offset) return datetime(year, month, day, h, m, s, ms, tzinfo=tzinfo)