Add Date_Range (#6621)

Closes #6543
This commit is contained in:
Radosław Waśko 2023-05-11 18:03:02 +02:00 committed by GitHub
parent 1302b693d8
commit cd7fb73232
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 892 additions and 74 deletions

View File

@ -433,6 +433,7 @@
- [Implemented `Column.format` for in-memory `Column`s.][6538]
- [Added `at_least_one` flag to `Table.tokenize_to_rows`.][6539]
- [Moved `Redshift` connector into a separate `AWS` library.][6550]
- [Added `Date_Range`.][6621]
[debug-shortcuts]:
https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug
@ -642,6 +643,7 @@
[6538]: https://github.com/enso-org/enso/pull/6538
[6539]: https://github.com/enso-org/enso/pull/6539
[6550]: https://github.com/enso-org/enso/pull/6550
[6621]: https://github.com/enso-org/enso/pull/6621
#### Enso Compiler

View File

@ -745,7 +745,9 @@ type Array
reverse : Vector Any
reverse self = Vector.reverse self
## Applies a function to each element of the array.
## PRIVATE
ADVANCED
Applies a function to each element of the array.
Unlike `map`, this method does not return the individual results,
therefore it is only useful for side-effecting computations.
@ -760,7 +762,9 @@ type Array
each : (Any -> Any) -> Nothing
each self f = Vector.each self f
## Applies a function to each element of the array.
## PRIVATE
ADVANCED
Applies a function to each element of the array.
Arguments:
- function: A function to apply that takes an index and an item.

View File

@ -286,7 +286,9 @@ type List
go t res.fill
res.value
## Applies a function to each element of the list.
## PRIVATE
ADVANCED
Applies a function to each element of the list.
Arguments:
- f: The function to apply to each element of the list.

View File

@ -308,7 +308,9 @@ type Map key value
self.to_vector.fold init acc-> pair->
function acc pair.first pair.last
## Applies a function to each value in the map.
## PRIVATE
ADVANCED
Applies a function to each value in the map.
Arguments:
- function: The function to apply to each value in the map, taking a
@ -329,7 +331,9 @@ type Map key value
kv_func = _ -> function
self.each_with_key kv_func
## Applies a function to each key-value pair in the map.
## PRIVATE
ADVANCED
Applies a function to each key-value pair in the map.
Arguments:
- function: The function to apply to each key-value pair in the map,

View File

@ -232,7 +232,9 @@ type Pair
reverse : Pair
reverse self = Pair.new self.second self.first
## Applies a function to each element of the pair.
## PRIVATE
ADVANCED
Applies a function to each element of the pair.
Unlike `map`, this method does not return the individual results,
therefore it is only useful for side-effecting computations.

View File

@ -1,7 +1,6 @@
import project.Any.Any
import project.Data.Filter_Condition.Filter_Condition
import project.Data.Numbers.Integer
import project.Data.Numbers.Number
import project.Data.Text.Text
import project.Data.Vector.Vector
import project.Errors.Common.Index_Out_Of_Bounds
@ -55,18 +54,21 @@ type Range
_ ->
Error.throw (Illegal_Argument.Error "Range step should be an integer.")
## Returns the first element that is included within the range or `Nothing`
if the range is empty.
## Returns the first element that is included within the range.
It will raise `Index_Out_Of_Bounds` if the range is empty.
first : Integer ! Index_Out_Of_Bounds
first self = if self.is_empty then Error.throw (Index_Out_Of_Bounds.Error 0 0) else self.start
## Returns the second element that is included within the range or `Nothing`
if the range has less than 2 element
## Returns the second element that is included within the range.
It will raise `Index_Out_Of_Bounds` if the range has less than two elements.
second : Integer ! Index_Out_Of_Bounds
second self = if self.length < 2 then Error.throw (Index_Out_Of_Bounds.Error 1 self.length) else self.start + self.step
## Returns the last element that is included within the range or `Nothing`
if the range is empty.
## Returns the last element that is included within the range.
It will raise `Index_Out_Of_Bounds` if the range is empty.
last : Integer ! Index_Out_Of_Bounds
last self = if self.is_empty then Error.throw (Index_Out_Of_Bounds.Error 0 0) else
self.start + self.step*(self.length - 1)
@ -77,18 +79,22 @@ type Range
The following range has 100 elements.
0.up_to 100 . length
length : Number
length : Integer
length self = if self.is_empty then 0 else
diff = self.end - self.start
steps = diff . div self.step
if self.start + steps*self.step == self.end then steps else steps+1
exact_fit = (self.start + steps*self.step) == self.end
## The `end` is excluded if it is reached exactly by the last step.
If it is not reached, that means that the last step is also included,
so we increase by one.
if exact_fit then steps else steps+1
## Gets an element from the Range at a specified index (0-based).
Arguments:
- index: The location in the Range to get the element from. The index is
also allowed be negative, then the elements are indexed from the back
of the final item, i.e. -1 will correspond to the last element.
also allowed be negative, then the elements are indexed from the back,
i.e. -1 will correspond to the last element.
> Example
Get the second element of a range.
@ -108,8 +114,8 @@ type Range
Arguments:
- index: The location in the Range to get the element from. The index is
also allowed be negative, then the elements are indexed from the back
of the Range, i.e. -1 will correspond to the last element.
also allowed be negative, then the elements are indexed from the back,
i.e. -1 will correspond to the last element.
- if_missing: The value to return if the index is out of bounds.
get : Integer -> Any -> Any
get self index ~if_missing=Nothing =
@ -149,7 +155,7 @@ type Range
the range.
1.up_to 10 . map (*2)
map : (Number -> Any) -> Vector Any
map : (Integer -> Any) -> Vector Any
map self function =
Vector.new self.length (i -> function (self.start + i*self.step))
@ -166,7 +172,7 @@ type Range
(0.up_to 7).filter (> 3)
(0.up_to 7).filter (Filter_Condition.Greater than=3)
filter : (Filter_Condition | (Any -> Boolean)) -> Vector Any
filter : (Filter_Condition | (Integer -> Boolean)) -> Vector Integer
filter self filter = case filter of
_ : Filter_Condition -> self.filter filter.to_predicate
predicate : Function ->
@ -174,7 +180,9 @@ type Range
if predicate elem then builder.append elem else builder
builder.to_vector
## Applies a function for each element in the range.
## PRIVATE
ADVANCED
Applies a function for each element in the range.
Arguments:
- function: The function to apply to each integer in the range.
@ -182,7 +190,7 @@ type Range
> Example
To print all the numbers from 1 to 10 use:
1.up_to 11 . each IO.println
each : (Number -> Any) -> Nothing
each : (Integer -> Any) -> Nothing
each self function =
if self.step == 0 then throw_zero_step_error else
end_condition = if self.step > 0 then (>=) else (<=)
@ -193,6 +201,7 @@ type Range
go self.start
## PRIVATE
ADVANCED
Applies a function to each element of the range.
Essentially acts like `range.to_vector.each_with_index`, but it is more
@ -206,7 +215,7 @@ type Range
Print range elements with their indices within the range.
(10.up_to 13).each_with_index ix-> elem-> IO.println (Pair ix elem) # Will print Pair 0 10, Pair 1 11, Pair 2 12
each_with_index : (Integer -> Any -> Any) -> Nothing
each_with_index : (Integer -> Integer -> Nothing) -> Nothing
each_with_index self function =
if self.step == 0 then throw_zero_step_error else
end_condition = if self.step > 0 then (>=) else (<=)
@ -220,7 +229,7 @@ type Range
passed function with next elements of the range.
Arguments:
- init: The initial integral value for the fold.
- init: The initial value for the fold.
- function: A binary function taking an item and a number, and returning
an item.
@ -234,7 +243,7 @@ type Range
less than 100.
0.up_to 100 . with_step 2 . fold 0 (+)
fold : Any -> (Any -> Number -> Any) -> Any
fold : Any -> (Any -> Integer -> Any) -> Any
fold self init function =
if self.step == 0 then throw_zero_step_error else
end_condition = if self.step > 0 then (>=) else (<=)
@ -253,9 +262,9 @@ type Range
- function: A function taking two elements and combining them.
> Example
Compute the running sum of all of the elements in a vector
Compute the running sum of all of the elements in a range.
[1, 2, 3].running_fold 0 (+)
(0.up_to 4).running_fold 0 (+)
running_fold : Any -> (Any -> Any -> Any) -> Vector Any
running_fold self init function =
wrapped builder value =
@ -275,7 +284,7 @@ type Range
Checking that all numbers in the range are greater than 5.
10.up_to 100 . all (> 5)
all : (Number -> Boolean) -> Boolean
all : (Integer -> Boolean) -> Boolean
all self predicate = self . any (predicate >> .not) . not
## Checks whether `predicate` is satisfied for any number in this range.
@ -289,7 +298,7 @@ type Range
Checking that at least one number in the range is greater than 10.
1.up_to 100 . any (> 10)
any : (Number -> Boolean) -> Boolean
any : (Integer -> Boolean) -> Boolean
any self predicate = self.find predicate . is_nothing . not
## Gets the first index when `predicate` is satisfied this range.
@ -347,7 +356,7 @@ type Range
0.up_to 100 . index_of 20 == 20
0.up_to 100 . with_step 5 . index_of 20 == 4
0.up_to 100 . with_step 5 . index_of (>10) == 3
index_of : (Integer | (Any -> Boolean)) -> Integer -> Integer | Nothing
index_of : (Integer | (Integer -> Boolean)) -> Integer -> Integer | Nothing
index_of self element start=0 =
check_start_valid start self used_start->
case element of
@ -370,7 +379,7 @@ type Range
Find the last index of an element in a pair.
Pair.new 2 2 . last_index_of 2 == 1
last_index_of : (Any | (Any -> Boolean)) -> Integer -> Integer | Nothing
last_index_of : (Integer | (Integer -> Boolean)) -> Integer -> Integer | Nothing
last_index_of self element start=-1 =
if self.is_empty && (start==-1 || start==0) then Nothing else
check_start_valid start self include_end=False used_start->
@ -403,21 +412,21 @@ type Range
Getting a vector of the numbers 1 to 5.
1.up_to 6 . to_vector
to_vector : Vector
to_vector : Vector Integer
to_vector self = self.map x->x
## Combines all the elements of a non-empty range using a binary operation.
If the range is empty, returns `if_empty`.
Arguments:
- function: A binary operation that takes two items and combines them.
- function: A binary operation that takes two integers and combines them.
- if_empty: Value returned if the range is empty.
> Example
Compute the sum of all the elements in a range.
0.up_to 10 . reduce (+)
reduce : (Any -> Any -> Any) -> Any -> Any
reduce : (Integer -> Integer -> Integer) -> Any -> Any
reduce self function ~if_empty=(Error.throw Empty_Error) =
len = self.length
case len of

View File

@ -3,20 +3,31 @@ import project.Data.Range.Range
import project.Error.Error
import project.Errors.Illegal_Argument.Illegal_Argument
from project.Data.Boolean import Boolean, True, False
## ALIAS Range
Creates an increasing right-exclusive range of integers from `self` to `n`.
Creates an increasing range of integers from `self` to `n`.
Arguments:
- n: 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 containing the numbers 0, 1, 2, 3, 4.
0.up_to 5
Integer.up_to : Integer -> Range
Integer.up_to self n = case n of
_ : Integer -> Range.Between self n
> Example
Create a range containing elements 1, 2, 3.
1.up_to 3 include_end=True
Integer.up_to : Integer -> Boolean -> Range
Integer.up_to self n include_end=False = case n of
_ : Integer ->
effective_end = if include_end then n+1 else n
Range.Between self effective_end 1
_ -> Error.throw (Illegal_Argument.Error "Expected range end to be an Integer.")
## ALIAS Range
@ -25,12 +36,21 @@ Integer.up_to self n = case n of
Arguments:
- n: 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 containing the numbers 5, 4, 3, 2, 1.
5.down_to 0
Integer.down_to : Integer -> Range
Integer.down_to self n = case n of
_ : Integer -> Range.Between self n -1
> Example
Create a range containing elements 3, 2, 1.
3.down_to 1 include_end=True
Integer.down_to : Integer -> Boolean -> Range
Integer.down_to self n include_end=False = case n of
_ : Integer ->
effective_end = if include_end then n-1 else n
Range.Between self effective_end -1
_ -> Error.throw (Illegal_Argument.Error "Expected range end to be an Integer.")

View File

@ -71,7 +71,9 @@ Text.reverse self =
@Tail_Call iterate next iterator.previous
iterate iterator.last iterator.previous
## Applies the provided `function` to each character in `self`.
## PRIVATE
ADVANCED
Applies the provided `function` to each character in `self`.
Arguments:
- function: The operation to apply to each character in the text.

View File

@ -6,6 +6,7 @@ import project.Data.Ordering.Ordering
import project.Data.Ordering.Comparable
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.Day_Of_Week.Day_Of_Week
import project.Data.Time.Day_Of_Week_From
@ -445,6 +446,56 @@ type Date
_ ->
Error.throw (Illegal_Argument.Error "Illegal period argument")
## ALIAS Date Range
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
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")
## Shift the date by the specified amount of business days.
For the purpose of this method, the business days are defined to be

View File

@ -25,12 +25,6 @@ type Date_Period
Day
## PRIVATE
This method could be replaced with matching on `Date_Period` supertype
if/when that is supported.
is_date_period : Boolean
is_date_period self = True
## PRIVATE
adjust_start : (Date | Date_Time) -> (Date | Date_Time)
adjust_start self date =

View File

@ -0,0 +1,504 @@
import project.Any.Any
import project.Data.Filter_Condition.Filter_Condition
import project.Data.Json.JS_Object
import project.Data.Numbers.Integer
import project.Data.Range.Empty_Error
import project.Data.Range.Extensions
import project.Data.Text.Text
import project.Data.Time.Date.Date
import project.Data.Time.Date_Period.Date_Period
import project.Data.Time.Period.Period
import project.Data.Vector.Vector
import project.Error.Error
import project.Errors.Common.Index_Out_Of_Bounds
import project.Errors.Illegal_Argument.Illegal_Argument
import project.Function.Function
import project.Nothing.Nothing
from project.Data.Boolean import Boolean, True, False
polyglot java import org.enso.base.Time_Utils
## Represents a range of dates.
type Date_Range
## PRIVATE
Never use the constructor directly to construct a range, as it does not
allow to verify invariants and may lead to unexpected behavior.
Internal_Constructor (start : Date) (end : Date) (step : Period) (increasing : Boolean) (cached_length : Integer)
## Create a representation of a right-exclusive range of dates.
The range is increasing or decreasing, depending on if the start date is
before or after the end date.
Arguments:
- start: The left boundary of the range. Its value is included.
- end: The right boundary of the range. Its value is excluded.
- step: The step between dates. It must be positive - to construct a
decreasing range, flip the start and the end or use `down_to`, but
keeping the positive step.
new : Date -> Date -> Date_Period|Period -> Date_Range
new start=Date.now end=Date.now step=Date_Period.Day =
increasing = start <= end
Date_Range.new_internal start end increasing step
## PRIVATE
new_internal : Date -> Date -> Boolean -> Date_Period|Period -> Date_Range
new_internal start end increasing step =
one_day = Period.new days=1
base_length = start.days_until end . abs
Date_Range.Internal_Constructor start end one_day increasing base_length . with_step step
## Creates a copy of this range with a changed step.
Arguments:
- new_step: The new step to use. It can either be a `Date_Period` or
`Period`. The provided `Period` must be positive, i.e. all of `years`,
`months` and `days` must be non-negative and at least one of them has
to be positive.
> Example
Create a range representing the first day of every month in a year.
(Date.new 2020 1 1).up_to (Date.new 2020 12 31) . with_step Date_Period.Month
> Example
Create a a decreasing range of every other day between two dates.
(Date.new 2022 10 23).down_to (Date.new 2022 10 1) . with_step (Period.new days=2)
with_step : Date_Period|Period -> Date_Range ! Illegal_Argument
with_step self new_step = case new_step of
_ : Period ->
effective_length = compute_length_and_verify self.start self.end new_step self.increasing
Date_Range.Internal_Constructor self.start self.end new_step self.increasing effective_length
_ : Date_Period ->
self.with_step new_step.to_period
## Convert to a textual representation.
to_text : Text
to_text self =
middle = if self.increasing then " up to " else " down to "
step = if self.step == (Period.new days=1) then "" else
" by " + self.step.to_display_text
"(Date_Range from " + self.start.to_text + middle + self.end.to_text + step + ")"
## Convert to a display representation.
to_display_text : Text
to_display_text self =
start = "[" + self.start.to_display_text + " .. " + self.end.to_display_text
step = if self.step == (Period.new days=1) then "" else
effective_step = if self.increasing then self.step else self.step.negated
" by " + effective_step.to_display_text
start + step + "]"
## Convert to a human-readable representation.
pretty : Text
pretty self = self.to_text
## PRIVATE
Converts this value to a JSON serializable object.
to_js_object : JS_Object
to_js_object self =
JS_Object.from_pairs [["type", "Date_Range"], ["start", self.start.to_js_object], ["end", self.end.to_js_object], ["step", self.step.to_js_object], ["increasing", self.increasing]]
## Returns the first element that is included within the range or `Nothing`
if the range is empty.
first : Integer ! Index_Out_Of_Bounds
first self = if self.is_empty then Error.throw (Index_Out_Of_Bounds.Error 0 0) else self.start
## Returns the second element that is included within the range or `Nothing`
if the range has less than 2 element
second : Integer ! Index_Out_Of_Bounds
second self = if self.length < 2 then Error.throw (Index_Out_Of_Bounds.Error 1 self.length) else self.start + self.step
## Returns the last element that is included within the range or `Nothing`
if the range is empty.
last : Integer ! Index_Out_Of_Bounds
last self = if self.is_empty then Error.throw (Index_Out_Of_Bounds.Error 0 0) else
self.start + self.step*(self.length - 1)
## Get the number of elements in the range.
> Example
The following range has 2 elements.
(Date.new 2023 04 05) . up_to (Date.new 2023 04 07) . length
length : Integer
length self = self.cached_length
## Gets an element from the range at a specified index (0-based).
Arguments:
- index: The location in the range to get the element from. The index is
also allowed be negative, then the elements are indexed from the back,
i.e. -1 will correspond to the last element.
> Example
Get the second element of a range.
(Date.new 2023 04 05) . up_to (Date.new 2023 04 07) . get 1 == (Date.new 2023 04 06)
> Example
Get the last element of a range with step.
(Date.new 2023 04 05) . up_to (Date.new 2023 10 07) . with_step Date_Period.Month . get -1 == (Date.new 2023 10 05)
at : Integer -> Any ! Index_Out_Of_Bounds
at self index =
self.get index (Error.throw (Index_Out_Of_Bounds.Error index self.length))
## Gets an element from the range at a specified index (0-based).
If the index is invalid then `if_missing` is returned.
Arguments:
- index: The location in the range to get the element from. The index is
also allowed be negative, then the elements are indexed from the back,
i.e. -1 will correspond to the last element.
- if_missing: The value to return if the index is out of bounds.
get : Integer -> Any -> Any
get self index ~if_missing=Nothing =
len = self.length
effective_index = if index < 0 then len + index else index
if effective_index >= 0 && effective_index < len then self.internal_at effective_index else
if_missing
## PRIVATE
Generates the i-th element of the range.
This method does no bounds checking, it should be used only internally.
internal_at self i =
nth_element_of_range self.start self.step self.increasing i
## Applies a function to each element in the range, producing a vector of
results.
Arguments:
- function: The function to apply to each date in the range.
> Example
Create a vector that contains the numbers twice that of the numbers in
the range.
1.up_to 10 . map (*2)
map : (Date -> Any) -> Vector Any
map self function =
Vector.new self.length (i -> function (self.internal_at i))
## Converts the range to a vector containing the dates in the range.
> Example
Getting a vector of dates from 2021-05-07 to 2021-05-10 (exclusive).
(Date.new 2021 05 07).up_to (Date.new 2021 05 10) . to_vector
to_vector : Vector Date
to_vector self = self.map x->x
## Checks if this range is empty.
> Example
Checking if the range of days from start of 2020 to start of 2023 is empty.
(Date.new 2020).up_to (Date.new 2023) . is_empty
is_empty : Boolean
is_empty self = self.length == 0
## Checks if this range is not empty.
> Example
Checking if the range of days from start of 2020 to start of 2023 is not empty.
(Date.new 2020).up_to (Date.new 2023) . is_empty
not_empty : Boolean
not_empty self = self.is_empty.not
## Returns a vector of all elements of this range which satisfy a condition.
Arguments:
- filter: The filter to apply to the range. It can either be an instance
of `Filter_Condition` or a predicate taking a value and returning a
boolean value indicating whether the corresponding element should be
kept or not.
> Example
Selecting all elements that are greater than 2020-10-12.
(Date.new 2020 10 01).up_to (Date.new 2020 10 15) . filter (> (Date.new 2020 10 12))
(Date.new 2020 10 01).up_to (Date.new 2020 10 15) . filter (Filter_Condition.Greater than=(Date.new 2020 10 12))
filter : (Filter_Condition | (Date -> Boolean)) -> Vector Date
filter self filter = case filter of
_ : Filter_Condition -> self.filter filter.to_predicate
predicate : Function ->
builder = self.fold Vector.new_builder builder-> elem->
if predicate elem then builder.append elem else builder
builder.to_vector
## PRIVATE
ADVANCED
Applies a function for each element in the range.
Arguments:
- function: The function to apply to each integer in the range.
> Example
To print all dates from 2020-10-01 to 2020-10-05.
(Date.new 2020 10 01).up_to (Date.new 2020 10 05) include_end=True . each IO.println
each : (Date -> Any) -> Nothing
each self function =
(0.up_to self.length).each ix->
function (self.internal_at ix)
## PRIVATE
ADVANCED
Applies a function to each element of the range.
Essentially acts like `range.to_vector.each_with_index`, but it is more
efficient.
Arguments:
- function: A function to apply that takes two parameters: first the
index of a given range element and then the actual range element.
> Example
Print range elements with their indices within the range.
(Date.new 2020 10 01).up_to (Date.new 2020 10 05).each_with_index ix-> elem-> IO.println (Pair ix elem)
each_with_index : (Integer -> Date -> Nothing) -> Nothing
each_with_index self function =
(0.up_to self.length).each_with_index ix->
function ix (self.internal_at ix)
## Combines all the elements of the range, by iteratively applying the
passed function with next elements of the range.
Arguments:
- init: The initial value for the fold.
- function: A binary function taking an item and a date, and returning
an item.
> Example
In the following example, we'll compute how many days in the range are
a Monday.
(Date.new 2020 10 01).up_to (Date.new 2020 10 31) . fold 0 acc-> date->
if date.day_of_week == Day_Of_Week.Monday then acc+1 else acc
fold : Any -> (Any -> Date -> Any) -> Any
fold self init function =
(0.up_to self.length).fold init acc-> ix->
function acc (self.internal_at ix)
## Combines all the elements of the range, by iteratively applying the
passed function with the next element of the range. After each step the
value is stored resulting in a new Vector of the same size as self.
Arguments:
- init: The initial value for the fold.
- function: A function taking two elements and combining them.
running_fold : Any -> (Any -> Date -> Any) -> Vector Any
running_fold self init function =
(0.up_to self.length).running_fold init acc-> ix->
function acc (self.internal_at ix)
## Checks whether `predicate` is satisfied for all dates in this range.
Arguments:
- predicate: A function that takes a list element and returns a boolean
value that says whether that value satisfies the conditions of the
function.
> Example
Checking that all dates in the range are after 2020-10-01.
(Date.new 2020 10 01).up_to (Date.new 2020 10 31) . all (> (Date.new 2020 10 01))
all : (Date -> Boolean) -> Boolean
all self predicate = self . any (predicate >> .not) . not
## Checks whether `predicate` is satisfied for any date in this range.
Arguments:
- predicate: A function that takes a list element and returns a boolean
value that says whether that value satisfies the conditions of the
function.
> Example
Checking that at least one date in the range is after 2020-10-01.
(Date.new 2020 10 01).up_to (Date.new 2020 10 31) . any (> (Date.new 2020 10 01))
any : (Date -> Boolean) -> Boolean
any self predicate = self.find predicate . is_nothing . not
## Gets the first index when `predicate` is satisfied this range.
If no index satisfies the predicate, returns `if_missing`.
Arguments:
- predicate: A function that takes a list element and returns a boolean
value that says whether that value satisfies the conditions of the
function.
- start: The index to start searching from. If the index is negative, it
is counted from the end of the range.
- if_missing: Value returned if no element satisfies the predicate.
> Example
Get the first date in the range that is a Monday.
(Date.new 2020 10 01).up_to (Date.new 2020 10 31) . find (d-> d.day_of_week == Day_Of_Week.Monday)
find : (Date -> Boolean) -> Integer -> Any -> Any
find self predicate start=0 ~if_missing=Nothing =
index = self.index_of predicate start
case index of
Nothing -> if_missing
_ : Integer -> self.internal_at index
## Checks if the range contains the specified value.
> Example
Check if a particular date is in the range.
(Date.new 2020 10 01).up_to (Date.new 2020 10 31) . with_step (Period.new days=2) . contains (Date.new 2020 10 15)
contains : Date -> Boolean
contains self value = self.find (== value) . is_nothing . not
## Returns the index of an element in the range.
Returns Nothing if the element is not found.
Arguments:
- element: The date to search for or a predicate function to test for
each element.
- start: The index to start searching from. If the index is negative, it
is counted from the end of the range.
> Example
Find the index of a first day that is a Monday.
(Date.new 2020 10 01).up_to (Date.new 2020 10 31) . index_of (d-> d.day_of_week == Day_Of_Week.Monday)
index_of : (Date | (Date -> Boolean)) -> Integer -> Integer | Nothing
index_of self element start=0 =
predicate = case element of
d : Date ->
ix-> self.internal_at ix == d
f : Function ->
ix-> f (self.internal_at ix)
(0.up_to self.length).index_of predicate start
## Returns the last index of an element in the range.
Returns Nothing if the element is not found.
Arguments:
- element: The date to search for or a predicate function to test for
each element.
- start: The index to start searching backwards from. If the index is
negative, it is counted from the end of the range.
> Example
Find the index of a first day that is a Monday.
(Date.new 2020 10 01).up_to (Date.new 2020 10 31) . last_index_of (d-> d.day_of_week == Day_Of_Week.Monday)
last_index_of : (Date | (Date -> Boolean)) -> Integer -> Integer | Nothing
last_index_of self element start=-1 =
predicate = case element of
d : Date ->
ix-> self.internal_at ix == d
f : Function ->
ix-> f (self.internal_at ix)
(0.up_to self.length).last_index_of predicate start
## Reverses the range, returning a vector with the same elements as the
original range, but in the opposite order.
> Example
Reverse a range of dates.
(Date.new 2020 10 01).up_to (Date.new 2020 10 31) . reverse
? Returning a `Vector`
This method cannot return back a `Date_Range`, as some ranges are not
reversible. For example, the range `(Date.new 2020 02 29).up_to (Date.new 2023) . with_step Date_Period.Year`
will have `2022-02-28` as its last entry. But if we create a
range starting at `2022-02-28` and going backwards by a year, its last
element will be `2020-02-28` and not `2020-02-29` as in the original.
Thus, to preserve the contents we need to return a vector.
reverse : Vector Date
reverse self = self.to_vector.reverse
## Combines all the elements of a non-empty range using a binary operation.
If the range is empty, returns `if_empty`.
Arguments:
- function: A binary operation that takes two dates and combines them
into a new date.
- if_empty: Value returned if the range is empty.
reduce : (Date -> Date -> Date) -> Any -> Any
reduce self function ~if_empty=(Error.throw Empty_Error) =
case self.length of
0 -> if_empty
1 -> self.start
length ->
(1.up_to length).fold self.start acc-> ix->
function acc (self.internal_at ix)
## PRIVATE
Computes the length of the range and verifies its invariants.
If any of the invariants are violated, a dataflow error is raised.
compute_length_and_verify : Date -> Date -> Period -> Boolean -> Integer ! Illegal_Argument
compute_length_and_verify start end step increasing =
if is_period_positive step . not then Error.throw (Illegal_Argument.Error "The step `Period` for `Date_Range` must be positive, i.e. all of `years`, `months` and `days` must be non-negative and at least one of them must be strictly positive.") else
is_range_empty = case increasing of
True -> start >= end
False -> start <= end
if is_range_empty then 0 else
# First a few heuristics for a fast path.
# If there are no years or months, we can perform a simple computation on day difference.
if step.total_months == 0 then compute_length_step_days start end step.days increasing else
# Similarly, if we are only shifting by months, we can rely on a simpler computation.
if step.days == 0 then compute_length_step_months start end step.total_months increasing else
# Then we go brute force for the general case.
compute_length_step_brute_force start end step increasing
## PRIVATE
is_period_positive period =
if (period.years < 0) || (period.months < 0) || (period.days < 0) then False else
(period.total_months > 0) || (period.days > 0)
## PRIVATE
Assumes that the range is not empty.
compute_length_step_days : Date -> Date -> Integer -> Boolean -> Integer
compute_length_step_days start end step increasing =
# Logic analogous to `Range.length`.
diff = case increasing of
True -> Time_Utils.days_between start end
False -> Time_Utils.days_between end start
# assert (diff >= 0)
steps = diff . div step
exact_fit = diff % step == 0
if exact_fit then steps else steps+1
## PRIVATE
Assumes that the range is not empty.
compute_length_step_months start end step increasing =
diff = case increasing of
True -> Time_Utils.months_between start end
False -> Time_Utils.months_between end start
# assert (diff >= 0)
steps = diff . div step
exact_fit = case increasing of
True -> start + Period.new months=steps*step == end
False -> start - Period.new months=steps*step == end
if exact_fit then steps else steps+1
## PRIVATE
nth_element_of_range start step increasing n = case increasing of
True -> start + step*n
False -> start - step*n
## PRIVATE
compute_length_step_brute_force start end step increasing =
is_exceeded = case increasing of
True -> (x -> x >= end)
False -> (x -> x <= end)
go current_date acc_length =
if is_exceeded current_date then acc_length else
next_date = nth_element_of_range start step increasing (acc_length + 1)
@Tail_Call go next_date (acc_length + 1)
go start 0

View File

@ -457,18 +457,18 @@ type Date_Time
start_of : (Date_Period|Time_Period) -> Date_Time
start_of self period=Date_Period.Month =
adjusted = period.adjust_start self
case period.is_date_period of
True -> Time_Period.Day.adjust_start adjusted
False -> adjusted
case period of
_ : Date_Period -> Time_Period.Day.adjust_start adjusted
_ : Time_Period -> adjusted
## Returns the last date within the `Time_Period` or `Date_Period`
containing self.
end_of : (Date_Period|Time_Period) -> Date_Time
end_of self period=Date_Period.Month =
adjusted = period.adjust_end self
case period.is_date_period of
True -> Time_Period.Day.adjust_end adjusted
False -> adjusted
case period of
_ : Date_Period -> Time_Period.Day.adjust_end adjusted
_ : Time_Period -> adjusted
## ALIAS Time to Date

View File

@ -78,7 +78,7 @@ type Period
Arguments:
- internal_period: An internal representation of period of type
java.time.Period.
Value internal_period
Value (internal_period : Java_Period)
## Get the portion of the period expressed in years.
years : Integer
@ -88,6 +88,11 @@ type Period
months : Integer
months self = self.internal_period.getMonths
## Get the portion of the period coming from months and years as months
(every year is translated to 12 months).
total_months : Integer
total_months self = self.internal_period.toTotalMonths
## Get the portion of the period expressed in days.
days : Integer
days self = self.internal_period.getDays
@ -107,10 +112,8 @@ type Period
+ : Period -> Period ! (Time_Error | Illegal_Argument)
+ self other_period =
ensure_period other_period <|
Panic.catch Any (Period.Value (self.internal_period.plus other_period.internal_period)) err->
case err of
_ : DateTimeException -> Error.throw Time_Error.Error "Period addition failed:"+err.getMessage
_ : ArithmeticException -> Error.throw Illegal_Argument.Error "Arithmetic error:"+err.getMessage cause=err
catch_java_exceptions "Period.+" <|
Period.Value (self.internal_period.plus other_period.internal_period)
## Subtract a specified amount of time from this period.
@ -128,10 +131,31 @@ type Period
- : Period -> Period ! (Time_Error | Illegal_Argument)
- self other_period =
ensure_period other_period <|
Panic.catch Any (Period.Value (self.internal_period.minus other_period.internal_period)) err->
case err of
DateTimeException -> Error.throw Time_Error.Error "Period subtraction failed"
ArithmeticException -> Error.throw Illegal_Argument.Error "Arithmetic error"
catch_java_exceptions "Period.-" <|
Period.Value (self.internal_period.minus other_period.internal_period)
## Multiply the amount of time in this period by the specified scalar.
Arguments:
- factor: The scalar to multiply by.
> Example
Multiply a period of 1 year and 2 months by 2
import Standard.Base.Data.Time.Period
example_multiply = (Period.new years=1 months=2) * 2
* : Integer -> Period ! Time_Error
* self factor =
catch_java_exceptions "Period.*" <|
Period.Value (self.internal_period.multipliedBy factor)
## Negate all amounts in the period.
This is useful when a period used for going forward in time needs to be
used for going backwards instead.
negated : Period
negated self = Period.Value (self.internal_period.negated)
## PRIVATE
Convert Period to a friendly string.
@ -163,3 +187,13 @@ type Period
if self.months==0 . not then b.append ["months", self.months]
if self.days==0 . not then b.append ["days", self.days]
JS_Object.from_pairs b.to_vector
## PRIVATE
catch_java_exceptions operation ~action =
handle_arithmetic_exception caught_panic =
Error.throw (Time_Error.Error "An overflow has occurred during the "+operation+" operation:"+caught_panic.payload.getMessage)
handle_date_time_exception caught_panic =
Error.throw (Time_Error.Error "The operation "+operation+" has failed:"+caught_panic.payload.getMessage)
Panic.catch ArithmeticException handler=handle_arithmetic_exception <|
Panic.catch DateTimeException handler=handle_date_time_exception <|
action

View File

@ -16,12 +16,6 @@ type Time_Period
Second
## PRIVATE
This method could be replaced with matching on `Date_Period` supertype
if/when that is supported.
is_date_period : Boolean
is_date_period self = False
## PRIVATE
to_java_unit : TemporalUnit
to_java_unit self = case self of

View File

@ -566,7 +566,9 @@ type Vector a
map_with_index : (Integer -> Any -> Any) -> Vector Any
map_with_index self function = Vector.new self.length i-> function i (self.at i)
## Applies a function to each element of the vector.
## PRIVATE
ADVANCED
Applies a function to each element of the vector.
Unlike `map`, this method does not return the individual results,
therefore it is only useful for side-effecting computations.
@ -583,7 +585,9 @@ type Vector a
0.up_to self.length . each ix->
f (self.at ix)
## Applies a function to each element of the vector.
## PRIVATE
ADVANCED
Applies a function to each element of the vector.
Arguments:
- function: A function to apply that takes an index and an item.

View File

@ -100,6 +100,7 @@ import project.Data.Text.Text_Ordering.Text_Ordering
import project.Data.Text.Text_Sub_Range.Text_Sub_Range
import project.Data.Time.Date.Date
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.Day_Of_Week.Day_Of_Week
import project.Data.Time.Day_Of_Week_From
@ -149,6 +150,7 @@ export project.Data.Text.Text_Ordering.Text_Ordering
export project.Data.Text.Text_Sub_Range.Text_Sub_Range
export project.Data.Time.Date.Date
export project.Data.Time.Date_Period.Date_Period
export project.Data.Time.Date_Range.Date_Range
export project.Data.Time.Date_Time.Date_Time
export project.Data.Time.Day_Of_Week.Day_Of_Week
export project.Data.Time.Day_Of_Week_From

View File

@ -204,10 +204,15 @@ public class Time_Utils {
/**
* Counts days within the range from start (inclusive) to end (exclusive).
*
* <p>If start is before end, it will return 0.
*/
public static long days_between(LocalDate start, LocalDate end) {
return ChronoUnit.DAYS.between(start, end);
}
/**
* Counts months within the range from start (inclusive) to end (exclusive).
*/
public static long months_between(LocalDate start, LocalDate end) {
return ChronoUnit.MONTHS.between(start, end);
}
}

View File

@ -29,6 +29,13 @@ spec = Test.group "Range" <|
range_3.end . should_equal 0
range_3.step . should_equal -1
Test.specify "should allow to include the end" <|
1.up_to 3 include_end=True . to_vector . should_equal [1, 2, 3]
3.down_to 1 include_end=True . to_vector . should_equal [3, 2, 1]
1.up_to 1 include_end=True . to_vector . should_equal [1]
1.down_to 1 include_end=True . to_vector . should_equal [1]
Test.specify "should allow creation with Range.new" <|
Range.new . should_equal (Range.Between 0 100 1)
Range.new 5 20 . should_equal (Range.Between 5 20 1)

View File

@ -0,0 +1,176 @@
from Standard.Base import all
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
from Standard.Test import Problems, Test, Test_Suite
import Standard.Test.Extensions
main = Test_Suite.run_main spec
spec =
Test.group "Date_Range" <|
Test.specify "should be created with up_to and down_to extension methods" <|
(Date.new 2020 02 28).up_to (Date.new 2020 03 02) . to_vector . should_equal [Date.new 2020 02 28, Date.new 2020 02 29, Date.new 2020 03 01]
(Date.new 2020 02 28).up_to (Date.new 2020 03 02) include_end=True . to_vector . should_equal [Date.new 2020 02 28, Date.new 2020 02 29, Date.new 2020 03 01, Date.new 2020 03 02]
(Date.new 2021 03 01).down_to (Date.new 2021 02 28) . to_vector . should_equal [Date.new 2021 03 01]
(Date.new 2021 03 01).down_to (Date.new 2021 02 28) include_end=True . to_vector . should_equal [Date.new 2021 03 01, Date.new 2021 02 28]
(Date.new 2023 12 31).up_to (Date.new 2023 12 31) . to_vector . should_equal []
(Date.new 2023 12 31).up_to (Date.new 2023 12 31) include_end=True . to_vector . should_equal [Date.new 2023 12 31]
(Date.new 2023 12 31).down_to (Date.new 2023 12 31) . to_vector . should_equal []
(Date.new 2023 12 31).down_to (Date.new 2023 12 31) include_end=True . to_vector . should_equal [Date.new 2023 12 31]
(Date.new 2023 12 31).down_to (Date.new 2023 12 31) . with_step Date_Period.Month . to_vector . should_equal []
Test.specify ".new should infer if the range should be increasing or not" <|
Date_Range.new (Date.new 2023 10 01) (Date.new 2023 10 04) . to_vector . should_equal [Date.new 2023 10 01, Date.new 2023 10 02, Date.new 2023 10 03]
Date_Range.new (Date.new 2023 10 04) (Date.new 2023 10 01) . to_vector . should_equal [Date.new 2023 10 04, Date.new 2023 10 03, Date.new 2023 10 02]
Test.specify "will be empty if the start and end are swapped with up_to or down_to" <|
(Date.new 2023 10 01).down_to (Date.new 2023 10 04) . to_vector . should_equal []
(Date.new 2023 10 04).up_to (Date.new 2023 10 01) . to_vector . should_equal []
(Date.new 2023 10 01).down_to (Date.new 2023 10 04) . with_step Date_Period.Month . to_vector . should_equal []
(Date.new 2023 10 04).up_to (Date.new 2023 10 01) . with_step Date_Period.Month . to_vector . should_equal []
Test.specify "should allow setting a custom step" <|
(Date.new 2020 01 10).up_to (Date.new 2020 01 31) . with_step (Period.new days=5) . to_vector . should_equal [Date.new 2020 01 10, Date.new 2020 01 15, Date.new 2020 01 20, Date.new 2020 01 25, Date.new 2020 01 30]
(Date.new 2020 01 10).up_to (Date.new 2020 01 30) . with_step (Period.new days=5) . to_vector . should_equal [Date.new 2020 01 10, Date.new 2020 01 15, Date.new 2020 01 20, Date.new 2020 01 25]
(Date.new 2020 01 10).up_to (Date.new 2020 01 30) include_end=True . with_step (Period.new days=5) . to_vector . should_equal [Date.new 2020 01 10, Date.new 2020 01 15, Date.new 2020 01 20, Date.new 2020 01 25, Date.new 2020 01 30]
(Date.new 2020 01 10).down_to (Date.new 2020 01 01) . with_step Date_Period.Week . to_vector . should_equal [Date.new 2020 01 10, Date.new 2020 01 03]
(Date.new 2020 01 01).up_to (Date.new 2020 12 31) . with_step Date_Period.Month . to_vector . should_equal [Date.new 2020 01 01, Date.new 2020 02 01, Date.new 2020 03 01, Date.new 2020 04 01, Date.new 2020 05 01, Date.new 2020 06 01, Date.new 2020 07 01, Date.new 2020 08 01, Date.new 2020 09 01, Date.new 2020 10 01, Date.new 2020 11 01, Date.new 2020 12 01]
(Date.new 2020 01 01).up_to (Date.new 2026) . with_step (Period.new years=2) . to_vector . should_equal [Date.new 2020 01 01, Date.new 2022 01 01, Date.new 2024 01 01]
(Date.new 2020 01 01).up_to (Date.new 2026) include_end=True . with_step (Period.new years=2) . to_vector . should_equal [Date.new 2020 01 01, Date.new 2022 01 01, Date.new 2024 01 01, Date.new 2026 01 01]
(Date.new 2060 11 25).down_to (Date.new 2020 11 24) . with_step (Period.new years=20) . to_vector . should_equal [Date.new 2060 11 25, Date.new 2040 11 25, Date.new 2020 11 25]
(Date.new 2020).up_to (Date.new 2023) . with_step (Period.new years=1 months=2 days=3) . to_vector . should_equal [Date.new 2020 01 01, Date.new 2021 03 04, Date.new 2022 05 07]
Test.specify "should handle end of month edge cases" <|
(Date.new 2020 01 31).up_to (Date.new 2020 12 31) include_end=True . with_step Date_Period.Month . to_vector . should_equal [Date.new 2020 01 31, Date.new 2020 02 29, Date.new 2020 03 31, Date.new 2020 04 30, Date.new 2020 05 31, Date.new 2020 06 30, Date.new 2020 07 31, Date.new 2020 08 31, Date.new 2020 09 30, Date.new 2020 10 31, Date.new 2020 11 30, Date.new 2020 12 31]
(Date.new 2021 01 28).up_to (Date.new 2021 05 10) . with_step Date_Period.Month . to_vector . should_equal [Date.new 2021 01 28, Date.new 2021 02 28, Date.new 2021 03 28, Date.new 2021 04 28]
(Date.new 2023 01 30).up_to (Date.new 2023 06 10) . with_step Date_Period.Month . to_vector . should_equal [Date.new 2023 01 30, Date.new 2023 02 28, Date.new 2023 03 30, Date.new 2023 04 30, Date.new 2023 05 30]
(Date.new 2023 01 30).up_to (Date.new 2023 06 10) . with_step (Period.new months=2) . to_vector . should_equal [Date.new 2023 01 30, Date.new 2023 03 30, Date.new 2023 05 30]
(Date.new 2020 02 29).up_to (Date.new 2023) . with_step Date_Period.Year . to_vector . should_equal [Date.new 2020 02 29, Date.new 2021 02 28, Date.new 2022 02 28]
Test.specify "should handle edge cases" <|
(Date.new 2020 02 27).up_to (Date.new 2020 03 02) include_end=True . with_step (Period.new days=2) . to_vector . should_equal [Date.new 2020 02 27, Date.new 2020 02 29, Date.new 2020 03 02]
(Date.new 2020 02 27).up_to (Date.new 2020 02 28) . with_step Date_Period.Month . to_vector . should_equal [Date.new 2020 02 27]
(Date.new 2020 02 27).up_to (Date.new 2020 04 27) . with_step Date_Period.Month . to_vector . should_equal [Date.new 2020 02 27, Date.new 2020 03 27]
(Date.new 2020 02 27).up_to (Date.new 2020 04 27) include_end=True . with_step Date_Period.Month . to_vector . should_equal [Date.new 2020 02 27, Date.new 2020 03 27, Date.new 2020 04 27]
(Date.new 2020 02 27).up_to (Date.new 2020 04 01) . with_step Date_Period.Month . to_vector . should_equal [Date.new 2020 02 27, Date.new 2020 03 27]
(Date.new 2021 02 01).up_to (Date.new 2021 03 01) include_end=True . with_step Date_Period.Month . to_vector . should_equal [Date.new 2021 02 01, Date.new 2021 03 01]
(Date.new 2020 01 31).up_to (Date.new 2020 04 30) . with_step Date_Period.Month . to_vector . should_equal [Date.new 2020 01 31, Date.new 2020 02 29, Date.new 2020 03 31]
(Date.new 2020 01 31).up_to (Date.new 2020 04 30) include_end=True . with_step Date_Period.Month . to_vector . should_equal [Date.new 2020 01 31, Date.new 2020 02 29, Date.new 2020 03 31, Date.new 2020 04 30]
(Date.new 2020 01 31).up_to (Date.new 2020 04 01) include_end=True . with_step Date_Period.Month . to_vector . should_equal [Date.new 2020 01 31, Date.new 2020 02 29, Date.new 2020 03 31]
v = (Date.new 2020 01 01).up_to (Date.new 2020 12 31) include_end=True . with_step Date_Period.Month . to_vector
v.length . should_equal 12
v.first . should_equal (Date.new 2020 01 01)
v.last . should_equal (Date.new 2020 12 01)
(Date.new 2020 01 01).up_to (Date.new 2020 12 31) include_end=True . with_step (Period.new months=3) . to_vector . should_equal [Date.new 2020 01 01, Date.new 2020 04 01, Date.new 2020 07 01, Date.new 2020 10 01]
(Date.new 2020 01 01).up_to (Date.new 2021 01 01) include_end=True . with_step (Period.new months=3) . to_vector . should_equal [Date.new 2020 01 01, Date.new 2020 04 01, Date.new 2020 07 01, Date.new 2020 10 01, Date.new 2021 01 01]
(Date.new 2020 01 01).up_to (Date.new 2021 01 01) include_end=False . with_step (Period.new months=3) . to_vector . should_equal [Date.new 2020 01 01, Date.new 2020 04 01, Date.new 2020 07 01, Date.new 2020 10 01]
(Date.new 2020 01 31).up_to (Date.new 2020 05 01) . with_step (Period.new months=2) . to_vector . should_equal [Date.new 2020 01 31, Date.new 2020 03 31]
(Date.new 2020 01 31).up_to (Date.new 2020 03 31) include_end=True . with_step (Period.new months=2) . to_vector . should_equal [Date.new 2020 01 31, Date.new 2020 03 31]
(Date.new 2020 01 31).up_to (Date.new 2020 03 31) . with_step (Period.new months=2) . to_vector . should_equal [Date.new 2020 01 31]
(Date.new 2020 01 31).up_to (Date.new 2020 04 02) . with_step (Period.new months=2) . to_vector . should_equal [Date.new 2020 01 31, Date.new 2020 03 31]
(Date.new 2020 12 31).up_to (Date.new 2021 01 01) . with_step (Period.new years=1) . to_vector . should_equal [Date.new 2020 12 31]
(Date.new 2020 12 31).up_to (Date.new 2021 01 01) . with_step (Period.new years=10) . to_vector . should_equal [Date.new 2020 12 31]
(Date.new 2020 12 31).up_to (Date.new 2023 01 01) . with_step (Period.new years=1) . to_vector . should_equal [Date.new 2020 12 31, Date.new 2021 12 31, Date.new 2022 12 31]
(Date.new 2020 12 31).up_to (Date.new 2023 01 01) . with_step (Period.new years=2) . to_vector . should_equal [Date.new 2020 12 31, Date.new 2022 12 31]
(Date.new 2020 12 31).up_to (Date.new 2023 01 01) . with_step (Period.new years=10) . to_vector . should_equal [Date.new 2020 12 31]
(Date.new 2021 01 01).up_to (Date.new 2023 12 31) . with_step (Period.new years=1) . to_vector . should_equal [Date.new 2021 01 01, Date.new 2022 01 01, Date.new 2023 01 01]
(Date.new 2021 01 01).up_to (Date.new 2023 12 31) . with_step (Period.new years=2) . to_vector . should_equal [Date.new 2021 01 01, Date.new 2023 01 01]
(Date.new 2021 01 01).up_to (Date.new 2023 12 31) include_end=True . with_step (Period.new years=2) . to_vector . should_equal [Date.new 2021 01 01, Date.new 2023 01 01]
Test.specify "should not allow a non-positive step" <|
(Date.new 2010).up_to (Date.new 2050) . with_step (Period.new years=0 months=0 days=0) . should_fail_with Illegal_Argument
(Date.new 2010).up_to (Date.new 2050) . with_step (Period.new years=0 months=-1 days=0) . should_fail_with Illegal_Argument
(Date.new 2010).up_to (Date.new 2050) . with_step (Period.new years=0 months=0 days=-1) . should_fail_with Illegal_Argument
(Date.new 2010).up_to (Date.new 2050) . with_step (Period.new years=-1 months=0 days=0) . should_fail_with Illegal_Argument
(Date.new 2010).up_to (Date.new 2050) . with_step (Period.new years=2 months=-1 days=0) . should_fail_with Illegal_Argument
(Date.new 2010).up_to (Date.new 2050) . with_step (Period.new years=-1 months=40 days=0) . should_fail_with Illegal_Argument
(Date.new 2010).up_to (Date.new 2050) . with_step (Period.new years=1 months=40 days=-20) . should_fail_with Illegal_Argument
(Date.new 2010).up_to (Date.new 2050) . with_step (Period.new years=-1 months=-2 days=2) . should_fail_with Illegal_Argument
(Date.new 2010).up_to (Date.new 2050) . with_step (Period.new years=-1 months=-2 days=-1) . should_fail_with Illegal_Argument
# e.g. 2021-06-05 + 1 month - 30 days == 2021-06-05 --> no progression
(Date.new 2021 06 05).up_to (Date.new 2021 06 08) . with_step (Period.new months=1 days=(-30)) . should_fail_with Illegal_Argument
(Date.new 2021 05 05).up_to (Date.new 2021 06 08) . with_step (Period.new months=1 days=(-30)) . should_fail_with Illegal_Argument
(Date.new 2021 02 28).up_to (Date.new 2021 03 31) . with_step ((Period.new years=1 months=(-11) days=(-28))) . should_fail_with Illegal_Argument
Test.specify "should allow to reverse a range, returning a vector" <|
(Date.new 2020 01 02).up_to (Date.new 2020 01 02) . reverse . should_equal []
(Date.new 2020 01 02).up_to (Date.new 2020 01 02) include_end=True . reverse . should_equal [Date.new 2020 01 02]
(Date.new 2020 01 03).down_to (Date.new 2020 01 01) . reverse . should_equal [Date.new 2020 01 02, Date.new 2020 01 03]
(Date.new 2020 02 29).up_to (Date.new 2023) . with_step Date_Period.Year . reverse . should_equal [Date.new 2022 02 28, Date.new 2021 02 28, Date.new 2020 02 29]
Test.specify "should be consistent with its to_vector representation" <|
r1 = (Date.new 2020 02 28).up_to (Date.new 2020 03 02)
r2 = (Date.new 2020 02 28).up_to (Date.new 2020 03 02) include_end=True
r3 = (Date.new 2021 03 01).down_to (Date.new 2021 02 28)
r4 = (Date.new 2021 03 01).down_to (Date.new 2021 02 28) include_end=True
r5 = (Date.new 2023 12 31).up_to (Date.new 2023 12 31)
r6 = (Date.new 2023 12 31).up_to (Date.new 2023 12 31) include_end=True
r7 = (Date.new 2023 12 31).down_to (Date.new 2023 12 31)
r8 = (Date.new 2023 12 31).down_to (Date.new 2023 12 31) include_end=True
r9 = (Date.new 2020 01 10).down_to (Date.new 2020 01 01) . with_step Date_Period.Week
r10 = (Date.new 2020 01 01).up_to (Date.new 2020 12 31) . with_step Date_Period.Month
r11 = (Date.new 2020 01 01).up_to (Date.new 2026) . with_step (Period.new years=2)
r12 = (Date.new 2020 01 01).up_to (Date.new 2026) include_end=True . with_step (Period.new years=2)
r13 = (Date.new 2060 11 25).down_to (Date.new 2020 11 24) . with_step (Period.new years=20)
r14 = (Date.new 2020 01 31).up_to (Date.new 2020 12 31) include_end=True . with_step Date_Period.Month
r15 = (Date.new 2020 02 29).up_to (Date.new 2023) . with_step Date_Period.Year
ranges = [r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15]
ranges.each r-> Test.with_clue r.to_text+": " <|
r.length . should_equal r.to_vector.length
r.is_empty . should_equal r.to_vector.is_empty
r.not_empty . should_equal r.to_vector.not_empty
r.map .day_of_week . should_equal (r.to_vector.map .day_of_week)
p = d-> d.day_of_week == Day_Of_Week.Monday
r.filter p . should_equal (r.to_vector.filter p)
r.all p . should_equal (r.to_vector.all p)
r.any p . should_equal (r.to_vector.any p)
r.find p . should_equal (r.to_vector.find p)
r.index_of p . should_equal (r.to_vector.index_of p)
r.last_index_of p . should_equal (r.to_vector.last_index_of p)
count_mondays acc date =
if date.day_of_week == Day_Of_Week.Monday then acc+1 else acc
r.fold 0 count_mondays . should_equal (r.to_vector.fold 0 count_mondays)
r.running_fold 0 count_mondays . should_equal (r.to_vector.running_fold 0 count_mondays)
reducer x y = if x > y then x else y
# Catch+to_text to fix Empty_Error equality.
r.reduce reducer . catch . to_text . should_equal (r.to_vector.reduce reducer . catch . to_text)
Test.specify "should define friendly text representations" <|
r1 = (Date.new 2020 02 28).up_to (Date.new 2020 03 02)
r2 = (Date.new 2020 03 20).down_to (Date.new 2020 03 01) include_end=True . with_step Date_Period.Week
r1.to_text . should_equal '(Date_Range from 2020-02-28 up to 2020-03-02)'
r2.to_text . should_equal '(Date_Range from 2020-03-20 down to 2020-02-29 by 7D)'
r1.pretty . should_equal r1.to_text
r2.pretty . should_equal r2.to_text
r1.to_display_text . should_equal '[2020-02-28 .. 2020-03-02]'
r2.to_display_text . should_equal '[2020-03-20 .. 2020-02-29 by -7D]'
Test.specify "should be serializable to JSON" <|
r = (Date.new 2020 01 01).up_to (Date.new 2020 01 03)
r.to_json . should_equal '{"type":"Date_Range","start":{"type":"Date","constructor":"new","day":1,"month":1,"year":2020},"end":{"type":"Date","constructor":"new","day":3,"month":1,"year":2020},"step":{"type":"Period","constructor":"new","days":1},"increasing":true}'

View File

@ -7,12 +7,14 @@ 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_Range_Spec
import project.Data.Time.Date_Time_Spec
import project.Data.Time.Time_Zone_Spec
import project.Data.Time.Day_Of_Week_Spec
spec =
Date_Spec.spec
Date_Range_Spec.spec
Duration_Spec.spec
Period_Spec.spec
Time_Of_Day_Spec.spec