Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor time_span.py to use an int representation instead of timedelta #3608

Merged
merged 8 commits into from
Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
36 changes: 4 additions & 32 deletions src/Fable.Transforms/Python/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5229,46 +5229,16 @@ let timeSpans
(args: Expr list)
=
// 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
)
|> 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",
"create",
t,
args,
i.SignatureArgTypes,
?thisArg = thisArg,
?loc = r
)
|> Some
Expand Down Expand Up @@ -5298,6 +5268,8 @@ let timeSpans
|> addError com ctx.InlinePath r

None
| "get_Nanoseconds"
| "get_TotalNanoseconds" -> None
| meth ->
let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst

Expand Down
9 changes: 7 additions & 2 deletions src/fable-library-py/fable_library/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
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(
Expand Down
27 changes: 17 additions & 10 deletions src/fable-library-py/fable_library/date_offset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
python_offset: timedelta | None = None
if isinstance(ms, TimeSpan):
python_offset = timedelta(microseconds=time_span.total_microseconds(ms))
ms = 0

if offset is None:
if python_offset is None:
return datetime(year, month, day, h, m, s, ms)

tzinfo = timezone(offset)
tzinfo = timezone(python_offset)
return datetime(year, month, day, h, m, s, ms, tzinfo=tzinfo)


Expand All @@ -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:
Expand Down
178 changes: 110 additions & 68 deletions src/fable-library-py/fable_library/time_span.py
Original file line number Diff line number Diff line change
@@ -1,132 +1,169 @@
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


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):
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(fmod(ts, 10000) / 10)


def negate(ts: timedelta) -> timedelta:
return -ts
def milliseconds(ts: TimeSpan) -> int:
return int(fmod(ts, 10000000) / 10000)


def duration(ts: timedelta) -> timedelta:
if ts < timedelta(0):
return -ts
def seconds(ts: TimeSpan) -> int:
return int(fmod(ts, 600000000) / 10000000)

return ts

def minutes(ts: TimeSpan) -> int:
return int(fmod(ts, 36000000000) / 600000000)

def add(ts: timedelta, other: timedelta) -> timedelta:
return ts + other

def hours(ts: TimeSpan) -> int:
return int(fmod(ts, 864000000000) / 36000000000)

def subtract(ts: timedelta, other: timedelta) -> timedelta:
return ts - other

def days(ts: TimeSpan) -> int:
res = ts / 864000000000
return ceil(res) if res < 0 else floor(res)

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")

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 < timedelta(0) else ""
sign: str = "-" if ts < 0 else ""

ms_str = (
""
Expand All @@ -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",
Expand Down
Loading
Loading