Add Period type (#3818)

This PR adds `Period` type, which is a date-only complement to `Duration` builtin type.

# Important Notes
- `Period` replaces `Date_Period`, and `Time_Period`.
- Added shorthand constructors for `Duration` and `Period`. For example: `Period.days 10` instead of `Period.new days=10`.
- `Period` can be compared to other `Period` in some cases, other cases throw an error.
This commit is contained in:
Pavel Marek 2022-10-28 19:27:20 +02:00 committed by GitHub
parent c4a7e28fb5
commit f8a4e2a9d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 522 additions and 222 deletions

View File

@ -223,6 +223,7 @@
selector variant.][3812]
- [Implemented `Table.rows` giving access to a vector of rows.][3827]
- [Define Enso epoch start as 15th October 1582][3804]
- [Implemented `Period` type][3818]
[debug-shortcuts]:
https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug
@ -358,6 +359,7 @@
[3827]: https://github.com/enso-org/enso/pull/3827
[3824]: https://github.com/enso-org/enso/pull/3824
[3804]: https://github.com/enso-org/enso/pull/3804
[3818]: https://github.com/enso-org/enso/pull/3818
#### Enso Compiler

View File

@ -1,6 +1,7 @@
from Standard.Base import all
import Standard.Base.Data.Time.Duration
import Standard.Base.Data.Time.Period
import Standard.Base.Data.Time.Date_Period
import Standard.Base.Polyglot
@ -9,6 +10,8 @@ from Standard.Base.Error.Common import Time_Error_Data, unimplemented
polyglot java import org.enso.base.Time_Utils
polyglot java import java.time.temporal.ChronoField
polyglot java import java.time.temporal.IsoFields
polyglot java import java.time.DateTimeException
polyglot java import java.lang.ArithmeticException
## Obtains the current date from the system clock in the system timezone.
@ -139,9 +142,6 @@ parse text pattern=Nothing =
## This type represents a date, often viewed as year-month-day.
Arguments:
- internal_local_date: The internal date representation.
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
@ -272,7 +272,7 @@ type Date
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 + 7.days)` and it will look at the 7 days
`date.work_days_until (date + (Period.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
@ -290,7 +290,7 @@ type Date
work_days_until : Date -> Vector Date -> Boolean -> Integer
work_days_until self end holidays=[] include_end_date=False =
Date_Time.ensure_in_epoch self <|
if include_end_date then self.work_days_until (end + 1.day) holidays include_end_date=False else
if include_end_date then self.work_days_until (end + (Period.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).
@ -313,7 +313,7 @@ type Date
from Standard.Base import Date, Time_Of_Day, Time_Zone
example_to_time = Date.new 2020 2 3 . to_time Time_Of_Day.new Time_Zone.utc
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) = self.to_time_builtin time_of_day zone
@ -327,9 +327,16 @@ type Date
import Standard.Base.Data.Time.Duration
example_add = Date.new 2020 + 6.months
+ : Date_Period -> Date
+ self _ = unimplemented "Date.+"
example_add = Date.new 2020 + (Period.months 6)
+ : Period -> Date ! (Time_Error | Illegal_Argument_Error)
+ self period =
case period of
_ : Period.Period ->
Time_Utils.date_adjust self Time_Utils.AdjustOp.PLUS period.internal_period
_ : Duration.Duration ->
Error.throw (Time_Error_Data "Date does not support adding/subtracting Duration. Use Period instead.")
_ ->
Error.throw (Illegal_Argument_Error_Data "Illegal period argument")
## Shift the date by the specified amount of business days.
@ -368,81 +375,90 @@ type Date
add_work_days : Integer -> Vector Date -> Date
add_work_days self days=1 holidays=[] =
Date_Time.ensure_in_epoch self <|
case days >= 0 of
True ->
full_weeks = days.div 5
remaining_days = days % 5
self.internal_add_work_days days holidays
# 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
## PRIVATE
## 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
Date_Time.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
days_to_shift = full_weeks*7 + remaining_days + additional_shift
end = self + days_to_shift.days
# 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
## 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.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 + 1.day) 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, 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
# 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
days_to_shift = full_weeks*7 + remaining_days + additional_shift
end = self + (Period.new days=days_to_shift)
## 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
## 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.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
## 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 + days_to_shift.days
workdays = end.work_days_until self holidays include_end_date=False
# 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
## `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.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 - 1.day) else end_date
go end
## 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.days 1)) else end_date
go end
## Subtract the specified amount of time from this instant to get another
date.
@ -456,9 +472,17 @@ type Date
from Standard.Base import Date
import Standard.Base.Data.Time.Duration
example_subtract = Date.new 2020 - 7.days
- : Date_Period -> Date
- self _ = unimplemented "Date.-"
example_subtract = Date.new 2020 - (Period.days 7)
- : Period -> Date ! (Time_Error | Illegal_Argument_Error)
- self period =
case period of
_ : Period.Period ->
new_java_date = Time_Utils.date_adjust self Time_Utils.AdjustOp.MINUS period.internal_period
new new_java_date.year new_java_date.month new_java_date.day
_ : Duration.Duration ->
Error.throw (Time_Error_Data "Date does not support adding/subtracting Duration. Use Period instead.")
_ ->
Error.throw (Illegal_Argument_Error_Data "Illegal period argument")
## A Date to Json conversion.
@ -538,7 +562,7 @@ week_days_between start end =
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) + 7.days
start_of_first_full_week = (start.start_of Date_Period.Week) + (Period.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
@ -554,7 +578,7 @@ week_days_between start end =
_ -> days_between
False ->
# We count the days in the first week up until Friday - the weekend is not counted.
first_week_days = Math.max 0 (Time_Utils.days_between start (start_of_first_full_week - 2.days))
first_week_days = Math.max 0 (Time_Utils.days_between start (start_of_first_full_week - (Period.days 2)))
# We count the days in the last week, not including the weekend.
last_week_days = Math.min (Time_Utils.days_between start_of_last_week end) 5
full_weeks_between * 5 + first_week_days + last_week_days

View File

@ -1,13 +1,12 @@
from Standard.Base import all
import Standard.Base.Data.Time.Duration
import Standard.Base.Data.Time.Date_Period
import Standard.Base.Data.Time.Time_Period
from Standard.Base.Data.Time import Duration, Period, Date_Period, Time_Period
from Standard.Base.Error.Common import Time_Error
polyglot java import java.time.format.DateTimeFormatter
polyglot java import java.time.temporal.ChronoField
polyglot java import java.time.temporal.IsoFields
polyglot java import java.time.ZonedDateTime
polyglot java import java.lang.ArithmeticException
polyglot java import org.enso.base.Time_Utils
@ -442,18 +441,24 @@ type Date_Time
## Add the specified amount of time to this instant to produce a new instant.
Arguments:
- amount: The amount of time to add to this instant.
- amount: The amount of time to add to this instant, either `Duration` for
adding time (hours, minutes, etc.), or `Period` for adding date (days,
months, years).
> Example
Add 15 years and 3 hours to a zoned date time.
from Standard.Base import Date_Time
import Standard.Base.Data.Time.Duration
from Standard.Base.Data.Time import Duration, Period
example_plus = Date_Time.new 2020 + 15.years + (Duration.new hours=3)
+ : Duration -> Date_Time ! Time_Error
example_plus = Date_Time.new 2020 + (Period.years 15) + (Duration.hours 3)
+ : (Duration | Period) -> Date_Time ! Time_Error
+ self amount =
Panic.catch ArithmeticException (self.plus_builtin amount) (err -> Error.throw (Time_Error_Data err.getMessage))
case amount of
duration : Duration.Duration ->
Panic.catch ArithmeticException (self.plus_builtin duration) (err -> Error.throw (Time_Error_Data err.getMessage))
period : Period.Period ->
Time_Utils.datetime_adjust self Time_Utils.AdjustOp.PLUS period.internal_period
## Shift the date by the specified amount of business days.
@ -502,18 +507,24 @@ type Date_Time
Produces a warning if the resulting date time is before an Enso epoch.
Arguments:
- amount: The amount of time to subtract from this instant.
- amount: The amount of time to subtract from this instant. Can be either
`Duration` for subtracting time (hours, minutes, etc.), or `Period`
for subtracting days, months and years.
> Example
Subtract 1 year and 9 months from a zoned date time.
Subtract 1 year, 9 months and 12 hours from a zoned date time.
from Standard.Base import Date_Time
import Standard.Base.Data.Time.Duration
example_minus = Date_Time.new 2020 - 1.year - 9.months
- : Duration -> Date_Time ! Time_Error
example_minus = Date_Time.new 2020 - (Period.years 1) - (Period.months 9) - (Duration.hours 5)
- : (Duration | Period) -> Date_Time ! Time_Error
- self amount =
result = Panic.catch ArithmeticException (self.minus_builtin amount) (err -> Error.throw (Time_Error_Data err.getMessage))
result = case amount of
duration : Duration.Duration ->
Panic.catch ArithmeticException (self.minus_builtin duration) (err -> Error.throw (Time_Error_Data err.getMessage))
period : Period.Period ->
Time_Utils.datetime_adjust self Time_Utils.AdjustOp.MINUS period.internal_period
ensure_in_epoch result result
## Convert this time to text using the default formatter.

View File

@ -1,5 +1,6 @@
from Standard.Base import all
import Standard.Base.System
from Standard.Base.Data.Time import Period
polyglot java import java.time.Duration as Java_Duration
polyglot java import java.time.Period as Java_Period
@ -9,8 +10,8 @@ polyglot java import java.lang.ArithmeticException
## Create an interval representing the duration between two points in time.
Arguments:
- start_inclusive: The start datetime of the duration.
- end_inclusive: The end datetime of the duration.
- start_inclusive: The start datetime of the duration, included.
- end_exclusive: The end datetime of the duration, excluded.
- timezone_aware: Whether the duration between two given times should be
aware of the timezone, that can be set for start or end times.
@ -44,6 +45,25 @@ new : Integer -> Integer -> Integer -> Integer -> Integer -> Integer -> Duration
new hours=0 minutes=0 seconds=0 milliseconds=0 nanoseconds=0 =
Duration.new_builtin hours minutes seconds milliseconds nanoseconds
## Create a Duration from hours.
hours : Integer -> Duration
hours h = new hours=h
## Create a Duration from minutes.
minutes : Integer -> Duration
minutes m = new minutes=m
## Create a Duration from seconds.
seconds : Integer -> Duration
seconds s = new seconds=s
## Create a Duration from milliseconds.
milliseconds : Integer -> Duration
milliseconds ms = new milliseconds=ms
## Create a Duration from nanoseconds.
nanoseconds : Integer -> Duration
nanoseconds ns = new nanoseconds=ns
## Create a zero (empty) duration.
> Example
@ -51,7 +71,7 @@ new hours=0 minutes=0 seconds=0 milliseconds=0 nanoseconds=0 =
import Standard.Base.Data.Time.Duration
durations = [(Duration.new seconds=1), (Duration.new seconds=2), (Duration.new seconds=3)]
durations = [(Duration.seconds 1), (Duration.seconds 2), (Duration.seconds 3)]
example_sum = durations.fold Duration.zero (+)
zero : Duration
zero = new
@ -70,6 +90,15 @@ time_execution ~function =
duration = new nanoseconds=(end - start)
Pair_Data duration result
## PRIVATE
ensure_duration : Any -> Suspend (Any -> Any) -> Any ! (Time_Error | Illegal_Argument_Error)
ensure_duration object ~action =
case object of
_ : Duration -> action
_ : Period.Period -> Error.throw (Time_Error_Data "Cannot use Period as a parameter")
x ->
Error.throw Illegal_Argument_Error_Data <|
"Expected Duration type, got: " + (Meta.get_qualified_type_name x)
@Builtin_Type
type Duration
@ -84,39 +113,36 @@ type Duration
import Standard.Base.Data.Time.Duration
example_add = (Duration.new minutes=3) + (Duration.new seconds=6)
example_add = (Duration.minutes 3) + (Duration.seconds 6)
> Example
Add 12 hours to a duration of a month.
Add 12 hours to a duration of 30 minutes.
import Standard.Base.Data.Time.Duration
example_add = 1.month + (Duration.new hours=12)
example_add = (Duration.minutes 30) + (Duration.hours 12)
+ : Duration -> Duration ! Time_Error
+ self that =
Panic.catch ArithmeticException (self.plus_builtin that) (err-> Error.throw (Time_Error_Data err.getMessage))
ensure_duration that <|
Panic.catch ArithmeticException (self.plus_builtin that) err->
Error.throw (Time_Error_Data err.getMessage)
## Subtract the specified amount of time from this duration.
Arguments:
- that: The duration to subtract from `self`.
> Example
Subtract 11 months from a duration of 3 years
import Standard.Base.Data.Time.Duration
example_subtract = 3.years - 11.months
> Example
Substract 30 minutes from a duration of 6 hours.
import Standard.Base.Data.Time.Duration
example_subtract = (Duration.new hours=6) - (Duration.new minutes=30)
example_subtract = (Duration.hours 6) - (Duration.minutes 30)
- : Duration -> Duration ! Time_Error
- self that =
Panic.catch ArithmeticException (self.minus_builtin that) (err -> Error.throw (Time_Error_Data err.getMessage))
ensure_duration that <|
Panic.catch ArithmeticException (self.minus_builtin that) err->
Error.throw (Time_Error_Data err.getMessage)
## Check two durations for equality.
@ -128,9 +154,12 @@ type Duration
import Standard.Base.Data.Time.Duration
example_eq = (Duration.new seconds=60).total_minutes == (Duration.new minutes=1).total_minutes
example_eq = (Duration.seconds 60).total_minutes == (Duration.minutes 1).total_minutes
== : Duration -> Boolean
== self that = self.equals_builtin that
== self that =
case that of
_ : Duration -> self.equals_builtin that
_ -> False
## Compares `self` to `that` to produce an ordering.
@ -144,10 +173,12 @@ type Duration
example_compare_to =
duration_1 = (Duration.new hour=1)
duration_2 = (Duration.new minutes=60) + (Duration.new minutes=5)
duration_2 = (Duration.minutes 60) + (Duration.minutes 5)
duration_1.compare_to duration_2
compare_to : Duration -> Ordering
compare_to self that = Ordering.from_sign (self.compare_to_builtin that)
compare_to self that =
ensure_duration that <|
Ordering.from_sign (self.compare_to_builtin that)
## Get the portion of the duration expressed in nanoseconds.
@ -243,7 +274,7 @@ type Duration
import Standard.Base.Data.Time.Duration
example_to_vec = (Duration.new nanoseconds=800)).to_vector
example_to_vec = (Duration.nanoseconds 800)).to_vector
to_vector : Vector.Vector Integer
to_vector self = [self.hours, self.minutes, self.seconds, self.milliseconds, self.nanoseconds]
@ -254,7 +285,7 @@ type Duration
import Standard.Base.Data.Time.Duration
example_to_json = (Duration.new seconds=10).to_json
example_to_json = (Duration.seconds 10).to_json
to_json : Json.Object
to_json self =
b = Vector.new_builder

