Skip to content

Commit 1e9df07

Browse files
dbrattliclaude
andauthored
[Beam] Implement missing DateTimeOffset members, add DateOnly and TimeOnly (#4479)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a3386bc commit 1e9df07

10 files changed

Lines changed: 1585 additions & 53 deletions

File tree

src/Fable.Cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
* [All] Add support for `Guid.CreateVersion7()` and `Guid.CreateVersion7(DateTimeOffset)`
1313
* [Rust] Add missing `System.Random` implementation and tests (by @ncave)
1414
* [Rust] Add missing `Array`, `List` and `Seq` module members and tests: `randomChoice`, `randomChoiceBy`, `randomChoiceWith`, `randomChoices`, `randomChoicesBy`, `randomChoicesWith`, `randomSample`, `randomSampleBy`, `randomSampleWith`, `randomShuffle`, `randomShuffleBy`, `randomShuffleWith` (by @ncave)
15+
* [Beam] Implement missing DateTimeOffset members, add DateOnly and TimeOnly support
1516

1617
### Fixed
1718

src/Fable.Compiler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
* [All] Add support for `Guid.CreateVersion7()` and `Guid.CreateVersion7(DateTimeOffset)`
1313
* [Rust] Add missing `System.Random` implementation and tests (by @ncave)
1414
* [Rust] Add missing `Array`, `List` and `Seq` module members and tests: `randomChoice`, `randomChoiceBy`, `randomChoiceWith`, `randomChoices`, `randomChoicesBy`, `randomChoicesWith`, `randomSample`, `randomSampleBy`, `randomSampleWith`, `randomShuffle`, `randomShuffleBy`, `randomShuffleWith` (by @ncave)
15+
* [Beam] Implement missing DateTimeOffset members, add DateOnly and TimeOnly support
1516

1617
### Fixed
1718

src/Fable.Transforms/Beam/Replacements.fs

Lines changed: 393 additions & 4 deletions
Large diffs are not rendered by default.

src/fable-library-beam/fable_date_offset.erl

Lines changed: 235 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,114 @@
33
create/7, create/8,
44
from_date/2,
55
from_ticks/2,
6+
from_date_time/3,
67
year/1,
78
month/1,
89
day/1,
910
hour/1,
1011
minute/1,
1112
second/1,
1213
millisecond/1,
14+
microsecond/1,
1315
ticks/1,
1416
offset/1,
1517
date_time/1,
18+
date/1,
19+
time_of_day/1,
20+
day_of_week/1,
21+
day_of_year/1,
22+
total_offset_minutes/1,
23+
utc_ticks/1,
1624
now/0,
1725
utc_now/0,
1826
min_value/0,
27+
max_value/0,
28+
unix_epoch/0,
29+
local_date_time/1,
30+
utc_date_time/1,
1931
to_local_time/1,
2032
to_universal_time/1,
33+
to_offset/2,
34+
add/2,
35+
subtract/2,
36+
add_years/2,
37+
add_months/2,
38+
add_days/2,
39+
add_hours/2,
40+
add_minutes/2,
41+
add_seconds/2,
42+
add_milliseconds/2,
43+
add_ticks/2,
44+
op_addition/2,
45+
op_subtraction/2,
46+
compare/2,
47+
equals/2,
48+
equals_exact/2,
49+
from_unix_time_seconds/1,
50+
from_unix_time_milliseconds/1,
51+
to_unix_time_seconds/1,
52+
to_unix_time_milliseconds/1,
2153
try_parse/2,
2254
to_string/1, to_string/2, to_string/3
2355
]).
2456

2557
-type datetimeoffset() :: {integer(), 0 | 1 | 2, integer()}.
58+
-type datetime() :: {integer(), 0 | 1 | 2}.
2659

