Skip to content

Commit

Permalink
Merge pull request #3608 from fable-compiler/feature/timespan_as_number
Browse files Browse the repository at this point in the history
  • Loading branch information
MangelMaxime authored Nov 25, 2023
2 parents f030cd3 + 0f60855 commit 91781c5
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 119 deletions.
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

0 comments on commit 91781c5

Please sign in to comment.