View File

@ -0,0 +1,155 @@
from Standard.Base import all
import Standard.Base.Data.Time.Duration
polyglot java import java.time.Period as Java_Period
polyglot java import java.time.DateTimeException
polyglot java import java.lang.ArithmeticException
## Create a Period representing the time interval between two dates.
Arguments:
- start_date_inclusive: The start date of the period, included.
- end_date_exclusive: The end date of the period, excluded.
> Example
Get a Period between 2022-10-21 and 2022-09-12
import Standard.Base.Data.Time.Period
example_period = Period.between (Date.new 2022 10 21) (Date.new 2022 9 12)
between : Date -> Date -> Period
between start_date_inclusive end_date_exclusive =
Period.Period_Data (Java_Period.between start_date_inclusive end_date_exclusive)
## Create a new Period from years, months and days.
Arguments:
- years: Amount of years.
- months: Amount of months.
- days: Amount of days.
> Example
Create a Period of 2 years and 5 days
import Standard.Base.Data.Time.Period
example_period = Period.new 2 0 5
new : Integer -> Integer -> Integer -> Period
new years=0 months=0 days=0 =
Period.Period_Data (Java_Period.of years months days)
## Create a new Period from days.
days : Integer -> Period
days d = new days=d
## Create a new Period from months.
months : Integer -> Period
months m = new months=m
## Create a new Period from years.
years : Integer -> Period
years y = new years=y
## PRIVATE
ensure_period : Any -> Suspend (Any -> Any) -> Text -> Any ! (Time_Error | Illegal_Argument_Error)
ensure_period object ~action error_msg="Cannot use Duration as a parameter" =
case object of
_ : Period -> action
_ : Duration.Duration ->
Error.throw (Time_Error_Data error_msg)
x ->
Error.throw Illegal_Argument_Error_Data <|
"Expected Period type, got: " + (Meta.get_qualified_type_name x)
## A date-based amount of time in the ISO-8601 calendar system, such as
'2 years, 3 months and 4 days'.
This type models an amount of time in terms of years, months and days.
`Duration` is its time-based equivalent. Moreover, `Period` counts with
daylight saving time. This means that a Period of 1 day does not necessarily
have to be 24 hours of Duration.
type Period
## PRIVATE
Arguments:
- internal_period: An internal representation of period of type
java.time.Period.
Period_Data internal_period
## Get the portion of the period expressed in years.
years : Integer
years self = self.internal_period.getYears
## Get the portion of the period expressed in months.
months : Integer
months self = self.internal_period.getMonths
## Get the portion of the period expressed in days.
days : Integer
days self = self.internal_period.getDays
## Add the specified amount of time to this period.
Arguments:
- other_period: The period to add to `self`. Note that this cannot be a
`Duration`, neither `Date_Time`.
> Example
Add 1 day to 1 month.
import Standard.Base.Data.Time.Period
example_add = (Period.months 1) + (Period.days 1)
+ : Period -> Period ! (Time_Error | Illegal_Argument_Error)
+ self other_period =
ensure_period other_period <|
Panic.catch Any (Period.Period_Data (self.internal_period.plus other_period.internal_period)) err->
case err of
_ : DateTimeException -> Error.throw Time_Error_Data "Period addition failed:"+err.getMessage
_ : ArithmeticException -> Error.throw Illegal_Argument_Error_Data "Arithmetic error:"+err.getMessage cause=err
## Subtract a specified amount of time from this period.
Arguments:
- other_period: Other Period to add to this Period. Note that this
cannot be a `Duration`, neither `Date_Time`.
> Example
Subtract 11 months from a period of 3 years
import Standard.Base.Data.Time.Period
example_subtract = (Period.years 3) - (Period.months 11)
- : Period -> Period ! (Time_Error | Illegal_Argument_Error)
- self other_period =
ensure_period other_period <|
Panic.catch Any (Period.Period_Data (self.internal_period.minus other_period.internal_period)) err->
case err of
DateTimeException -> Error.throw Time_Error_Data "Period subtraction failed"
ArithmeticException -> Error.throw Illegal_Argument_Error_Data "Arithmetic error"
## Check two periods for equality.
Note that two periods are equal if they have the exact same amount of
years, months, and days. So `(Period.days 30)` and
`(Period.months 1)` are not equal. Even `(Period.years 1)` and
`(Period.months 12)` are not equal.
Arguments:
- other_period: The period to compare against `self`.
== : Period -> Boolean
== self that =
ensure_period that <|
self.internal_period.equals that.internal_period
## Just throws Incomparable_Values_Error, because periods cannot be
compared without additional context.
To compare two Periods, use something like:
`(start_date + period1) .compare_to (start_date + period2)`
compare_to : Period -> Nothing ! Incomparable_Values_Error
compare_to self _ = Error.throw Incomparable_Values_Error