2760
-spec create(integer(), integer(), integer(), integer(), integer(), integer(), integer()) ->
2861
datetimeoffset().
2962
-spec create(
3063
integer(), integer(), integer(), integer(), integer(), integer(), integer(), integer()
3164
) -> datetimeoffset().
32-
-spec from_date({integer(), integer()}, integer()) -> datetimeoffset().
65+
-spec from_date(datetime(), integer()) -> datetimeoffset().
3366
-spec from_ticks(integer(), integer()) -> datetimeoffset().
67+
-spec from_date_time(datetime(), integer(), integer()) -> datetimeoffset().
3468
-spec year(datetimeoffset()) -> integer().
3569
-spec month(datetimeoffset()) -> integer().
3670
-spec day(datetimeoffset()) -> integer().
3771
-spec hour(datetimeoffset()) -> integer().
3872
-spec minute(datetimeoffset()) -> integer().
3973
-spec second(datetimeoffset()) -> integer().
4074
-spec millisecond(datetimeoffset()) -> integer().
75+
-spec microsecond(datetimeoffset()) -> integer().
4176
-spec ticks(datetimeoffset()) -> integer().
4277
-spec offset(datetimeoffset()) -> integer().
43-
-spec date_time(datetimeoffset()) -> {integer(), integer()}.
78+
-spec date_time(datetimeoffset()) -> datetime().
79+
-spec date(datetimeoffset()) -> datetime().
80+
-spec time_of_day(datetimeoffset()) -> integer().
81+
-spec day_of_week(datetimeoffset()) -> integer().
82+
-spec day_of_year(datetimeoffset()) -> integer().
83+
-spec total_offset_minutes(datetimeoffset()) -> integer().
84+
-spec utc_ticks(datetimeoffset()) -> integer().
4485
-spec now() -> datetimeoffset().
4586
-spec utc_now() -> datetimeoffset().
4687
-spec min_value() -> datetimeoffset().
88+
-spec max_value() -> datetimeoffset().
89+
-spec unix_epoch() -> datetimeoffset().
90+
-spec local_date_time(datetimeoffset()) -> datetime().
91+
-spec utc_date_time(datetimeoffset()) -> datetime().
4792
-spec to_local_time(datetimeoffset()) -> datetimeoffset().
4893
-spec to_universal_time(datetimeoffset()) -> datetimeoffset().
94+
-spec to_offset(datetimeoffset(), integer()) -> datetimeoffset().
95+
-spec add(datetimeoffset(), integer()) -> datetimeoffset().
96+
-spec subtract(datetimeoffset(), datetimeoffset() | integer()) -> datetimeoffset() | integer().
97+
-spec add_years(datetimeoffset(), integer()) -> datetimeoffset().
98+
-spec add_months(datetimeoffset(), integer()) -> datetimeoffset().
99+
-spec add_days(datetimeoffset(), number()) -> datetimeoffset().
100+
-spec add_hours(datetimeoffset(), number()) -> datetimeoffset().
101+
-spec add_minutes(datetimeoffset(), number()) -> datetimeoffset().
102+
-spec add_seconds(datetimeoffset(), number()) -> datetimeoffset().
103+
-spec add_milliseconds(datetimeoffset(), number()) -> datetimeoffset().
104+
-spec add_ticks(datetimeoffset(), integer()) -> datetimeoffset().
105+
-spec op_addition(datetimeoffset(), integer()) -> datetimeoffset().
106+
-spec op_subtraction(datetimeoffset(), datetimeoffset() | integer()) -> datetimeoffset() | integer().
107+
-spec compare(datetimeoffset(), datetimeoffset()) -> -1 | 0 | 1.
108+
-spec equals(datetimeoffset(), datetimeoffset()) -> boolean().
109+
-spec equals_exact(datetimeoffset(), datetimeoffset()) -> boolean().
110+
-spec from_unix_time_seconds(integer()) -> datetimeoffset().
111+
-spec from_unix_time_milliseconds(integer()) -> datetimeoffset().
112+
-spec to_unix_time_seconds(datetimeoffset()) -> integer().
113+
-spec to_unix_time_milliseconds(datetimeoffset()) -> integer().
49114
-spec try_parse(binary(), reference()) -> boolean().
50115
-spec to_string(datetimeoffset()) -> binary().
51116
-spec to_string(datetimeoffset(), binary()) -> binary().
@@ -55,14 +120,15 @@
55120
%% - Ticks: same as DateTime (100-ns intervals from Jan 1, 0001)
56121
%% - Kind: 0=Unspecified, 1=UTC, 2=Local
57122
%% - OffsetTicks: offset from UTC as TimeSpan ticks
58-
%%
59-
%% For most property access, we delegate to fable_date since the
60-
%% underlying datetime components are the same.
61123

62124
-define(TICKS_PER_MILLISECOND, 10000).
63125
-define(TICKS_PER_SECOND, 10000000).
64126
-define(TICKS_PER_MINUTE, 600000000).
65127
-define(TICKS_PER_HOUR, 36000000000).
128+
-define(TICKS_PER_DAY, 864000000000).
129+
130+
%% .NET epoch offset: ticks from 0001-01-01 to 1970-01-01
131+
-define(UNIX_EPOCH_TICKS, 621355968000000000).
66132

