Dates and Times¶
Island Time has a wide array of different date-time classes, each tailored to its own set of use cases. These classes model the date and timekeeping system defined in ISO-8601, the international standard for the exchange of dates and times. The ISO standard applies the present-day Gregorian calendar proleptically, which is to say, even to the time period before it was adopted.
Date Representations¶
Class | Precision | Example ISO Representation |
---|---|---|
Date |
day | 2020-02-15 |
YearMonth |
month | 2020-02 |
Year |
year | 2020 |
The Date
class represents a date in an ambiguous region. It could be in New York City, it could be in Tokyo. The instants in time that define the start and end of a Date
can only be determined in the context of a particular time zone — hence the ambiguous part.
// Get the current date in the local time zone of the system
val today = Date.now()
// Create a date from components
val leapDay = Date(2020, Month.February, 29)
// Parse a date from ISO string representation
val cincoDeMayo = "2020-05-05".toDate()
It's also possible to represent a date with reduced precision. For example, a YearMonth
could be used to represent a credit card expiration date containing just a year and month.
// Create a year-month from a year and month
val expiration = YearMonth(year, month)
// A year-month can also be built using the "at" operator
val yearMonth: YearMonth = Year(2020) at Month.AUGUST
Time of Day¶
The Time
class can be used to represent a time of the day in an ambiguous region. Unlike Date
, there are no classes with reduced precision — a Time
is always precise to the nanosecond.
// Get the current time in the local time zone of the system
val currentTime = Time.now()
// Create a time from components
val time = Time(13, 59, 59, 999_999_999)
// Destructure a time back into components
val (hour, minute, second, nanosecond) = time
Combined Date and Time of Day¶
A DateTime
combines a Date
and Time
, allowing you to represent both in a single data structure, still in an ambiguous region.
// Create a date-time from individual date and time components
val dateTime = DateTime(2020, Month.JANUARY, 21, 1, 0)
// Destructure a date-time
val (date, time) = dateTime
val date = Date(2019, Month.MARCH, 15)
// The "at" operator can also be used to create a date-time
val anotherDateTime: DateTime = date at Time.NOON
// Or you could get the date-time at midnight from a date
val startOfDay: DateTime = date.startOfDay
There's no guarantee that a DateTime
will exist exactly once in a given time zone. Due to daylight savings time transitions, it may exist twice or it may not exist at all. We'll get into this more shortly, but it's important to keep in mind that working with and manipulating a DateTime
directly can lead to subtle bugs.
Instants in Time¶
So far, the classes we've looked at model dates and times in an ambiguous region, but often we want to unambiguously capture an instant in time. There are three different classes in Island Time that can do this, each serving a different purpose.
Class | Description |
---|---|
Instant |
A timestamp |
ZonedDateTime |
A date and time of day in a particular time zone |
OffsetDateTime |
A date and time of day with fixed UTC offset |
An Instant
is simply a number of seconds and nanoseconds that have elapsed since the Unix epoch (1970-01-01T00:00Z
), ignoring leap seconds. There's no concept of "date" without conversion to one of the other types. Practically speaking, this is the class you should use when you don't care about the local time and just want a UTC timestamp.
data class DogDto(
val name: String,
val breed: String,
// Capture the current system time
val creationTime: Instant = Instant.now()
)
To capture an instant along with the local time, you have two options — OffsetDateTime
and ZonedDateTime
. Both store a DateTime
along with a UtcOffset
, however, ZonedDateTime
is also aware of time zone rules, which is an important distinction.
TimeZone
vs. UtcOffset
¶
In Island Time, a UtcOffset
is just a number of seconds that a local time must be adjusted forward or backward by to be equivalent to UTC. A TimeZone
defines the rules used to determine the UTC offset. Time zones fall into two categories — region-based (TimeZone.Region
) and fixed offset (TimeZone.FixedOffset
).
Region-based zones have identifiers, such as "America/New_York" or "Europe/London", that correspond to entries in the IANA Time Zone Database.
Fixed offset zones have a fixed UTC offset. While region-based zones are generally preferrable, a suitable one may not exist in all situations.
ZonedDateTime
vs. OffsetDateTime
¶
While most platforms nowadays draw their understanding of time zones from the IANA Time Zone Database, time zones and their rules change all the time and different systems might have different versions of the database or only a subset of it available. This makes persistance and serialization of ZonedDateTime
troublesome since there's the possibility that when the stored data gets read later, the zone can't be found or its rules have changed, thus altering the local date and time.
Using OffsetDateTime
guarantees that you'll never get an exception due to an unavailable time zone and that the value you save will be the value that's read later, making it well-suited for this particular use case. More often than not, you should use ZonedDateTime
since it will handle daylight savings transitions correctly when doing any sort of calendar math, but you may want to consider converting to an OffsetDateTime
when you persist or serialize your data.
val date = Date(2020, Month.MARCH, 8)
val time = Time(2, 30)
val zone = TimeZone("America/New_York")
// 2:00 on March 8 marks the beginning of daylight savings time on the east
// coast of the United States, so 2:30 doesn't exist. The time is automatically
// adjusted by an hour to 3:30.
val zonedDateTime = date at time at zone
println(zonedDateTime)
// Output: 2020-03-08T03:30-04:00 [America/New_York]
// If we subtract an hour, the offset will revert to that of standard time
println(zonedDateTime - 1.hours)
// Output: 2020-03-08T01:30-05:00 [America/New_York]
// It's easy to convert a ZonedDateTime to an OffsetDateTime
println(zonedDateTime.toOffsetDateTime())
// Output: 2020-03-08T03:30-04:00
// It's also possible to change the time zone such that it uses a fixed offset
// instead of "America/New_York", making it functionally equivalent to an
// OffsetDateTime.
println(zonedDateTime.withFixedOffsetZone())
// Output: 2020-03-08T03:30-04:00
// Or change the zone while preserving the captured instant
println(zonedDateTime.adjustedTo(TimeZone("America/Los_Angeles")))
// Output: 2020-03-07T23:30-08:00
Patterns, Properties, and Operators¶
Throughout Island Time's date-time primitives, you'll find a set of patterns that remain (relatively) constant, as well as a number of properties and operators that simplify common tasks.
at
¶
The at
infix function can be used to build up date-time primitives from "smaller" pieces. For example, we can create a DateTime
by combining a Date
and a Time
.
val dateTime = Date.now() at Time.NOON
We can then turn that into a ZonedDateTime
by combining it with a TimeZone
.
val zonedDateTime = dateTime at TimeZone.systemDefault()
copy()
¶
Similar to Kotlin's data classes, each date-time primitive has a copy()
method available, making it easy to create a copy while changing any number of properties.
val dateTime = DateTime.now().copy(dayOfMonth = 15)
val dateTimeAtMidnight = dateTime.copy(time = Time.MIDNIGHT)
Addition and subtraction¶
A duration of time can be added or subtracted from a date-time primitive. Which units are supported will vary depending on whether the primitive is date-based, time-based, or both.
val tomorrow = Date.now() + 1.days
val yesterday = Date.now() - 1.days
val tenSecondsLater = Instant.now() + 10.seconds
When working with ZonedDateTime
, adding a day-based period of time may cross a daylight savings time transition, in which case, adding 1.days
may not be the same as adding 24.hours
.
Start and end of time periods¶
Relative to any date-based primitive, it's possible to get the start or end of a given period, be it the year, month, week, or day.
val date = Date.now()
val startOfYear = date.startOfYear
val endOfYear = date.endOfYear
val startOfMonth = date.startOfMonth
val startOfDay: DateTime = date.startOfDay
When it comes to weeks, we need to consider which day represents the start of the week. According to the ISO standard, that's Monday. However, depending on the locale, that may be on Sunday or Saturday instead. WeekSettings
and the platform Locale
type can be used to provide control over this.
// Start of ISO week (Monday start)
val isoStart = Date.now().startOfWeek
// Start of week using Sunday as start
val sundayStart = Date.now().startOfWeek(WeekSettings.SUNDAY_START)
// Respect the user's system settings (usually, most appropriate)
val systemStart = Date.now().startOfWeek(WeekSettings.systemDefault())
// Use the default associated with a particular locale
val localeStart = Date.now().startOfWeek(explicitLocale)
You can also get the week period as a range or interval.
// Get the date range of the current week
val rangeOfWeek: DateRange = Date.now().week(WeekSettings.systemDefault())
Previous or next day of week¶
To get, say, the previous Tuesday from a particular date-time, you can do something like this:
val now = ZonedDateTime.now()
// Get the Tuesday before "now" at the same time of day
val nowOnTuesday = now.previous(TUESDAY)
// Get the Tuesday before "now" or "now" if it already falls on a Tuesday
val nowOnTuesdayOrSame = now.previousOrSame(TUESDAY)
Similarly, you can use next()
or nextOrSame()
to get the next day of the week relative to the current date.
val today = Date.now()
val nextWednesday = today.next(WEDNESDAY)
val nextWednesdayOrToday = today.nextOrSame(WEDNESDAY)
Rounding¶
A time or date-time can be rounded up, down, or half-up to the precision of a particular unit.
val dateTime = DateTime.now()
// Output: 2020-06-30T06:32:14.168
// Round half-up to the nearest minute
val roundedToMinute = dateTime.roundedTo(MINUTES)
// Output: 2020-06-30T06:32
// Round up to the nearest hour
val roundedUpToHour = dateTime.roundedUpTo(HOURS)
// Output: 2020-06-30T07:00
// Round down to the nearest hour (alternatively, you can use roundedDownTo())
val roundedDownToHour = dateTime.truncatedTo(HOURS)
// Output: 2020-06-30T06:00
You can also round to the nearest 15 minutes — or whatever increment you'd like.
// Round half-up to the nearest 15 minutes
val roundedToNearest15Mins = dateTime.roundedToNearest(15.minutes)
// Output: 2020-06-30T06:30
// Round down to the nearest 100 milliseconds
val roundedDownTo100Millis = dateTime.roundedDownToNearest(100.milliseconds)
// Output: 2020-06-30T06:32:14.1
Week numbers¶
You can obtain the week number and year as defined in the ISO week date system like this:
val date = Date.now()
// Get the week-based year, which may differ from the regular year
val isoWeekYear: Int = date.weekBasedYear
// Get the week of the week-based year
val isoWeekNumber: Int = date.weekOfWeekBasedYear
// Or convert the date to a full ISO week date representation in a single step
date.toWeekDate { year: Int, week: Int, day: Int ->
// ...
}
Different week definitions can be used by specifying the WeekSettings
explicitly.
val usaWeekNumber: Int = date.weekOfWeekBasedYear(WeekSettings.SUNDAY_START)
weekOfYear
vs. weekOfWeekBasedYear
The week number associated with a particular date could fall in the prior or subsequent year, depending on how the week is defined. weekOfYear
will return the week number relative to the date's regular year
— 0
if it falls in the prior year, for example. On the other hand, weekOfWeekBasedYear
will adjust the number to the weekBasedYear
.
ISO Representation¶
Any date-time primitive can be converted to an appropriate ISO string format by simply calling toString()
. To convert a string into a date-time primitive, use the appropriate conversion function, such as String.toDate()
or String.toInstant()
.
val date = Date.now()
val isoString: String = date.toString
val dateFromString: Date = isoString.toDate()
By default, Island Time reads and writes using ISO-8601 extended format, which is most common. Predefined parsers are also available that can handle other formats.