View File

@ -1,6 +1,7 @@
from Standard.Base import all
import Standard.Base.Data.Time.Duration
import Standard.Base.Data.Time.Period
import Standard.Base.Data.Time.Time_Period
from Standard.Base.Error.Common import Time_Error
@ -208,16 +209,20 @@ type Time_Of_Day
## Add the specified amount of time to this instant to get a new instant.
Arguments:
- amount: The amount of time to add to this instant.
- amount: The amount of time to add to this instant. Can be only
`Duration`.
> Example
from Standard.Base import Time_Of_Day
import Standard.Base.Data.Time.Duration
example_plus = Time_Of_Day.new + (Duration.new seconds=3)
+ : Duration -> Time_Of_Day
+ self amount = self.plus_builtin amount
example_plus = Time_Of_Day.new + (Duration.seconds 3)
+ : Duration -> Time_Of_Day ! Time_Error
+ self amount =
case amount of
duration : Duration.Duration -> self.plus_builtin duration
_ : Period.Period -> Error.throw (Time_Error_Data "Time_Of_Day does not support date intervals (periods)")
## Subtract the specified amount of time from this instant to get a new
instant.
@ -231,9 +236,12 @@ type Time_Of_Day
from Standard.Base import Time_Of_Day
import Standard.Base.Data.Time.Duration
example_minus = Time_Of_Day.now - (Duration.new hours=12)
- : Duration -> Time_Of_Day
- self amount = self.minus_builtin amount
example_minus = Time_Of_Day.now - (Duration.hours 12)
- : Duration -> Time_Of_Day ! Time_Error
- self amount =
case amount of
duration : Duration.Duration -> self.minus_builtin duration
_ : Period.Period -> Error.throw (Time_Error_Data "Time_Of_Day does not support date intervals (periods)")
## Format this time of day as text using the default formatter.