67133
%% ============================================================
68134
%% Constructors
@@ -72,24 +138,14 @@
72138
create(Y, M, D, H, Min, S, OffsetTicks) when is_integer(OffsetTicks) ->
73139
DT = fable_date:create(Y, M, D, H, Min, S),
74140
{Ticks, _Kind} = DT,
75-
Kind =
76-
case OffsetTicks of
77-
% UTC
78-
0 -> 1;
79-
% Unspecified
80-
_ -> 0
81-
end,
141+
Kind = offset_to_kind(OffsetTicks),
82142
{Ticks, Kind, OffsetTicks}.
83143

84144
%% create(Y, M, D, H, Min, S, Ms, OffsetTimeSpan)
85145
create(Y, M, D, H, Min, S, Ms, OffsetTicks) when is_integer(OffsetTicks) ->
86146
DT = fable_date:create(Y, M, D, H, Min, S, Ms),
87147
{Ticks, _Kind} = DT,
88-
Kind =
89-
case OffsetTicks of
90-
0 -> 1;
91-
_ -> 0
92-
end,
148+
Kind = offset_to_kind(OffsetTicks),
93149
{Ticks, Kind, OffsetTicks}.
94150

95151
%% from_date(DateTime, OffsetTimeSpan)
@@ -98,13 +154,19 @@ from_date({Ticks, Kind}, OffsetTicks) ->
98154

99155
%% from_ticks(Ticks, OffsetTimeSpan)
100156
from_ticks(Ticks, OffsetTicks) ->
101-
Kind =
102-
case OffsetTicks of
103-
0 -> 1;
104-
_ -> 0
105-
end,
157+
Kind = offset_to_kind(OffsetTicks),
106158
{Ticks, Kind, OffsetTicks}.
107159

160+
%% from_date_time(DateOnly, TimeOnlyTicks, OffsetTimeSpan)
161+
%% DateOnly is {DayTicks, Kind}, TimeOnly is ticks since midnight
162+
from_date_time({DayTicks, _Kind}, TimeOnlyTicks, OffsetTicks) ->
163+
Ticks = DayTicks + TimeOnlyTicks,
164+
Kind = offset_to_kind(OffsetTicks),
165+
{Ticks, Kind, OffsetTicks}.
166+
167+
offset_to_kind(0) -> 1; % UTC
168+
offset_to_kind(_) -> 0. % Unspecified
169+
108170
%% ============================================================
109171
%% Properties (delegate to fable_date)
110172
%% ============================================================
@@ -116,6 +178,7 @@ hour({Ticks, Kind, _Offset}) -> fable_date:hour({Ticks, Kind}).
116178
minute({Ticks, Kind, _Offset}) -> fable_date:minute({Ticks, Kind}).
117179
second({Ticks, Kind, _Offset}) -> fable_date:second({Ticks, Kind}).
118180
millisecond({Ticks, Kind, _Offset}) -> fable_date:millisecond({Ticks, Kind}).
181+
microsecond({Ticks, Kind, _Offset}) -> fable_date:microsecond({Ticks, Kind}).
119182
ticks({Ticks, _Kind, _Offset}) -> Ticks.
120183

121184
%% Offset returns the TimeSpan offset (ticks)
@@ -124,30 +187,161 @@ offset({_Ticks, _Kind, OffsetTicks}) -> OffsetTicks.
124187
%% DateTime property: returns a DateTime {Ticks, Kind=Unspecified}
125188
date_time({Ticks, _Kind, _Offset}) -> {Ticks, 0}.
126189

190+
%% Date property: returns DateTime with time zeroed
191+
date({Ticks, Kind, _Offset}) -> fable_date:date({Ticks, Kind}).
192+
193+
%% TimeOfDay: returns TimeSpan (ticks) of the time-of-day component
194+
time_of_day({Ticks, _Kind, _Offset}) ->
195+
Ticks rem ?TICKS_PER_DAY.
196+
197+
%% DayOfWeek
198+
day_of_week({Ticks, Kind, _Offset}) -> fable_date:day_of_week({Ticks, Kind}).
199+
200+
%% DayOfYear
201+
day_of_year({Ticks, Kind, _Offset}) -> fable_date:day_of_year({Ticks, Kind}).
202+
203+
%% TotalOffsetMinutes
204+
total_offset_minutes({_Ticks, _Kind, OffsetTicks}) ->
205+
OffsetTicks div ?TICKS_PER_MINUTE.
206+
207+
%% UtcTicks: Ticks adjusted by offset
208+
utc_ticks({Ticks, _Kind, OffsetTicks}) ->
209+
Ticks - OffsetTicks.
210+
127211
%% ============================================================
128212
%% Static methods
129213
%% ============================================================
130214

