diff --git a/CHANGELOG.md b/CHANGELOG.md index db676cc450c..9a8bdabe0b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Array.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Array.enso index e1933462f28..3042950e92b 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Array.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Array.enso @@ -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. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/List.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/List.enso index 0702fd08cc3..0cfda7c1aca 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/List.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/List.enso @@ -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. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Map.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Map.enso index 10767ffea67..b75143d0d5d 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Map.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Map.enso @@ -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, diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Pair.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Pair.enso index 0689b53f58c..bb0bc630d22 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Pair.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Pair.enso @@ -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. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Range.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Range.enso index 23a9738bff7..df9b105f93b 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Range.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Range.enso @@ -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 diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Range/Extensions.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Range/Extensions.enso index 0996f5cf57f..4aa1d37887f 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Range/Extensions.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Range/Extensions.enso @@ -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.") diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso index fc2c1c6d3b5..39a4794cd60 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso @@ -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. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date.enso index a1f2d1d7c44..ee7212ba054 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date.enso @@ -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 diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Period.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Period.enso index 4eab0356cb2..80e639f280a 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Period.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Period.enso @@ -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 = diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Range.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Range.enso new file mode 100644 index 00000000000..c548855796d --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Range.enso @@ -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 diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time.enso index 3a85f9d0984..e2bd0ad1eb7 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time.enso @@ -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 diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Period.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Period.enso index cc2fbab1670..6e2c81dc4f9 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Period.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Period.enso @@ -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 diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Time_Period.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Time_Period.enso index 84c752811ee..2c9580e5963 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Time_Period.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Time_Period.enso @@ -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 diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Vector.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Vector.enso index 656270126a5..660a4fc22ca 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Vector.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Vector.enso @@ -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. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso index d8d4cfc58bb..dd0822e2858 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso @@ -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 diff --git a/std-bits/base/src/main/java/org/enso/base/Time_Utils.java b/std-bits/base/src/main/java/org/enso/base/Time_Utils.java index 5a80e3bc76f..44ae223ce62 100644 --- a/std-bits/base/src/main/java/org/enso/base/Time_Utils.java +++ b/std-bits/base/src/main/java/org/enso/base/Time_Utils.java @@ -204,10 +204,15 @@ public class Time_Utils { /** * Counts days within the range from start (inclusive) to end (exclusive). - * - *

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); + } } diff --git a/test/Tests/src/Data/Range_Spec.enso b/test/Tests/src/Data/Range_Spec.enso index 582b2585087..64766a5052f 100644 --- a/test/Tests/src/Data/Range_Spec.enso +++ b/test/Tests/src/Data/Range_Spec.enso @@ -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) diff --git a/test/Tests/src/Data/Time/Date_Range_Spec.enso b/test/Tests/src/Data/Time/Date_Range_Spec.enso new file mode 100644 index 00000000000..20fe0933ef2 --- /dev/null +++ b/test/Tests/src/Data/Time/Date_Range_Spec.enso @@ -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}' diff --git a/test/Tests/src/Data/Time/Spec.enso b/test/Tests/src/Data/Time/Spec.enso index 9d19faa68d2..1322d2dd627 100644 --- a/test/Tests/src/Data/Time/Spec.enso +++ b/test/Tests/src/Data/Time/Spec.enso @@ -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