View File

@ -31,6 +31,7 @@ import project.Data.Text.Text_Ordering
import project.Data.Text.Span
import project.Data.Time.Date
import project.Data.Time.Date_Time
import project.Data.Time.Duration
import project.Data.Time.Time_Of_Day
import project.Data.Time.Time_Zone
import project.Data.Time.Day_Of_Week.Day_Of_Week
@ -76,6 +77,7 @@ export project.Data.Text.Text_Matcher
export project.Data.Text.Regex_Matcher
export project.Data.Time.Date
export project.Data.Time.Date_Time
export project.Data.Time.Duration
export project.Data.Time.Time_Of_Day
export project.Data.Time.Time_Zone
export project.Data.Time.Day_Of_Week.Day_Of_Week

View File

@ -327,6 +327,7 @@ is_a value typ = if is_same_object value typ then True else
_ : Decimal -> typ == Decimal
_ : Date_Time.Date_Time -> typ.is_same_object_as Date_Time.Date_Time
_ : Date.Date -> typ.is_same_object_as Date.Date
_ : Duration.Duration -> typ.is_same_object_as Duration.Duration
_ : Time_Of_Day.Time_Of_Day -> typ.is_same_object_as Time_Of_Day.Time_Of_Day
_ : Time_Zone.Time_Zone -> typ.is_same_object_as Time_Zone.Time_Zone
Base.Polyglot ->

View File

@ -36,7 +36,7 @@ polyglot java import org.enso.base.Http_Utils
> Example
Create an HTTP client with extended timeout.
Http.new timeout=(Duration.new seconds=30)
Http.new timeout=(Duration.seconds 30)
> Example
Create an HTTP client with extended timeout and proxy settings.
@ -46,9 +46,9 @@ polyglot java import org.enso.base.Http_Utils
import Standard.Base.Network.Proxy
example_new =
Http.new (timeout = (Duration.new seconds=30)) (proxy = Proxy.new "example.com" 8080)
Http.new (timeout = (Duration.seconds 30)) (proxy = Proxy.new "example.com" 8080)
new : Duration -> Boolean -> Proxy -> Http
new (timeout = (Duration.new seconds=10)) (follow_redirects = True) (proxy = Proxy.System) (version = Version.Http_1_1) =
new (timeout = (Duration.seconds 10)) (follow_redirects = True) (proxy = Proxy.System) (version = Version.Http_1_1) =
Http_Data timeout follow_redirects proxy version
## Send an Options request.
@ -595,7 +595,7 @@ type Http
example_request =
form = [Form.text_field "name" "John Doe"]
req = Request.new Method.Post "http://httpbin.org/post" . with_form form
http = Http.new (timeout = (Duration.new seconds=30))
http = Http.new (timeout = (Duration.seconds 30))
http.request req
request : Request -> Response ! Request_Error
request self req =

View File

