-
Notifications
You must be signed in to change notification settings - Fork 316
/
Date.enso
829 lines (660 loc) · 33.9 KB
/
Date.enso
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
import project.Any.Any
import project.Data.Json.JS_Object
import project.Data.Locale.Locale
import project.Data.Numbers.Integer
import project.Data.Ordering.Comparable
import project.Data.Ordering.Ordering
import project.Data.Text.Text
import project.Data.Time.Date_Period.Date_Period
import project.Data.Time.Date_Range.Date_Range
import project.Data.Time.Date_Time.Date_Time
import project.Data.Time.Date_Time_Formatter.Date_Time_Formatter
import project.Data.Time.Day_Of_Week.Day_Of_Week
import project.Data.Time.Day_Of_Week_From
import project.Data.Time.Duration.Duration
import project.Data.Time.Period.Period
import project.Data.Time.Time_Of_Day.Time_Of_Day
import project.Data.Time.Time_Zone.Time_Zone
import project.Data.Vector.Vector
import project.Error.Error
import project.Errors.Common.Type_Error
import project.Errors.Illegal_Argument.Illegal_Argument
import project.Errors.Time_Error.Time_Error
import project.Math
import project.Meta
import project.Nothing.Nothing
import project.Panic.Panic
from project.Data.Boolean import Boolean, False, True
from project.Data.Text.Extensions import all
from project.Data.Time.Date_Time import ensure_in_epoch
from project.Widget_Helpers import make_date_format_selector
polyglot java import java.lang.ArithmeticException
polyglot java import java.lang.Exception as JException
polyglot java import java.time.DateTimeException
polyglot java import java.time.temporal.ChronoField
polyglot java import java.time.temporal.IsoFields
polyglot java import org.enso.base.Time_Utils
## PRIVATE
Constructs a new Date from a year, month, and day.
Arguments
- year: The year to represent.
- month: The month-of-year to represent, from 1 (January) to 12 (December).
- day: The day-of-month to represent, from 1 to 31. It must be valid for the
year and month.
Recommended to use `Date.new` instead which handles potential exceptions.
new_builtin : Integer -> Integer -> Integer -> Date
new_builtin year month day = @Builtin_Method "Date.new_builtin"
## This type represents a date, often viewed as year-month-day.
For example, the value "2nd October 2007" can be stored in a `Date`.
This class does not store or represent a time or timezone. Instead, it
is a description of the date, as used for birthdays. It cannot represent
an instant on the time-line without additional information such as an
offset or timezone.
@Builtin_Type
type Date
## ALIAS current date, now
GROUP DateTime
Obtains the current date from the system clock in the system timezone.
> Example
Get the current date.
example_today = Date.today
today : Date
today = @Builtin_Method "Date.today"
## GROUP DateTime
Constructs a new Date from a year, month, and day.
Arguments
- year: The year to represent.
- month: The month-of-year to represent, from 1 (January) to 12 (December).
- day: The day-of-month to represent, from 1 to 31. It must be valid for the
year and month.
Returns a `Time_Error` if the provided time is not valid.
> Example
Create a new local date at Unix epoch.
from Standard.Base import Date
example_new = Date.new 1970
> Example
Get the local date of 5th August 1986.
example_new = Date.new 1986 8 5
new : Integer -> Integer -> Integer -> Date ! Time_Error
new year (month = 1) (day = 1) =
Panic.catch JException (new_builtin year month day) caught->
Error.throw (Time_Error.Error caught.payload.getMessage)
## ALIAS date from text
GROUP Conversions
Converts text containing a date into a Date object.
Arguments:
- text: The text to try and parse as a date.
- format: A pattern describing how to parse the text,
or a `Date_Time_Formatter`.
Returns a `Time_Error` if the provided `text` cannot be parsed using the
provided `format`.
? Default Date Formatting
Unless you provide a custom format, the text must represent a valid date
that can be parsed using the ISO-8601 extended local date format. The
format consists of:
- Four digits or more for the year. Years in the range 0000 to 9999
will be pre-padded by zero to ensure four digits. Years outside
that range will have a prefixed positive or negative symbol.
- A dash
- Two digits for the month-of-year. This is pre-padded by zero to ensure
two digits.
- A dash
- Two digits for the day-of-month. This is pre-padded by zero to ensure two
digits.
? Pattern Syntax
If the pattern is provided as `Text`, it is parsed using the format
described below. See `Date_Time_Formatter` for more options.
- y: Year. The number of pattern letters determines the minimum number of
digits.
- y: The year using any number of digits.
- yy: The year, using at most two digits. The default range is
1950-2049, but this can be changed by including the end year in
braces e.g. `yy{2099}`.
- yyyy: The year, using exactly four digits.
- M: Month of year. The number of pattern letters determines the format:
- M: Any number (1-12).
- MM: Month number with zero padding required (01-12).
- MMM: Short name of the month (Jan-Dec).
- MMMM: Full name of the month (January-December).
The month names depend on the selected locale.
- d: Day. The number of pattern letters determines the format:
- d: Any number (1-31).
- dd: Day number with zero padding required (01-31).
- ddd: Short name of the day of week (Mon-Sun).
- dddd: Full name of the day of week (Monday-Sunday).
The weekday names depend on the selected locale.
Both day of week and day of month may be included in a single pattern -
in such case the day of week is used as a sanity check.
- Q: Quarter of year.
If only year and quarter are provided in the pattern, when parsing a
date, the result will be the first day of that quarter.
> Example
Parse the date of 23rd December 2020.
from Standard.Base import Date
example_parse = Date.parse "2020-12-23"
> Example
Recover from an error due to a wrong format.
from Standard.Base import Date
from Standard.Base.Errors.Common import Time_Error
example_parse_err = Date.parse "my birthday" . catch Time_Error _->
Date.new 2000 1 1
> Example
Parse "1999-1-1" as Date using a custom format.
from Standard.Base import Date
example_parse = Date.parse "1999-1-1" "yyyy-M-d"
> Example
Recover from the parse error.
from Standard.Base import Date
from Standard.Base.Errors.Common import Time_Error
example_parse_err =
date = Date.parse "1999-1-1" "yyyy-MM-dd"
date.catch Time_Error (_->Date.new 2000 1 1)
@format make_date_format_selector
parse : Text -> Date_Time_Formatter -> Date ! Time_Error
parse text:Text format:Date_Time_Formatter=Date_Time_Formatter.iso_date =
format.parse_date text
## GROUP Metadata
Get the year field.
> Example
Get the current year.
from Standard.Base import Date
example_year = Date.today.year
year : Integer
year self = @Builtin_Method "Date.year"
## GROUP Metadata
Get the month of year field, as a number from 1 to 12.
> Example
Get the current month.
example_month = Date.today.month
month : Integer
month self = @Builtin_Method "Date.month"
## GROUP Metadata
Get the day of month field.
> Example
Get the current day.
from Standard.Base import Date
example_day = Date.today.day
day : Integer
day self = @Builtin_Method "Date.day"
## Returns the number of week of year this date falls into.
Produces a warning for a Date that is before epoch start.
Arguments:
- locale: the locale used to define the notion of weeks of year.
If no locale is provided, then the ISO 8601 week of year is used.
! Locale Dependency
Note that this operation is locale-specific. It varies both by the
local definition of the first day of week and the definition of the
first week of year. For example, in the US, the first day of the week
is Sunday and week 1 is the week containing January 1. In the UK on the
other hand, the first day of the week is Monday, and week 1 is the week
containing the first Thursday of the year. Therefore it is important to
properly specify the `locale` argument.
week_of_year : (Locale | Nothing) -> Integer
week_of_year self locale=Nothing =
ensure_in_epoch self <|
if locale.is_nothing then Time_Utils.get_field_as_localdate self IsoFields.WEEK_OF_WEEK_BASED_YEAR else
Time_Utils.week_of_year_localdate self locale.java_locale
## GROUP DateTime
Returns if the date is in a leap year.
Produces a warning for a Date that is before epoch start.
is_leap_year : Boolean
is_leap_year self =
ensure_in_epoch self <|
Time_Utils.is_leap_year self
## GROUP DateTime
Returns the number of days in the year represented by this date.
Produces a warning for a Date that is before epoch start.
length_of_year : Integer
length_of_year self = if self.is_leap_year then 366 else 365
## Returns the century of the date.
century : Integer
century self = if self.year > 0 then (self.year - 1).div 100 + 1 else
Error.throw (Illegal_Argument.Error "Century can only be given for AD years.")
## GROUP Metadata
Returns the quarter of the year the date falls into.
quarter : Integer
quarter self = Time_Utils.get_field_as_localdate self IsoFields.QUARTER_OF_YEAR
## GROUP DateTime
Returns the number of days in the month represented by this date.
Produces a warning for a Date that is before epoch start.
length_of_month : Integer
length_of_month self =
ensure_in_epoch self <|
Time_Utils.length_of_month self
## Returns the day of the year.
day_of_year : Integer
day_of_year self = Time_Utils.get_field_as_localdate self ChronoField.DAY_OF_YEAR
## GROUP Metadata
Returns the day of the week.
Produces a warning for a Date that is before epoch start.
day_of_week : Day_Of_Week
day_of_week self =
ensure_in_epoch self <|
Day_Of_Week.from (Time_Utils.get_field_as_localdate self ChronoField.DAY_OF_WEEK) Day_Of_Week.Monday
## GROUP DateTime
Returns the first date within the `Date_Period` containing self.
start_of : Date_Period -> Date
start_of self period=Date_Period.Month = period.adjust_start self
## GROUP DateTime
Returns the last date within the `Date_Period` containing self.
end_of : Date_Period -> Date
end_of self period=Date_Period.Month = period.adjust_end self
## GROUP DateTime
Returns the next date adding the `Date_Period` to self.
Produces a warning for a Date that is before epoch start.
See `Date_Time.enso_epoch_start`.
Arguments:
- period: the period to add to self.
next : Date_Period -> Date
next self period=Date_Period.Day = self + period.to_period
## GROUP DateTime
Returns the previous date subtracting the `Date_Period` from self.
Produces a warning for a Date that is before epoch start.
See `Date_Time.enso_epoch_start`.
Arguments:
- period: the period to add to self.
previous : Date_Period -> Date
previous self period=Date_Period.Day = self - period.to_period
## GROUP DateTime
Creates a `Period` between self and the provided end date.
Produces a warning for a Date that is before epoch start.
See `Date_Time.enso_epoch_start`.
Arguments:
- end: the end date of the interval to count workdays in.
until : Date -> Period
until self end =
ensure_in_epoch self <| ensure_in_epoch end <|
Period.between self end
## GROUP DateTime
Counts the days between self (inclusive) and the provided end date
(exclusive, or inclusive if include_end_date=True).
Produces a warning for a Date that is before epoch start.
See `Date_Time.enso_epoch_start`.
Arguments:
- end: the end date of the interval to count workdays in.
- include_end_date: whether to include the end date in the count.
By default the end date is not included in the interval.
days_until : Date -> Boolean -> Integer
days_until self end include_end_date=False =
if end < self then -(end.days_until self include_end_date) else
ensure_in_epoch self <| ensure_in_epoch end <|
(Time_Utils.days_between self end) + if include_end_date then 1 else 0
## GROUP DateTime
Returns a requested date part as integer.
Produces a warning for a Date that is before epoch start.
See `Date_Time.enso_epoch_start`.
date_part : Date_Period -> Integer
date_part self (period : Date_Period) =
case period of
Date_Period.Year -> self.year
Date_Period.Quarter -> self.quarter
Date_Period.Month -> self.month
Date_Period.Week _ -> self.week_of_year locale=Nothing
Date_Period.Day -> self.day
## GROUP DateTime
Computes a time difference between the two dates.
It returns an integer expressing how many periods fit between the two
dates.
The difference will be positive if `end` is greater than `self`.
Produces a warning for a Date that is before epoch start.
See `Date_Time.enso_epoch_start`.
Arguments:
- end: A date to compute the difference from.
- period: The period to compute the difference in.
date_diff : Date -> Date_Period -> Integer
date_diff self (end : Date) (period : Date_Period) = ensure_in_epoch self <|
Time_Utils.unit_date_difference period.to_java_unit self end
## GROUP DateTime
Shifts the date by a specified period.
Produces a warning for a Date that is before epoch start.
See `Date_Time.enso_epoch_start`.
Arguments:
- amount: An integer specifying by how many periods to shift the date.
- period: The period by which to shift.
date_add : Integer -> Date_Period -> Date
date_add self (amount : Integer) (period : Date_Period) = ensure_in_epoch self <|
Time_Utils.unit_date_add period.to_java_unit self amount
## GROUP DateTime
Counts workdays between self (inclusive) and the provided end date
(exclusive).
Produces a warning for a Date that is before epoch start.
See `Date_Time.enso_epoch_start`.
Arguments:
- end: the end date of the interval to count workdays in.
- holidays: dates of holidays to skip when counting workdays.
- include_end_date: whether to include the end date in the count.
By default the end date is not included in the interval.
? Including the end date
To be consistent with how we usually represent intervals (in an
end-exclusive manner), by default the end date is not included in the
count. This has the nice property that for example to count the work
days within the next week you can do
`date.work_days_until (date + (Period.new days=7))` and it will look at
the 7 days starting from the current `date` and not 8 days. This also
gives us a property that
`date.work_days_until (date.add_work_days N) == N` for any non-negative
N. On the other hand, sometimes we may want the end date to be included
in the count, so we provide the `include_end_date` argument for that
purpose. Setting it to `True` should make the result consistent with
the `NETWORKDAYS` function in Excel and similar products.
> Example
Count the number of workdays between two dates.
from Standard.Base import Date
example_workdays = Date.new 2020 1 1 . work_days_until (Date.new 2020 1 5)
work_days_until : Date -> Vector Date -> Boolean -> Integer
work_days_until self end holidays=[] include_end_date=False =
ensure_in_epoch self <|
if include_end_date then self.work_days_until (end + (Period.new days=1)) holidays include_end_date=False else
weekdays = week_days_between self end
## We count holidays that occurred within the period, but not on the
weekends (as weekend days have already been excluded from the count).
We also need to ensure we exclude each holiday only once, even if the
user provided it multiple times.
overlapping_holidays = holidays.filter holiday->
fits_in_range self end holiday && (is_weekend holiday).not
weekdays - overlapping_holidays.distinct.length
## ALIAS date to time
GROUP Conversions
Combine this date with time of day to create a point in time.
Arguments:
- time_of_day: The time to combine with the date to create a time.
- zone: The time-zone in which to create the time.
> Example
Convert this date to midnight UTC time.
from Standard.Base import Date, Time_Of_Day, Time_Zone
example_to_time = Date.new 2020 2 3 . to_date_time Time_Of_Day.new Time_Zone.utc
to_date_time : Time_Of_Day -> Time_Zone -> Date_Time
to_date_time self (time_of_day=Time_Of_Day.new) (zone=Time_Zone.system) =
Time_Utils.make_zoned_date_time self time_of_day zone
## ALIAS add period
GROUP Operators
Add the specified amount of time to this instant to get another date.
Arguments:
- amount: The amount of time to add to this instant. It can be a
`Period` or `Date_Period`.
> Example
Add 6 months to a local date.
import Standard.Base.Data.Time.Duration
example_add = Date.new 2020 + (Period.new months=6)
> Example
Add a month to a local date.
import Standard.Base.Data.Time.Date_Period
example_add = Date.new 2020 + Date_Period.Month
+ : Period | Date_Period -> Date ! (Time_Error | Illegal_Argument)
+ self (amount : Period | Date_Period) =
case amount of
period : Period ->
Time_Utils.date_adjust self Time_Utils.AdjustOp.PLUS period.internal_period
date_period : Date_Period ->
self + date_period.to_period
## ALIAS date range
GROUP Input
Creates an increasing range of dates from `self` to `end`.
Arguments:
- end: The end of the range.
- include_end: Specifies if the right end of the range should be included. By
default, the range is right-exclusive.
> Example
Create a range of dates.
(Date.new 2021 12 05).up_to (Date.new 2021 12 10)
> Example
Create a range containing dates [2021-12-05, 2021-12-06].
(Date.new 2021 12 05).up_to (Date.new 2021 12 06) include_end=True
up_to : Date -> Boolean -> Date_Range
up_to self end include_end=False = case end of
_ : Date ->
effective_end = if include_end then end.next else end
Date_Range.new_internal self effective_end increasing=True step=(Period.new days=1)
_ -> Error.throw (Type_Error.Error Date end "end")
## ALIAS date range
GROUP Input
Creates a decreasing range of dates from `self` to `end`.
Arguments:
- end: The end of the range.
- include_end: Specifies if the right end of the range should be included. By
default, the range is right-exclusive.
> Example
Create a reverse range of dates.
(Date.new 2021 12 10).down_to (Date.new 2021 12 05)
> Example
Create a range containing dates [2021-12-06, 2021-12-05].
(Date.new 2021 12 06).down_to (Date.new 2021 12 05) include_end=True
down_to : Date -> Boolean -> Date_Range
down_to self end include_end=False = case end of
_ : Date ->
effective_end = if include_end then end.previous else end
Date_Range.new_internal self effective_end increasing=False step=(Period.new days=1)
_ -> Error.throw (Type_Error.Error Date end "end")
## GROUP DateTime
Shift the date by the specified amount of business days.
For the purpose of this method, the business days are defined to be
Monday through Friday.
Produces a warning for a Date that is before epoch start. See
`Date_Time.enso_epoch_start`.
This method always returns a day which is a business day - if the shift
amount is zero, the closest following business day is returned. For the
purpose of calculating the shift, the holidays are treated as if we were
starting at the next business day after them, for example counting the
shift starting on Saturday or Sunday works as if we were counting the
shift from Monday (for positive shifts). So shifting Sunday by zero days
will return Monday, but shifting it by one day will return a Tuesday
(so that there is the full work day - Monday) within the interval. For
negative shifts, shifting either Saturday or Sunday one day backwards
will return Friday, but shifting Monday one day backwards will return a
Friday. The whole logic is made consistent with `work_days_until`, so
that the following properties hold:
date.work_days_until (date.add_work_days N) == N for any N >= 0
(date.add_work_days N).work_days_until date == -N for any N < 0
Arguments:
- amount: The number of business days to shift the date by. If `amount`
is zero, the current date is returned, unless it is a weekend or a
holiday, in which case the next business day is returned.
- holidays: An optional list of dates of custom holidays that should also
be skipped. If it is not provided, only weekends are skipped.
> Example
Shift the date by 5 business days.
example_shift = Date.new 2020 2 3 . add_work_days 5
add_work_days : Integer -> Vector Date -> Date
add_work_days self days=1 holidays=[] =
ensure_in_epoch self <|
self.internal_add_work_days days holidays
## PRIVATE
ensure_in_epoch breaks tail call annotation and causes
stack overflow. That is why `add_work_days` method is split into
two methods.
internal_add_work_days : Integer -> Vector Date -> Date
internal_add_work_days self days=1 holidays=[] =
case days >= 0 of
True ->
full_weeks = days.div 5
remaining_days = days % 5
# If the current day is a Saturday, the ordinal will be 6.
ordinal = self.day_of_week.to_integer first_day=Day_Of_Week.Monday start_at_zero=False
## If the current day is a Sunday, we just need to shift by one day
to 'escape' the weekend, regardless of the overall remaining
shift. On any other day, we check if current day plus the shift
overlaps a weekend, we need the shift to be 2 days since we need
to skip both Saturday and Sunday.
additional_shift = if ordinal == 7 then 1 else
if ordinal + remaining_days > 5 then 2 else 0
days_to_shift = full_weeks*7 + remaining_days + additional_shift
end = self + (Period.new days=days_to_shift)
## We have shifted the date so that weekends are taken into account,
but other holidays may have happened during that shift period.
Thus we may have shifted by less workdays than really desired. We
compute the difference and if there are still remaining workdays
to shift by, we re-run the whole shift procedure.
workdays = self.work_days_until end holidays include_end_date=False
diff = days - workdays
if diff > 0 then @Tail_Call end.internal_add_work_days diff holidays else
## Otherwise we have accounted for all workdays we were asked
to. But that is still not the end - we still need to ensure
that the final day on which we have 'landed' is a workday
too. Our procedure ensures that it is not a weekend, but it
can still be a holiday. So we will be shifting the end date
as long as needed to fall on a non-weekend non-holiday
workday.
go end_date =
if holidays.contains end_date || is_weekend end_date then @Tail_Call go (end_date + (Period.new days=1)) else end_date
go end
False ->
## We shift a bit so that if shifting by N full weeks, the 'last'
shift is done on `remaining_days` and not full weeks. That is
because shifting a Saturday back 5 days does not want us to get
to the earlier Saturday and fall back to the Friday before it,
but we want to stop at the Monday just after that Saturday.
full_weeks = (days + 1).div 5
remaining_days = (days + 1) % 5 - 1
# If the current day is a Sunday, the ordinal will be 1.
ordinal = self.day_of_week.to_integer first_day=Day_Of_Week.Sunday start_at_zero=False
## If we overlapped the weekend, we need to increase the shift by
one day (our current shift already shifts us by one day, but we
need one more to skip the whole two-day weekend).
additional_shift = if ordinal == 1 then -1 else
if ordinal + remaining_days <= 1 then -2 else 0
## The rest of the logic is analogous to the positive case, we
just need to correctly handle the reverse order of dates. The
`days_to_shift` will be negative so `end` will come _before_
`self`.
days_to_shift = full_weeks*7 + remaining_days + additional_shift
end = self + (Period.new days=days_to_shift)
workdays = end.work_days_until self holidays include_end_date=False
## `days` is negative but `workdays` is positive, `diff` will be
zero if we accounted for all days or negative if there are
still workdays we need to shift by - then it will be exactly
the remaining offset that we need to shift by.
diff = days + workdays
if diff < 0 then @Tail_Call end.internal_add_work_days diff holidays else
## As in the positive case, if the final end date falls on a
holiday, we need to ensure that we move it - this time
backwards - to the first workday.
go end_date =
if holidays.contains end_date || is_weekend end_date then @Tail_Call go (end_date - (Period.new days=1)) else end_date
go end
## ALIAS subtract period
GROUP Operators
Subtract the specified amount of time from this instant to get another
date.
Arguments:
- amount: The amount of time to add to this instant. It can be a
`Period` or `Date_Period`.
> Example
Subtract 7 days from a local date.
from Standard.Base import Date
import Standard.Base.Data.Time.Duration
example_subtract = Date.new 2020 - (Period.new days=7)
> Example
Subtract a month from a local date.
import Standard.Base.Data.Time.Date_Period
example_add = Date.new 2020 - Date_Period.Month
- : Period | Date_Period -> Date ! (Time_Error | Illegal_Argument)
- self amount:(Period | Date_Period) =
case amount of
period : Period ->
new_java_date = Time_Utils.date_adjust self Time_Utils.AdjustOp.MINUS period.internal_period
Date.new new_java_date.year new_java_date.month new_java_date.day
date_period : Date_Period ->
self - date_period.to_period
## PRIVATE
Convert to a display representation of this Date.
to_display_text : Text
to_display_text self =
self.format "yyyy-MM-dd"
## PRIVATE
Convert to a JS_Object representing this Date.
> Example
Convert the current date to a JS_Object.
example_to_json = Date.today.to_js_object
to_js_object : JS_Object
to_js_object self =
type_pair = ["type", "Date"]
cons_pair = ["constructor", "new"]
JS_Object.from_pairs [type_pair, cons_pair, ["day", self.day], ["month", self.month], ["year", self.year]]
## GROUP Conversions
Format this date using the provided format specifier.
Arguments:
- format: A pattern describing how to format the text,
or a `Date_Time_Formatter`.
? Pattern Syntax
If the pattern is provided as `Text`, it is parsed using the format
described below. See `Date_Time_Formatter` for more options.
- y: Year. The number of pattern letters determines the minimum number of
digits.
- y: The year using any number of digits.
- yy: The year, using at most two digits. The default range is
1950-2049, but this can be changed by including the end year in
braces e.g. `yy{2099}`.
- yyyy: The year, using exactly four digits.
- M: Month of year. The number of pattern letters determines the format:
- M: Any number (1-12).
- MM: Month number with zero padding required (01-12).
- MMM: Short name of the month (Jan-Dec).
- MMMM: Full name of the month (January-December).
The month names depend on the selected locale.
- d: Day. The number of pattern letters determines the format:
- d: Any number (1-31).
- dd: Day number with zero padding required (01-31).
- ddd: Short name of the day of week (Mon-Sun).
- dddd: Full name of the day of week (Monday-Sunday).
The weekday names depend on the selected locale.
Both day of week and day of month may be included in a single pattern -
in such case the day of week is used as a sanity check.
- Q: Quarter of year.
If only year and quarter are provided in the pattern, when parsing a
date, the result will be the first day of that quarter.
> Example
Format "2020-06-02" as "2 Jun 2020"
from Standard.Base import Date
example_format = Date.new 2020 6 2 . format "d MMMM yyyy"
> Example
Format "2020-06-02" as "2 Jun 20"
example_format = Date.new 2020 6 2 . format "d MMMM yy"
> Example
Format "2020-06-02" as "Tuesday, 02 Jun 2020"
example_format = Date.new 2020 6 2 . format "EEEE, dd MMMM yyyy"
> Example
Format "2020-06-02" as "Tue Jun 2"
example_format = Date.new 2020 6 2 . format "EEE MMM d"
> Example
Format "2020-06-02" as "2020AD"
example_format = Date.new 2020 6 2 . format "yyyyGG"
> Example
Format "2020-06-21" with French locale as "21. juin 2020"
example_format = Date.new 2020 6 21 . format (Date_Time_Formatter.from "d. MMMM yyyy" (Locale.new "fr"))
@format (value-> make_date_format_selector value)
format : Date_Time_Formatter -> Text
format self format:Date_Time_Formatter=Date_Time_Formatter.iso_date =
format.format_date self
## PRIVATE
week_days_between start end =
## We split the interval into 3 periods: the first week (containing the
starting point), the last week (containing the end point), and the full
weeks in between those. In some cases there may be no weeks in-between
and the first and last week can be the same week.
start_of_first_full_week = (start.start_of Date_Period.Week) + (Period.new days=7)
start_of_last_week = end.start_of Date_Period.Week
full_weeks_between = (Time_Utils.days_between start_of_first_full_week start_of_last_week).div 7
case full_weeks_between < 0 of
# Either start is before end or they both lie within the same week.
True ->
days_between = Time_Utils.days_between start end
if days_between <= 0 then 0 else
## The end day is not counted, but if the end day was a Sunday,
the last day was a Saturday and it should not be counted
either, so we need to subtract it.
case end.day_of_week of
Day_Of_Week.Sunday -> days_between - 1
_ -> days_between
False ->
# We count the days in the first week up until Friday - the weekend is not counted.
first_week_days = (Time_Utils.days_between start (start_of_first_full_week - (Period.new days=2))).max 0
# We count the days in the last week, not including the weekend.
last_week_days = (Time_Utils.days_between start_of_last_week end).min 5
full_weeks_between * 5 + first_week_days + last_week_days
## PRIVATE
is_weekend date =
dow = date.day_of_week
(dow == Day_Of_Week.Saturday) || (dow == Day_Of_Week.Sunday)
## PRIVATE
fits_in_range start end date =
(start <= date) && (date < end)