From fa287de37d64aa1263bd2923ef772394bf952a04 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 22 Nov 2023 22:03:32 +0100 Subject: [PATCH] time_span.py has been rewritten to be represented using float --- src/Fable.Transforms/Python/Replacements.fs | 16 +- 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 | 94 +++++++++- 5 files changed, 231 insertions(+), 85 deletions(-) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index c2ac3e7a1c..d0ce11d299 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -3027,14 +3027,14 @@ let timeSpans (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o Helper.LibCall(com, "time_span", meth, t, args, i.SignatureArgTypes, ?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 3c0adada82..db04641c6a 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,91 @@ let main argv = // use file = builtins.``open``(StringPath "data.txt") // file.read() |> printfn "File contents: %s" - 0 \ No newline at end of file + 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 + + // .NET: 10.85428252 + // Python (best precision): 10.854282523263889