131215
now() ->
132216
{Ticks, _Kind} = fable_date:now(),
133-
{Ticks, 2, 0}.
217+
OffsetTicks = local_offset_ticks(),
218+
{Ticks, 2, OffsetTicks}.
134219

135220
utc_now() ->
136221
{Ticks, _Kind} = fable_date:utc_now(),
137222
{Ticks, 1, 0}.
138223

139224
min_value() -> {0, 0, 0}.
140225

226+
max_value() -> {3155378975999999999, 0, 0}.
227+
228+
unix_epoch() ->
229+
{?UNIX_EPOCH_TICKS, 1, 0}.
230+
141231
%% ============================================================
142232
%% Conversion
143233
%% ============================================================
144234

145-
to_local_time({Ticks, _Kind, _Offset}) ->
146-
{Ticks, 2, 0}.
235+
to_local_time({Ticks, _Kind, OffsetTicks}) ->
236+
%% Convert to UTC ticks, then to local
237+
UtcTicks = Ticks - OffsetTicks,
238+
LocalOffset = local_offset_ticks(),
239+
{UtcTicks + LocalOffset, 2, LocalOffset}.
147240

148241
to_universal_time({Ticks, _Kind, OffsetTicks}) ->
149242
{Ticks - OffsetTicks, 1, 0}.
150243

