diff --git a/CHANGELOG.md b/CHANGELOG.md index 894d489f1034..8eff8c138e3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -193,6 +193,8 @@ - [Implemented specialized storage for the in-memory Table.][3673] - [Implemented `Table.distinct` for the in-memory backend.][3684] - [Added `databases`, `schemas`, `tables` support to database Connection.][3632] +- [Implemented `start_of` and `end_of` methods for date/time types allowing to + find start and end of a period of time containing the provided time.][3695] [debug-shortcuts]: https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug @@ -309,6 +311,7 @@ [3647]: https://github.com/enso-org/enso/pull/3647 [3673]: https://github.com/enso-org/enso/pull/3673 [3684]: https://github.com/enso-org/enso/pull/3684 +[3695]: https://github.com/enso-org/enso/pull/3695 #### Enso Compiler diff --git a/build.sbt b/build.sbt index f5a89af8e4d4..50023c41be27 100644 --- a/build.sbt +++ b/build.sbt @@ -5,8 +5,11 @@ import sbt.Keys.{libraryDependencies, scalacOptions} import sbt.addCompilerPlugin import sbt.complete.DefaultParsers._ import sbt.complete.Parser -import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject} -import src.main.scala.licenses.{DistributionDescription, SBTDistributionComponent} +import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} +import src.main.scala.licenses.{ + DistributionDescription, + SBTDistributionComponent +} import java.io.File @@ -14,9 +17,9 @@ import java.io.File // === Global Configuration =================================================== // ============================================================================ -val scalacVersion = "2.13.8" -val graalVersion = "21.3.0" -val javaVersion = "11" +val scalacVersion = "2.13.8" +val graalVersion = "21.3.0" +val javaVersion = "11" val defaultDevEnsoVersion = "0.0.0-dev" val ensoVersion = sys.env.getOrElse( "ENSO_VERSION", @@ -713,11 +716,11 @@ lazy val `profiling-utils` = project "org.netbeans.api" % "org-netbeans-modules-sampler" % netbeansApiVersion exclude ("org.netbeans.api", "org-openide-loaders") exclude ("org.netbeans.api", "org-openide-nodes") - exclude("org.netbeans.api", "org-netbeans-api-progress-nb") - exclude("org.netbeans.api", "org-netbeans-api-progress") - exclude("org.netbeans.api", "org-openide-util-lookup") - exclude("org.netbeans.api", "org-openide-util") - exclude("org.netbeans.api", "org-openide-dialogs") + exclude ("org.netbeans.api", "org-netbeans-api-progress-nb") + exclude ("org.netbeans.api", "org-netbeans-api-progress") + exclude ("org.netbeans.api", "org-openide-util-lookup") + exclude ("org.netbeans.api", "org-openide-util") + exclude ("org.netbeans.api", "org-openide-dialogs") exclude ("org.netbeans.api", "org-openide-filesystems") exclude ("org.netbeans.api", "org-openide-util-ui") exclude ("org.netbeans.api", "org-openide-awt") @@ -1007,7 +1010,6 @@ val truffleRunOptions = if (java.lang.Boolean.getBoolean("bench.compileOnly")) { ) } - val truffleRunOptionsSettings = Seq( fork := true, javaOptions ++= truffleRunOptions @@ -1744,7 +1746,8 @@ lazy val `std-base` = project Compile / packageBin / artifactPath := `base-polyglot-root` / "std-base.jar", libraryDependencies ++= Seq( - "com.ibm.icu" % "icu4j" % icuVersion + "com.ibm.icu" % "icu4j" % icuVersion, + "org.graalvm.truffle" % "truffle-api" % graalVersion % "provided" ), Compile / packageBin := Def.task { val result = (Compile / packageBin).value @@ -1767,7 +1770,7 @@ lazy val `std-table` = project Compile / packageBin / artifactPath := `table-polyglot-root` / "std-table.jar", libraryDependencies ++= Seq( - "com.ibm.icu" % "icu4j" % icuVersion % "provided", + "com.ibm.icu" % "icu4j" % icuVersion % "provided", "com.univocity" % "univocity-parsers" % "2.9.1", "org.apache.poi" % "poi-ooxml" % "5.2.2", "org.apache.xmlbeans" % "xmlbeans" % "5.1.0", 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 2168ede5b37b..4ca1bfccd9e2 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 @@ -1,6 +1,7 @@ from Standard.Base import all import Standard.Base.Data.Time.Duration +import Standard.Base.Data.Time.Date_Period import Standard.Base.Polyglot from Standard.Base.Error.Common import Time_Error_Data @@ -230,6 +231,14 @@ type Date day_of_week self = Day_Of_Week.from (Time_Utils.get_field_as_localdate self ChronoField.DAY_OF_WEEK) Day_Of_Week.Monday + ## Returns the first date within the `Date_Period` containing self. + start_of : Date_Period -> Date + start_of self period=Date_Period.Month = period.adjust_start self + + ## Returns the last date within the `Date_Period` containing self. + end_of : Date_Period -> Date + end_of self period=Date_Period.Month = period.adjust_end self + ## ALIAS Date to Time Combine this date with time of day to create a point in time. @@ -244,8 +253,8 @@ type Date from Standard.Base import Date, Time_Of_Day, Time_Zone example_to_time = Date.new 2020 2 3 . to_time Time_Of_Day.new Time_Zone.utc - to_time : Time_Of_Day -> Time_Zone -> Date_Time - to_time self time_of_day (zone=Time_Zone.system) = self.to_time_builtin time_of_day zone + to_date_time : Time_Of_Day -> Time_Zone -> Date_Time + to_date_time self (time_of_day=Time_Of_Day.new) (zone=Time_Zone.system) = self.to_time_builtin time_of_day zone ## Add the specified amount of time to this instant to get another date. 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 new file mode 100644 index 000000000000..a4aeaabc0265 --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Period.enso @@ -0,0 +1,36 @@ +from Standard.Base import all + +polyglot java import org.enso.base.Time_Utils +polyglot java import org.enso.base.time.Date_Period_Utils +polyglot java import java.time.temporal.TemporalAdjuster +polyglot java import java.time.temporal.TemporalAdjusters + +## Represents a period of time longer on the scale of days (longer than a day). +type Date_Period + Year + Quarter + Month + + ## 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 = + adjuster = case self of + Year -> TemporalAdjusters.firstDayOfYear + Quarter -> Date_Period_Utils.quarter_start + Month -> TemporalAdjusters.firstDayOfMonth + (Time_Utils.utils_for date).apply_adjuster date adjuster + + ## PRIVATE + adjust_end : (Date | Date_Time) -> (Date | Date_Time) + adjust_end self date = + adjuster = case self of + Year -> TemporalAdjusters.lastDayOfYear + Quarter -> Date_Period_Utils.quarter_end + Month -> TemporalAdjusters.lastDayOfMonth + (Time_Utils.utils_for date).apply_adjuster date adjuster 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 6cc94119f20b..68c471038be2 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 @@ -1,6 +1,8 @@ from Standard.Base import all import Standard.Base.Data.Time.Duration +import Standard.Base.Data.Time.Date_Period +import Standard.Base.Data.Time.Time_Period from Standard.Base.Error.Common import Time_Error polyglot java import java.time.format.DateTimeFormatter @@ -326,6 +328,24 @@ type Date_Time day_of_week self = Day_Of_Week.from (Time_Utils.get_field_as_zoneddatetime self ChronoField.DAY_OF_WEEK) Day_Of_Week.Monday + ## Returns the first date within the `Time_Period` or `Date_Period` + containing self. + 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 + + ## 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 + ## ALIAS Time to Date Convert this point in time to date, discarding the time of day @@ -409,7 +429,7 @@ type Date_Time example_to_json = Date_Time.now.to_json to_json : Json.Object - to_json self = Json.from_pairs [["type", "Time"], ["year", self.year], ["month", self.month], ["day", self.day], ["hour", self.hour], ["minute", self.minute], ["second", self.second], ["nanosecond", self.nanosecond], ["zone", self.zone]] + to_json self = Json.from_pairs [["type", "Date_Time"], ["year", self.year], ["month", self.month], ["day", self.day], ["hour", self.hour], ["minute", self.minute], ["second", self.second], ["nanosecond", self.nanosecond], ["zone", self.zone]] ## Format this time as text using the specified format specifier. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Time_Of_Day.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Time_Of_Day.enso index bb5f56006249..4020e65e48a0 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Time_Of_Day.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Time_Of_Day.enso @@ -1,6 +1,7 @@ from Standard.Base import all import Standard.Base.Data.Time.Duration +import Standard.Base.Data.Time.Time_Period from Standard.Base.Error.Common import Time_Error polyglot java import java.time.format.DateTimeFormatter @@ -170,6 +171,14 @@ type Time_Of_Day nanosecond : Integer nanosecond self = @Builtin_Method "Time_Of_Day.nanosecond" + ## Returns the first time within the `Time_Period` containing self. + start_of : Time_Period -> Time_Of_Day + start_of self period=Time_Period.Day = period.adjust_start self + + ## Returns the last time within the `Time_Period` containing self. + end_of : Time_Period -> Time_Of_Day + end_of self period=Time_Period.Day = period.adjust_end self + ## Extracts the time as the number of seconds, from 0 to 24 * 60 * 60 - 1. > Example @@ -193,8 +202,8 @@ type Time_Of_Day from Standard.Base import Time_Of_Day example_to_time = Time_Of_Day.new 12 30 . to_time (Date.new 2020) - to_time : Date -> Time_Zone -> Time - to_time self date (zone=Time_Zone.system) = self.to_time_builtin date zone + to_date_time : Date -> Time_Zone -> Date_Time + to_date_time self date (zone=Time_Zone.system) = self.to_time_builtin date zone ## Add the specified amount of time to this instant to get a new instant. 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 new file mode 100644 index 000000000000..881493fbb098 --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Time_Period.enso @@ -0,0 +1,35 @@ +from Standard.Base import all + +polyglot java import org.enso.base.Time_Utils +polyglot java import java.time.temporal.ChronoUnit + +## Represents a period of time of a day or shorter. +type Time_Period + Day + Hour + Minute + 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 + Day -> ChronoUnit.DAYS + Hour -> ChronoUnit.HOURS + Minute -> ChronoUnit.MINUTES + Second -> ChronoUnit.SECONDS + + ## PRIVATE + adjust_start : (Time_Of_Day | Date_Time) -> (Time_Of_Day | Date_Time) + adjust_start self date = + (Time_Utils.utils_for date).start_of_time_period date self.to_java_unit + + ## PRIVATE + adjust_end : (Time_Of_Day | Date_Time) -> (Time_Of_Day | Date_Time) + adjust_end self date = + (Time_Utils.utils_for date).end_of_time_period date self.to_java_unit diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoTimeZone.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoTimeZone.java index 06ac036d6f15..f062bc2c9619 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoTimeZone.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoTimeZone.java @@ -14,6 +14,7 @@ import java.time.DateTimeException; import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.time.zone.ZoneRulesException; @ExportLibrary(InteropLibrary.class) @@ -54,6 +55,11 @@ public static EnsoTimeZone system() { return new EnsoTimeZone(ZoneId.systemDefault()); } + @Builtin.Method(description = "Return the text representation of this timezone.") + public Text toText() { + return Text.create(zone.toString()); + } + @ExportMessage boolean isTimeZone() { return true; 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 c7f914a4ac4a..3f63c7b1a4c2 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 @@ -1,11 +1,15 @@ package org.enso.base; +import org.enso.base.time.Date_Time_Utils; +import org.enso.base.time.Date_Utils; +import org.enso.base.time.TimeUtilsBase; +import org.enso.base.time.Time_Of_Day_Utils; +import org.graalvm.polyglot.Value; + import java.time.*; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; -import java.time.temporal.TemporalAccessor; -import java.time.temporal.TemporalField; -import java.time.temporal.WeekFields; +import java.time.temporal.*; import java.util.Locale; /** Utils for standard library operations on Time. */ @@ -206,4 +210,24 @@ public static LocalTime parse_time(String text, String pattern, Locale locale) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); return (LocalTime.parse(text, formatter.withLocale(locale))); } + + /** + * Normally this method could be done in Enso by pattern matching, but currently matching on Time + * types is not supported, so this is a workaround. + * + *
TODO once the related issue is fixed, this workaround may be replaced with pattern matching
+ * in Enso; the related Pivotal issue: https://www.pivotaltracker.com/story/show/183219169
+ */
+ public static TimeUtilsBase utils_for(Value value) {
+ boolean isDate = value.isDate();
+ boolean isTime = value.isTime();
+ if (isDate && isTime) return Date_Time_Utils.INSTANCE;
+ if (isDate) return Date_Utils.INSTANCE;
+ if (isTime) return Time_Of_Day_Utils.INSTANCE;
+ throw new IllegalArgumentException("Unexpected argument type: " + value);
+ }
+
+ public static ZoneOffset get_datetime_offset(ZonedDateTime datetime) {
+ return datetime.getOffset();
+ }
}
diff --git a/std-bits/base/src/main/java/org/enso/base/time/Date_Period_Utils.java b/std-bits/base/src/main/java/org/enso/base/time/Date_Period_Utils.java
new file mode 100644
index 000000000000..6972c4a6f47a
--- /dev/null
+++ b/std-bits/base/src/main/java/org/enso/base/time/Date_Period_Utils.java
@@ -0,0 +1,25 @@
+package org.enso.base.time;
+
+import java.time.YearMonth;
+import java.time.temporal.*;
+
+public class Date_Period_Utils implements TimeUtilsBase {
+
+ public static TemporalAdjuster quarter_start =
+ (Temporal temporal) -> {
+ int currentQuarter = temporal.get(IsoFields.QUARTER_OF_YEAR);
+ int month = (currentQuarter - 1) * 3 + 1;
+ return temporal
+ .with(ChronoField.MONTH_OF_YEAR, month)
+ .with(TemporalAdjusters.firstDayOfMonth());
+ };
+
+ public static TemporalAdjuster quarter_end =
+ (Temporal temporal) -> {
+ int currentQuarter = YearMonth.from(temporal).get(IsoFields.QUARTER_OF_YEAR);
+ int month = (currentQuarter - 1) * 3 + 3;
+ return temporal
+ .with(ChronoField.MONTH_OF_YEAR, month)
+ .with(TemporalAdjusters.lastDayOfMonth());
+ };
+}
diff --git a/std-bits/base/src/main/java/org/enso/base/time/Date_Time_Utils.java b/std-bits/base/src/main/java/org/enso/base/time/Date_Time_Utils.java
new file mode 100644
index 000000000000..048556b48f16
--- /dev/null
+++ b/std-bits/base/src/main/java/org/enso/base/time/Date_Time_Utils.java
@@ -0,0 +1,21 @@
+package org.enso.base.time;
+
+import java.time.ZonedDateTime;
+import java.time.temporal.TemporalAdjuster;
+import java.time.temporal.TemporalUnit;
+
+public class Date_Time_Utils implements TimeUtilsBase {
+ public static final Date_Time_Utils INSTANCE = new Date_Time_Utils();
+
+ public ZonedDateTime start_of_time_period(ZonedDateTime date, TemporalUnit unit) {
+ return date.truncatedTo(unit);
+ }
+
+ public ZonedDateTime end_of_time_period(ZonedDateTime date, TemporalUnit unit) {
+ return date.truncatedTo(unit).plus(1, unit).minusNanos(1);
+ }
+
+ public ZonedDateTime apply_adjuster(ZonedDateTime date, TemporalAdjuster adjuster) {
+ return date.with(adjuster);
+ }
+}
diff --git a/std-bits/base/src/main/java/org/enso/base/time/Date_Utils.java b/std-bits/base/src/main/java/org/enso/base/time/Date_Utils.java
new file mode 100644
index 000000000000..3a37e428aab9
--- /dev/null
+++ b/std-bits/base/src/main/java/org/enso/base/time/Date_Utils.java
@@ -0,0 +1,12 @@
+package org.enso.base.time;
+
+import java.time.LocalDate;
+import java.time.temporal.TemporalAdjuster;
+
+public class Date_Utils implements TimeUtilsBase {
+ public static final Date_Utils INSTANCE = new Date_Utils();
+
+ public LocalDate apply_adjuster(LocalDate date, TemporalAdjuster adjuster) {
+ return date.with(adjuster);
+ }
+}
diff --git a/std-bits/base/src/main/java/org/enso/base/time/TimeUtilsBase.java b/std-bits/base/src/main/java/org/enso/base/time/TimeUtilsBase.java
new file mode 100644
index 000000000000..ee1931d62649
--- /dev/null
+++ b/std-bits/base/src/main/java/org/enso/base/time/TimeUtilsBase.java
@@ -0,0 +1,6 @@
+package org.enso.base.time;
+
+/**
+ * A base type for date/time util classes. Used just to mark the return type of {@code utils_for}.
+ */
+public interface TimeUtilsBase {}
diff --git a/std-bits/base/src/main/java/org/enso/base/time/Time_Of_Day_Utils.java b/std-bits/base/src/main/java/org/enso/base/time/Time_Of_Day_Utils.java
new file mode 100644
index 000000000000..b54d5be467b1
--- /dev/null
+++ b/std-bits/base/src/main/java/org/enso/base/time/Time_Of_Day_Utils.java
@@ -0,0 +1,19 @@
+package org.enso.base.time;
+
+import java.time.LocalTime;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
+
+public class Time_Of_Day_Utils implements TimeUtilsBase {
+ public static final Time_Of_Day_Utils INSTANCE = new Time_Of_Day_Utils();
+
+ public LocalTime start_of_time_period(LocalTime date, TemporalUnit unit) {
+ return date.truncatedTo(unit);
+ }
+
+ public LocalTime end_of_time_period(LocalTime date, TemporalUnit unit) {
+ LocalTime truncated = date.truncatedTo(unit);
+ LocalTime adjusted = unit.equals(ChronoUnit.DAYS) ? truncated : truncated.plus(1, unit);
+ return adjusted.minusNanos(1);
+ }
+}
diff --git a/test/Tests/src/Data/Time/Date_Spec.enso b/test/Tests/src/Data/Time/Date_Spec.enso
index 446879a74c45..4f1fc50c7cf5 100644
--- a/test/Tests/src/Data/Time/Date_Spec.enso
+++ b/test/Tests/src/Data/Time/Date_Spec.enso
@@ -1,6 +1,7 @@
from Standard.Base import all
import Standard.Base.Data.Text.Text_Sub_Range
+import Standard.Base.Data.Time.Date_Period
import Standard.Base.Data.Time.Duration
from Standard.Base.Error.Common import Time_Error
@@ -76,7 +77,7 @@ spec_with name create_new_date parse_date =
Test.fail ("Unexpected result: " + result.to_text)
Test.specify "should convert to time" <|
- time = create_new_date 2000 12 21 . to_time (Time_Of_Day.new 12 30 45) Time_Zone.utc
+ time = create_new_date 2000 12 21 . to_date_time (Time_Of_Day.new 12 30 45) Time_Zone.utc
time . year . should_equal 2000
time . month . should_equal 12
time . day . should_equal 21
@@ -132,8 +133,52 @@ spec_with name create_new_date parse_date =
date_1>date_2 . should_be_true
date_1