@ -86,19 +86,25 @@ public abstract class TypeOfNode extends Node {
guards = {"interop.isTimeZone(value)", "!interop.isDate(value)", "!interop.isTime(value)"})
Object doTimeZone(Object value, @CachedLibrary(limit = "3") InteropLibrary interop) {
Context ctx = Context.get(this);
return Context.get(this).getBuiltins().timeZone();
return ctx.getBuiltins().timeZone();
}
@Specialization(guards = {"interop.isDate(value)", "!interop.isTime(value)"})
Object doDate(Object value, @CachedLibrary(limit = "3") InteropLibrary interop) {
Context ctx = Context.get(this);
return Context.get(this).getBuiltins().date();
return ctx.getBuiltins().date();
}
@Specialization(guards = {"interop.isTime(value)", "!interop.isDate(value)"})
Object doTime(Object value, @CachedLibrary(limit = "3") InteropLibrary interop) {
Context ctx = Context.get(this);
return Context.get(this).getBuiltins().timeOfDay();
return ctx.getBuiltins().timeOfDay();
}
@Specialization(guards = "interop.isDuration(value)")
Object doDuration(Object value, @CachedLibrary(limit = "3") InteropLibrary interop) {
Context ctx = Context.get(this);
return ctx.getBuiltins().duration();
}
@Specialization(

View File

@ -215,6 +215,7 @@ public final class ParseStdLibTest extends TestCase {
"Data/Time/Date_Period.enso",
"Data/Time/Date_Time.enso",
"Data/Time/Duration.enso",
"Data/Time/Period.enso",
"Data/Time/Time_Of_Day.enso",
"Data/Time/Time_Zone.enso",
"Data/Vector.enso",

View File

@ -79,15 +79,18 @@ public class Time_Utils {
return date.atTime(time).atZone(zone);
}
public static LocalDate date_adjust(LocalDate date, AdjustOp op, Period duration) {
switch (op) {
case PLUS:
return date.plus(duration);
case MINUS:
return date.minus(duration);
default:
throw new DateTimeException("Unknown adjust operation");
}
public static LocalDate date_adjust(LocalDate date, AdjustOp op, Period period) {
return switch (op) {
case PLUS -> date.plus(period);
case MINUS -> date.minus(period);
};
}
public static ZonedDateTime datetime_adjust(ZonedDateTime datetime, AdjustOp op, Period period) {
return switch (op) {
case PLUS -> datetime.plus(period);
case MINUS -> datetime.minus(period);
};
}
public static int get_field_as_localdate(LocalDate date, TemporalField field) {

View File

@ -1,5 +1,5 @@
from Standard.Base import all
import Standard.Base.Data.Time.Duration
from Standard.Base.Data.Time import Duration, Period
from Standard.Test import Bench
@ -62,10 +62,10 @@ main =
first_day = Date.new 1971
date_vec = integer_vec.map x->
first_day + x.days
first_day + (Period.new days=x)
first_dt = Date_Time.new 1971
datetime_vec = integer_vec.map x->
first_dt + x.minutes
first_dt + (Duration.new minutes=x)
Bench.measure (count_entries integer_vec 4567) "Integer Equality" iter_size num_iterations
Bench.measure (count_entries boolean_vec True expected_count=(n . div 5)) "Boolean Equality" iter_size num_iterations

View File

@ -1,6 +1,6 @@
from Standard.Base import all
from Standard.Base.Data.Index_Sub_Range import Sample
import Standard.Base.Data.Time.Duration
import Standard.Base.Data.Time.Period
from Standard.Test import Bench
@ -11,7 +11,7 @@ main =
num_iterations = 10
first_day = Date.new 2020 1 1
dates = Vector.new 1000 (x -> first_day + x.days)
dates = Vector.new 1000 (x -> first_day + (Period.new days=x))
holidays = dates.take (Sample 100 100)
shifts = [1, 5, 20, 100]
@ -20,7 +20,7 @@ main =
Bench.measure (dates.zip shifted_dates d1-> d2-> d1.work_days_until d2) "(Shift="+shift.to_text+") work_days_until" iter_size num_iterations
Bench.measure (dates.zip shifted_dates d1-> d2-> d1.work_days_until d2 holidays=holidays) "(Shift="+shift.to_text+") work_days_until with holidays" iter_size num_iterations
Bench.measure (dates.map date-> date + shift.days) "(Shift="+shift.to_text+") add regular days" iter_size num_iterations
Bench.measure (dates.map date-> date + (Period.new days=shift)) "(Shift="+shift.to_text+") add regular days" iter_size num_iterations
Bench.measure (dates.map date-> date.add_work_days shift) "(Shift="+shift.to_text+") add work days" iter_size num_iterations
Bench.measure (dates.map date-> date.add_work_days shift holidays=holidays) "(Shift="+shift.to_text+") add work days with holidays" iter_size num_iterations

View File

@ -3,6 +3,7 @@ from Standard.Base import all
import Standard.Base.Data.Text.Text_Sub_Range
import Standard.Base.Data.Time.Date_Period
import Standard.Base.Data.Time.Duration
import Standard.Base.Data.Time.Period
from Standard.Base.Error.Common import Time_Error
from Standard.Test import Test, Test_Suite
@ -92,35 +93,35 @@ spec_with name create_new_date parse_date =
date.to_json.should_equal <|
Json.from_pairs [["type", "Date"], ["day", date.day], ["month", date.month], ["year", date.year]]
Test.specify "should add date-based interval" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
date = create_new_date 1970 + 1.day
Test.specify "should add date-based interval" <|
date = create_new_date 1970 + (Period.days 1)
date . year . should_equal 1970
date . month . should_equal 1
date . day . should_equal 2
Test.specify "should subtract date-based interval" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
date = create_new_date 1970 - 1.year
Test.specify "should subtract date-based interval" <|
date = create_new_date 1970 - (Period.years 1)
date . year . should_equal 1969
date . month . should_equal 1
date . day . should_equal 1
Test.specify "should support mixed interval operators" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
date = create_new_date 1970 + 1.month - 1.year
Test.specify "should support mixed interval operators" <|
date = create_new_date 1970 + (Period.months 1) - (Period.years 1)
date . year . should_equal 1969
date . month . should_equal 2
date . day . should_equal 1
Test.specify "should throw error when adding time-based interval" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
case (create_new_date 1970 + (Duration.new hours=1)) . catch of
Test.specify "should throw error when adding time-based Duration" <|
case (create_new_date 1970 + (Duration.hours 1)) . catch of
Time_Error_Data message ->
message . should_equal "Date does not support time intervals"
message . should_equal "Date does not support adding/subtracting Duration. Use Period instead."
result ->
Test.fail ("Unexpected result: " + result.to_text)
Test.specify "should throw error when subtracting time-based interval" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
case (create_new_date 1970 - (1.day - (Duration.new minutes=1))) . catch of
Test.specify "should throw error when subtracting time-based Duration" <|
case (create_new_date 1970 - (Duration.minutes 1)) . catch of
Time_Error_Data message ->
message . should_equal "Date does not support time intervals"
message . should_equal "Date does not support adding/subtracting Duration. Use Period instead."
result ->
Test.fail ("Unexpected result: " + result.to_text)
@ -228,7 +229,7 @@ spec_with name create_new_date parse_date =
(create_new_date 2000 7 1).end_of Date_Period.Quarter . should_equal (Date.new 2000 9 30)
(create_new_date 2000 6 30).end_of Date_Period.Quarter . should_equal (Date.new 2000 6 30)
Test.specify "should allow to compute the number of working days until a later date" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
Test.specify "should allow to compute the number of working days until a later date" <|
# 2000-2-1 is a Tuesday
create_new_date 2000 2 1 . work_days_until (create_new_date 2000 2 1) . should_equal 0
create_new_date 2000 2 1 . work_days_until (create_new_date 2000 2 2) . should_equal 1
@ -279,7 +280,7 @@ spec_with name create_new_date parse_date =
# We duplicate the holiday entries to check that the functions are resilient to such input data.
duplicated_holiday_november year =
holiday_november year + holiday_november year + holiday_november year
Test.specify "should allow to compute the number of working days until a date, skipping custom set holidays" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
Test.specify "should allow to compute the number of working days until a date, skipping custom set holidays" <|
holiday_february = Vector.new 29 (i -> create_new_date 2000 2 i+1)
create_new_date 2000 2 1 . work_days_until (create_new_date 2000 3 1) holiday_february . should_equal 0
create_new_date 2000 2 10 . work_days_until (create_new_date 2000 2 12) holiday_february . should_equal 0
@ -294,7 +295,7 @@ spec_with name create_new_date parse_date =
create_new_date 2000 11 1 . work_days_until (create_new_date 2000 12 1) (duplicated_holiday_november 2020) . should_equal 22
create_new_date 1999 11 1 . work_days_until (create_new_date 1999 12 1) (duplicated_holiday_november 1999) . should_equal 19
Test.specify "should allow to compute the number of working days including the end, in a manner consistent with NETWORKDAYS" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
Test.specify "should allow to compute the number of working days including the end, in a manner consistent with NETWORKDAYS" <|
create_new_date 2000 2 1 . work_days_until (create_new_date 2000 2 1) include_end_date=True . should_equal 1
create_new_date 2000 2 1 . work_days_until (create_new_date 2000 2 2) include_end_date=True . should_equal 2
create_new_date 2000 2 3 . work_days_until (create_new_date 2000 2 16) include_end_date=True . should_equal 10
@ -311,7 +312,7 @@ spec_with name create_new_date parse_date =
create_new_date 2000 2 6 . work_days_until (create_new_date 2000 2 8) include_end_date=True . should_equal 2
create_new_date 2000 2 6 . work_days_until (create_new_date 2000 2 5) include_end_date=True . should_equal 0
Test.specify "should allow to shift the date by N working days" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
Test.specify "should allow to shift the date by N working days" <|
# 2000-2-1 is a Tuesday
create_new_date 2000 2 1 . add_work_days 0 . should_equal (Date.new 2000 2 1)
create_new_date 2000 2 1 . add_work_days . should_equal (Date.new 2000 2 2)
@ -343,7 +344,7 @@ spec_with name create_new_date parse_date =
create_new_date 2022 3 27 . add_work_days 0 . should_equal (Date.new 2022 3 28)
create_new_date 2022 3 27 . add_work_days 1 . should_equal (Date.new 2022 3 29)
Test.specify "should allow to shift the date by negative amount of working days" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
Test.specify "should allow to shift the date by negative amount of working days" <|
# 2000-2-1 is a Tuesday
create_new_date 2000 2 1 . add_work_days -1 . should_equal (Date.new 2000 1 31)
create_new_date 2000 2 1 . add_work_days -2 . should_equal (Date.new 2000 1 28)
@ -372,10 +373,10 @@ spec_with name create_new_date parse_date =
create_new_date 2000 2 6 . add_work_days -5 . should_equal (Date.new 2000 1 31)
create_new_date 2000 2 6 . add_work_days -6 . should_equal (Date.new 2000 1 28)
Test.specify "should allow to shift the date by N working days, skipping custom holidays" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
Test.specify "should allow to shift the date by N working days, skipping custom holidays" <|
all_year_holiday year =
first_day = create_new_date year 1 1
Vector.new first_day.length_of_year (n -> first_day + n.days)
Vector.new first_day.length_of_year (n -> first_day + (Period.days n))
two_years_vacation = all_year_holiday 1999 + all_year_holiday 2000
@ -393,10 +394,10 @@ spec_with name create_new_date parse_date =
create_new_date 1999 10 30 . add_work_days 0 (duplicated_holiday_november 1999) . should_equal (Date.new 1999 11 3)
create_new_date 1999 10 30 . add_work_days 1 (duplicated_holiday_november 1999) . should_equal (Date.new 1999 11 4)
Test.specify "add_work_days and work_days_until should be consistent with each other" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
Test.specify "add_work_days and work_days_until should be consistent with each other" <|
first_day = create_new_date 2020 1 1
dates = Vector.new 100 (n -> first_day + n.days)
holidays = [1, 2, 10, 11, 12, 13, 14, 15, 30, 40, 41, 42, 50, 60].map (n -> first_day + n.days)
dates = Vector.new 100 (n -> first_day + (Period.new days=n))
holidays = [1, 2, 10, 11, 12, 13, 14, 15, 30, 40, 41, 42, 50, 60].map (n -> first_day + (Period.new days=n))
shifts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 90, 100]
dates.each date->
date.work_days_until (date.add_work_days 0) . should_equal 0

View File

@ -1,7 +1,5 @@
from Standard.Base import all
import Standard.Base.Data.Time.Date_Period
import Standard.Base.Data.Time.Time_Period
import Standard.Base.Data.Time.Duration
from Standard.Base.Data.Time import Duration, Period, Date_Period, Time_Period
from Standard.Test import Test, Test_Suite
@ -231,7 +229,7 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=
time . day . should_equal 1
Test.specify "should add time interval" <|
time = create_new_datetime 1970 (zone = Time_Zone.utc) + (Duration.new nanoseconds=1)
time = create_new_datetime 1970 (zone = Time_Zone.utc) + (Duration.nanoseconds 1)
time . year . should_equal 1970
time . month . should_equal 1
time . day . should_equal 1
@ -241,8 +239,8 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=
time . nanosecond . should_equal 1
time . zone . should_equal Time_Zone.utc
Test.specify "should add date interval" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
time = create_new_datetime 1970 (zone = Time_Zone.utc) + 1.month
Test.specify "should add date interval" <|
time = (create_new_datetime 1970 (zone = Time_Zone.utc)) + (Period.months 1)
time . year . should_equal 1970
time . month . should_equal 2
time . day . should_equal 1
@ -252,8 +250,8 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=
time . nanosecond . should_equal 0
time . zone . zone_id . should_equal Time_Zone.utc.zone_id
Test.specify "should add mixed date time interval" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
time = create_new_datetime 1970 (zone = Time_Zone.utc) + (1.month + (Duration.new hours=3))
Test.specify "should add mixed date time interval" <|
time = create_new_datetime 1970 (zone = Time_Zone.utc) + (Period.months 1) + (Duration.hours 3)
time . year . should_equal 1970
time . month . should_equal 2
time . day . should_equal 1
@ -264,7 +262,7 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=
time . zone . zone_id . should_equal Time_Zone.utc.zone_id
Test.specify "should subtract time interval" <|
time = create_new_datetime 1970 (zone = Time_Zone.utc) - (Duration.new hours=1)
time = create_new_datetime 1970 (zone = Time_Zone.utc) - (Duration.hours 1)
time . year . should_equal 1969
time . month . should_equal 12
time . day . should_equal 31
@ -274,8 +272,8 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=
time . nanosecond . should_equal 0
time . zone . zone_id . should_equal Time_Zone.utc.zone_id
Test.specify "should subtract date interval" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
time = create_new_datetime 1970 (zone = Time_Zone.utc) - 1.month
Test.specify "should subtract date interval" <|
time = create_new_datetime 1970 (zone = Time_Zone.utc) - (Period.months 1)
time . year . should_equal 1969
time . month . should_equal 12
time . day . should_equal 1
@ -285,19 +283,19 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=
time . nanosecond . should_equal 0
time . zone . zone_id . should_equal Time_Zone.utc.zone_id
Test.specify "should subtract mixed date time interval" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
time = create_new_datetime 1970 (zone = Time_Zone.utc) - (1.month - (Duration.new hours=3))
Test.specify "should subtract mixed date time interval" <|
time = create_new_datetime 1970 (zone = Time_Zone.utc) - (Period.months 1) - (Duration.hours 3)
time . year . should_equal 1969
time . month . should_equal 12
time . day . should_equal 1
time . hour . should_equal 3
time . month . should_equal 11
time . day . should_equal 30
time . hour . should_equal 21
time . minute . should_equal 0
time . second . should_equal 0
time . nanosecond . should_equal 0
time . zone . zone_id . should_equal Time_Zone.utc.zone_id
Test.specify "should support mixed interval operators" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
time = create_new_datetime 1970 (zone = Time_Zone.utc) - 1.month + (Duration.new hours=12)
Test.specify "should support mixed interval operators" <|
time = create_new_datetime 1970 (zone = Time_Zone.utc) - (Period.months 1) + (Duration.hours 12)
time . year . should_equal 1969
time . month . should_equal 12
time . day . should_equal 1
@ -450,7 +448,7 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=
Test.specify "should find start/end of a Date_Period or Time_Period containing the current datetime correctly near the spring DST switch" pending=js_dst_pending <|
d1 = create_new_datetime 2022 3 27 1 34 15 0 tz
d2 = create_new_datetime 2022 3 27 3 34 15 0 tz
d1_plus = d1 + (Duration.new hours=1)
d1_plus = d1 + (Duration.hours 1)
d1_plus . should_equal d2
check_dates_spring date =
@ -482,7 +480,7 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=
dst_overlap_message = "We cannot correctly migrate the datetime inside of the timeline overlap through the polyglot boundar - as due to polyglot conversion limitation, always the earlier one is chosen. See the bug report: https://github.com/oracle/graal/issues/4918"
Test.specify "should find start/end of a Date_Period or Time_Period containing the current datetime correctly near the autumn DST switch" pending=dst_overlap_message <|
d3 = create_new_datetime 2022 10 30 2 30 15 0 tz
d4 = d3 + (Duration.new hours=1)
d4 = d3 + (Duration.hours 1)
d3.hour . should_equal 2
d4.hour . should_equal 2
@ -524,7 +522,7 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=
d4_end.nanosecond . should_equal max_nanos
Time_Utils.get_datetime_offset d4_end . should_equal offset_1_h
Test.specify "should allow to shift the date by N working days" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
Test.specify "should allow to shift the date by N working days" <|
# 2000-2-1 is a Tuesday
create_new_datetime 2000 2 1 12 30 . add_work_days 0 . should_equal (Date_Time.new 2000 2 1 12 30)
create_new_datetime 2000 2 1 12 15 55 . add_work_days . should_equal (Date_Time.new 2000 2 2 12 15 55)
@ -545,9 +543,9 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=
create_new_datetime 2000 2 27 12 10 . add_work_days 3 . should_equal (Date_Time.new 2000 3 2 12 10)
create_new_datetime 1999 2 27 12 10 . add_work_days 3 . should_equal (Date_Time.new 1999 3 4 12 10)
Test.specify "should handle shifting dates around spring DST edge cases" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
Test.specify "should handle shifting dates around spring DST edge cases" <|
# 2022-10-30 and 2022-03-27 are DST switch days, Sundays.
create_new_datetime 2022 10 30 2 30 55 1234 . add_work_days 0 . should_equal (Date_Time.new 2022 10 31 2 30 55 1234)
create_new_datetime 2022 10 30 2 30 55 1234 . add_work_days 0 . should_equal (create_new_datetime 2022 10 31 2 30 55 1234)
create_new_datetime 2022 10 30 1 30 . add_work_days 1 . should_equal (Date_Time.new 2022 11 1 1 30)
create_new_datetime 2022 10 30 3 30 . add_work_days 1 . should_equal (Date_Time.new 2022 11 1 3 30)
@ -556,12 +554,12 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=
Test.specify "should handle shifting dates around autumn DST edge cases" pending=dst_overlap_message <|
d3 = create_new_datetime 2022 10 30 2 30 15 0 tz
d4 = d3 + (Duration.new hours=1)
d4 = d3 + (Duration.hours 1)
# TODO we need to check and document the actual behaviour once it is expressible, it may be equally acceptable to shift to 3:30 instead of 2:30.
d4 . add_work_days 0 . should_equal (Date_Time.new 2022 10 31 2 30 15 0 tz)
Test.specify "should allow to shift the date by negative amount of working days" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
Test.specify "should allow to shift the date by negative amount of working days" <|
# 2000-2-1 is a Tuesday
create_new_datetime 2000 2 1 12 30 . add_work_days -1 . should_equal (Date_Time.new 2000 1 31 12 30)
create_new_datetime 2000 2 1 13 30 . add_work_days -2 . should_equal (Date_Time.new 2000 1 28 13 30)
@ -571,10 +569,10 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=
create_new_datetime 2000 2 6 0 1 . add_work_days -2 . should_equal (Date_Time.new 2000 2 3 0 1)
create_new_datetime 2000 2 6 23 59 . add_work_days -6 . should_equal (Date_Time.new 2000 1 28 23 59)
Test.specify "should allow to shift the date by N working days, skipping custom holidays" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
Test.specify "should allow to shift the date by N working days, skipping custom holidays" <|
all_year_holiday year =
first_day = Date.new year 1 1
Vector.new first_day.length_of_year (n -> first_day + n.days)
Vector.new first_day.length_of_year (n -> first_day + (Period.new days=n))
holiday_november year =
[Date.new year 11 1, Date.new year 11 2, Date.new year 11 11]
# We duplicate the holiday entries to check that the functions are resilient to such input data.

View File

@ -1,6 +1,7 @@
from Standard.Base import all
import Standard.Base.Data.Time.Duration
import Standard.Base.Data.Time.Period
from Standard.Test import Test, Test_Suite
polyglot java import java.time.Duration as Java_Duration
@ -24,7 +25,7 @@ spec =
Test.group "Duration" <|
Test.specify "should create interval seconds" <|
duration = (Duration.new seconds=5)
duration = (Duration.seconds 5)
duration.seconds . should_equal 5
duration.milliseconds . should_equal 0
@ -38,42 +39,54 @@ spec =
interval.is_empty . should_be_true
Test.specify "should normalize periods" <|
(Duration.new seconds=60).total_minutes . should_equal 1
(Duration.new milliseconds=1000).total_seconds . should_equal 1
(Duration.seconds 60).total_minutes . should_equal 1
(Duration.milliseconds 1000).total_seconds . should_equal 1
Test.specify "should normalize addition" <|
duration = (Duration.new hours=11) + (Duration.new hours=1)
duration = (Duration.hours 11) + (Duration.hours 1)
duration.hours . should_equal 12
Test.specify "should normalize subtraction" <|
duration = (Duration.new hours=13) - (Duration.new hours=1)
duration = (Duration.hours 13) - (Duration.hours 1)
duration.hours . should_equal 12
Test.specify "should convert to Json" <|
interval = (Duration.new nanoseconds=120) + (Duration.new seconds=30) + (Duration.new hours=14)
interval = (Duration.nanoseconds 120) + (Duration.seconds 30) + (Duration.hours 14)
interval.to_json.should_equal <|
duration_pairs = [["nanoseconds", interval.nanoseconds], ["seconds", interval.seconds], ["hours", interval.hours]]
Json.from_pairs ([["type", "Duration"]] + duration_pairs)
Test.specify "should be comparable" <|
duration_1 = (Duration.new hours=5)
duration_2 = (Duration.new minutes=1)
duration_1 = (Duration.hours 5)
duration_2 = (Duration.minutes 1)
duration_1.compare_to duration_1 . should_equal Ordering.Equal
duration_1==duration_1 . should_be_true
duration_1!=duration_2 . should_be_true
duration_1>duration_2 . should_be_true
duration_1<duration_2 . should_be_false
Test.specify "should not mix Duration and Period" <|
durations = [(Duration.hours 1), (Duration.zero), (Duration.new hours=1 seconds=30)]
periods = [(Period.days 1), (Period.new 0), (Period.years 30), (Period.new years=3 months=2)]
durations.each duration->
periods.each period->
(duration + period).should_fail_with Time_Error_Data
(duration - period).should_fail_with Time_Error_Data
(period + duration).should_fail_with Time_Error_Data
(period - duration).should_fail_with Time_Error_Data
(duration > period).should_fail_with Time_Error_Data
(duration < period).should_fail_with Time_Error_Data
Test.specify "Date_Time supports adding and subtracting Duration" <|
((Date_Time.new 2022 10 1 hour=10) + (Duration.new hours=2)) . should_equal (Date_Time.new 2022 10 1 hour=12)
((Date_Time.new 2022 10 1 hour=10) - (Duration.new hours=2)) . should_equal (Date_Time.new 2022 10 1 hour=8)
((Date_Time.new 2022 10 2) - (Duration.new hours=24)) . should_equal (Date_Time.new 2022 10 1)
((Date_Time.new 2022 10 1 hour=2) - (Duration.new minutes=3)) . should_equal (Date_Time.new 2022 10 1 hour=1 minute=57)
((Date_Time.new 2022 10 1 hour=10) + (Duration.hours 2)) . should_equal (Date_Time.new 2022 10 1 hour=12)
((Date_Time.new 2022 10 1 hour=10) - (Duration.hours 2)) . should_equal (Date_Time.new 2022 10 1 hour=8)
((Date_Time.new 2022 10 2) - (Duration.hours 24)) . should_equal (Date_Time.new 2022 10 1)
((Date_Time.new 2022 10 1 hour=2) - (Duration.minutes 3)) . should_equal (Date_Time.new 2022 10 1 hour=1 minute=57)
Test.specify "Java Duration is equal to Enso Duration" <|
(Duration.new hours=1) . should_equal (Java_Duration.ofHours 1)
(Duration.new minutes=80) . should_equal (Java_Duration.ofMinutes 80)
(Java_Duration.ofSeconds 30) . should_equal (Duration.new seconds=30)
(Duration.hours 1) . should_equal (Java_Duration.ofHours 1)
(Duration.minutes 80) . should_equal (Java_Duration.ofMinutes 80)
(Java_Duration.ofSeconds 30) . should_equal (Duration.seconds 30)
Test.specify "Difference of Java Date and Enso date should be an Enso Duration" <|
(Duration.between (java_datetime 2022 01 01) (Date_Time.new 2022 01 02) timezone_aware=False).total_hours . should_equal 24

View File

@ -0,0 +1,43 @@
from Standard.Base import all
from Standard.Test import Test, Test_Suite
from Standard.Base.Data.Time import Period
spec =
Test.group "Period" <|
Test.specify "should create period years" <|
period = (Period.years 5)
period.years . should_equal 5
period.days . should_equal 0
Test.specify "should add two Periods" <|
((Period.years 1) + (Period.years 2)).years . should_equal 3
((Period.days 1) + (Period.months 2)).days . should_equal 1
((Period.days 1) + (Period.months 2)).months . should_equal 2
((Period.months 2) + (Period.days 1)).days . should_equal 1
((Period.months 2) + (Period.days 1)).months . should_equal 2
Test.specify "should subtract two Periods" <|
((Period.years 2) - (Period.years 1)).years . should_equal 1
((Period.years 1) - (Period.months 2)).months . should_equal (-2)
((Period.years 1) - (Period.months 2)).years . should_equal 1
Test.specify "should get Period between two dates" <|
(Period.between (Date.new year=100) (Date.new year=150)) . should_equal (Period.years 50)
(Period.between (Date.new year=150) (Date.new year=100)) . should_equal (Period.new years=(-50))
(Period.between (Date.new 2022 10 19) (Date.new 2022 11 01)) . should_equal (Period.days 13)
Test.specify "should not compare between two periods" <|
((Period.days 10) > (Period.days 1)) . should_fail_with Incomparable_Values_Error
((Period.years 10) > (Period.days 1)) . should_fail_with Incomparable_Values_Error
((Period.new years=10 months=3) > (Period.months 5)) . should_fail_with Incomparable_Values_Error
Test.specify "two Periods are equal iff their fields are equal" <|
((Period.days 1) == (Period.days 1)) . should_be_true
((Period.months 12) == (Period.years 1)) . should_be_false
((Period.months 3) == (Period.months 3)) . should_be_true
((Period.days (-5)) == (Period.days (-5))) . should_be_true
((Period.new years=1 days=10) == (Period.new years=1 days=10)) . should_be_true
((Period.days 1) != (Period.months 1)) . should_be_true
main = Test_Suite.run_main spec

View File

@ -3,6 +3,7 @@ from Standard.Base import all
from Standard.Test import Test_Suite
import project.Data.Time.Duration_Spec
import project.Data.Time.Period_Spec
import project.Data.Time.Time_Of_Day_Spec
import project.Data.Time.Date_Spec
import project.Data.Time.Date_Time_Spec
@ -12,6 +13,7 @@ import project.Data.Time.Day_Of_Week_Spec
spec =
Date_Spec.spec
Duration_Spec.spec
Period_Spec.spec
Time_Of_Day_Spec.spec
Date_Time_Spec.spec
Time_Zone_Spec.spec

View File

@ -1,6 +1,5 @@
from Standard.Base import all
import Standard.Base.Data.Time.Duration
import Standard.Base.Data.Time.Time_Period
from Standard.Base.Data.Time import Duration, Period, Time_Period
from Standard.Base.Error.Common import Time_Error_Data
from Standard.Test import Test, Test_Suite
@ -83,28 +82,28 @@ specWith name create_new_time parse_time =
datetime . zone . zone_id . should_equal Time_Zone.utc.zone_id
Test.specify "should add time-based interval" <|
time = create_new_time 0 + (Duration.new minutes=1)
time = create_new_time 0 + (Duration.minutes 1)
time . to_seconds . should_equal 60
Test.specify "should subtract time-based interval" <|
time = create_new_time 0 - (Duration.new minutes=1)
time = create_new_time 0 - (Duration.minutes 1)
time . to_seconds . should_equal 86340
Test.specify "should support mixed interval operators" <|
time = create_new_time 0 + (Duration.new hours=1) - (Duration.new seconds=1)
time = create_new_time 0 + (Duration.hours 1) - (Duration.seconds 1)
time . to_seconds . should_equal 3599
Test.specify "should throw error when adding date-based interval" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
case (create_new_time 0 + 1.day) . catch of
Test.specify "should throw error when adding date-based interval" <|
case (create_new_time 0 + (Period.days 1)) . catch of
Time_Error_Data message ->
message . should_equal "Time_Of_Day does not support date intervals"
message . should_equal "Time_Of_Day does not support date intervals (periods)"
result ->
Test.fail ("Unexpected result: " + result.to_text)
Test.specify "should throw error when subtracting date-based interval" pending="Wait until Period type is implemented (https://www.pivotaltracker.com/story/show/183336003)" <|
case (create_new_time 0 - (1.day - (Duration.new minutes=1))) . catch of
Test.specify "should throw error when subtracting date-based interval" <|
case (create_new_time 0 - (Period.days 1)) . catch of
Time_Error_Data message ->
message . should_equal "Time_Of_Day does not support date intervals"
message . should_equal "Time_Of_Day does not support date intervals (periods)"
result ->
Test.fail ("Unexpected result: " + result.to_text)

View File

@ -30,8 +30,8 @@ spec =
url_get = base_url_with_slash + "get"
url_post = base_url_with_slash + "post"
Test.specify "should create HTTP client with timeout setting" <|
http = Http.new (timeout = (Duration.new seconds=30))
http.timeout.should_equal (Duration.new seconds=30)
http = Http.new (timeout = (Duration.seconds 30))
http.timeout.should_equal (Duration.seconds 30)
Test.specify "should create HTTP client with follow_redirects setting" <|
http = Http.new (follow_redirects = False)
http.follow_redirects.should_equal False