244+
to_offset({Ticks, _Kind, OffsetTicks}, NewOffsetTicks) ->
245+
%% Convert to UTC, then apply new offset
246+
UtcTicks = Ticks - OffsetTicks,
247+
Kind = offset_to_kind(NewOffsetTicks),
248+
{UtcTicks + NewOffsetTicks, Kind, NewOffsetTicks}.
249+
250+
local_date_time({Ticks, _Kind, OffsetTicks}) ->
251+
UtcTicks = Ticks - OffsetTicks,
252+
LocalOffset = local_offset_ticks(),
253+
{UtcTicks + LocalOffset, 2}.
254+
255+
utc_date_time({Ticks, _Kind, OffsetTicks}) ->
256+
{Ticks - OffsetTicks, 1}.
257+
258+
%% ============================================================
259+
%% Arithmetic
260+
%% ============================================================
261+
262+
add({Ticks, Kind, OffsetTicks}, TimeSpanTicks) ->
263+
{Ticks + TimeSpanTicks, Kind, OffsetTicks}.
264+
265+
subtract({Ticks1, _Kind1, Offset1}, {Ticks2, _Kind2, Offset2}) ->
266+
%% DTO - DTO -> TimeSpan (compare UTC instants)
267+
(Ticks1 - Offset1) - (Ticks2 - Offset2);
268+
subtract({Ticks, Kind, OffsetTicks}, TimeSpanTicks) when is_integer(TimeSpanTicks) ->
269+
{Ticks - TimeSpanTicks, Kind, OffsetTicks}.
270+
271+
add_years({Ticks, Kind, OffsetTicks}, Years) ->
272+
{NewTicks, _} = fable_date:add_years({Ticks, Kind}, Years),
273+
{NewTicks, Kind, OffsetTicks}.
274+
275+
add_months({Ticks, Kind, OffsetTicks}, Months) ->
276+
{NewTicks, _} = fable_date:add_months({Ticks, Kind}, Months),
277+
{NewTicks, Kind, OffsetTicks}.
278+
279+
add_days({Ticks, Kind, OffsetTicks}, Days) ->
280+
Added = trunc(Days * ?TICKS_PER_DAY),
281+
{Ticks + Added, Kind, OffsetTicks}.
282+
283+
add_hours({Ticks, Kind, OffsetTicks}, Hours) ->
284+
Added = trunc(Hours * ?TICKS_PER_HOUR),
285+
{Ticks + Added, Kind, OffsetTicks}.
286+
287+
add_minutes({Ticks, Kind, OffsetTicks}, Minutes) ->
288+
Added = trunc(Minutes * ?TICKS_PER_MINUTE),
289+
{Ticks + Added, Kind, OffsetTicks}.
290+
291+
add_seconds({Ticks, Kind, OffsetTicks}, Seconds) ->
292+
Added = trunc(Seconds * ?TICKS_PER_SECOND),
293+
{Ticks + Added, Kind, OffsetTicks}.
294+
295+
add_milliseconds({Ticks, Kind, OffsetTicks}, Milliseconds) ->
296+
Added = trunc(Milliseconds * ?TICKS_PER_MILLISECOND),
297+
{Ticks + Added, Kind, OffsetTicks}.
298+
299+
add_ticks({Ticks, Kind, OffsetTicks}, AddedTicks) ->
300+
{Ticks + AddedTicks, Kind, OffsetTicks}.
301+
302+
op_addition(DTO, TimeSpanTicks) -> add(DTO, TimeSpanTicks).
303+
op_subtraction(DTO, Other) -> subtract(DTO, Other).
304+
305+
%% ============================================================
306+
%% Comparison / Equality
307+
%% ============================================================
308+
309+
%% Compare by UTC instant
310+
compare({Ticks1, _Kind1, Offset1}, {Ticks2, _Kind2, Offset2}) ->
311+
Utc1 = Ticks1 - Offset1,
312+
Utc2 = Ticks2 - Offset2,
313+
if
314+
Utc1 < Utc2 -> -1;
315+
Utc1 > Utc2 -> 1;
316+
true -> 0
317+
end.
318+
319+
equals({Ticks1, _Kind1, Offset1}, {Ticks2, _Kind2, Offset2}) ->
320+
(Ticks1 - Offset1) =:= (Ticks2 - Offset2).
321+
322+
equals_exact({Ticks1, _Kind1, Offset1}, {Ticks2, _Kind2, Offset2}) ->
323+
(Ticks1 - Offset1) =:= (Ticks2 - Offset2) andalso Offset1 =:= Offset2.
324+
325+
%% ============================================================
326+
%% Unix time
327+
%% ============================================================
328+
329+
from_unix_time_seconds(Seconds) ->
330+
Ticks = ?UNIX_EPOCH_TICKS + Seconds * ?TICKS_PER_SECOND,
331+
{Ticks, 1, 0}.
332+
333+
from_unix_time_milliseconds(Ms) ->
334+
Ticks = ?UNIX_EPOCH_TICKS + Ms * ?TICKS_PER_MILLISECOND,
335+
{Ticks, 1, 0}.
336+
337+
to_unix_time_seconds({Ticks, _Kind, OffsetTicks}) ->
338+
UtcTicks = Ticks - OffsetTicks,
339+
(UtcTicks - ?UNIX_EPOCH_TICKS) div ?TICKS_PER_SECOND.
340+
341+
to_unix_time_milliseconds({Ticks, _Kind, OffsetTicks}) ->
342+
UtcTicks = Ticks - OffsetTicks,
343+
(UtcTicks - ?UNIX_EPOCH_TICKS) div ?TICKS_PER_MILLISECOND.
344+
151345
%% ============================================================
152346
%% TryParse
153347
%% ============================================================
@@ -196,3 +390,19 @@ format_offset(OffsetTicks) ->
196390
H = AbsTicks div ?TICKS_PER_HOUR,
197391
M = (AbsTicks rem ?TICKS_PER_HOUR) div ?TICKS_PER_MINUTE,
198392
iolist_to_binary(io_lib:format("~s~2..0B:~2..0B", [Sign, H, M])).
393+
394+
%% ============================================================
395+
%% Internal helpers
396+
%% ============================================================
397+
398+
local_offset_ticks() ->
399+
%% Get local offset from UTC in ticks
400+
UtcSecs = erlang:system_time(second),
401+
{{UY,UM,UD},{UH,UMin,US}} = calendar:now_to_universal_time({UtcSecs div 1000000, UtcSecs rem 1000000, 0}),
402+
{{LY,LM,LD},{LH,LMin,LS}} = calendar:now_to_local_time({UtcSecs div 1000000, UtcSecs rem 1000000, 0}),
403+
UtcDays = calendar:date_to_gregorian_days({UY, UM, UD}),
404+
LocalDays = calendar:date_to_gregorian_days({LY, LM, LD}),
405+
UtcSecsOfDay = UH * 3600 + UMin * 60 + US,
406+
LocalSecsOfDay = LH * 3600 + LMin * 60 + LS,
407+
DiffSecs = (LocalDays - UtcDays) * 86400 + (LocalSecsOfDay - UtcSecsOfDay),
408+
DiffSecs * ?TICKS_PER_SECOND.

0 commit comments

Comments
 (0)