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 ().
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
72138create (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)
85145create (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)
100156from_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}).
116178minute ({Ticks , Kind , _Offset }) -> fable_date :minute ({Ticks , Kind }).
117179second ({Ticks , Kind , _Offset }) -> fable_date :second ({Ticks , Kind }).
118180millisecond ({Ticks , Kind , _Offset }) -> fable_date :millisecond ({Ticks , Kind }).
181+ microsecond ({Ticks , Kind , _Offset }) -> fable_date :microsecond ({Ticks , Kind }).
119182ticks ({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}
125188date_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
131215now () ->
132216 {Ticks , _Kind } = fable_date :now (),
133- {Ticks , 2 , 0 }.
217+ OffsetTicks = local_offset_ticks (),
218+ {Ticks , 2 , OffsetTicks }.
134219
135220utc_now () ->
136221 {Ticks , _Kind } = fable_date :utc_now (),
137222 {Ticks , 1 , 0 }.
138223
139224min_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
148241to_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