erikc5000 / island-time Goto Github PK
View Code? Open in Web Editor NEWA Kotlin Multiplatform library for working with dates and times
Home Page: https://islandtime.io
License: Apache License 2.0
A Kotlin Multiplatform library for working with dates and times
Home Page: https://islandtime.io
License: Apache License 2.0
When trying out the new java.time desugaring support, I discovered that Android doesn't support ZoneRulesProvider
at all (they've removed it from the docs entirely), relying on the ICU TimeZone
class instead for its TZDB. That class is API 24 and above and unfortunately, not desugared as far as I can tell.
It should be possible to implement an Island Time TimeZoneRulesProvider
that uses the android.icu.TimeZone
class. Of course, with a minimum API version that's going to be a deal breaker for many right now.
It's unclear if there's any way to make this work with the Android default TZDB on older API versions -- short of typealiasing the Java 8 time classes anyway, which is not a road I'd like to go down.
Creating a time zone rules provider that packages its own TZDB, which can be used on any platform supported by Island Time might be an avenue worth pursuing in the long term, but the threetenabp-extensions will accomplish that for Android at the moment. Just with extra baggage in a world where java.time is desugared. Another option too is creating a derivative of threetenabp/threetenbp with all the non-TZDB essential parts removed.
There is definitely value in providing an option to package a TZDB rather than relying on a potentially outdated default. See recent Brazil time zones woes: https://www.reddit.com/r/androiddev/comments/e00swv/does_anyone_face_brazils_dst_related_issue_by/
Edit: My initial assessment was off. It's possible to get enough information from the available desugared java.time APIs to make everything work. It'll just require doing things a bit different on Android.
When constructing intervals, it would be nice to support a syntax like:
for (date in today until 1.weeks.later) {
// ...
}
for (date in 1.weeks.earlier until today) {
// ...
}
It reads a bit nicer than:
for (date in today until today + 1.weeks) {
// ...
}
for (date in today - 1.weeks until today) {
// ...
}
The ".earlier" and ".later" would just be syntactic sugar around a duration unit, but at no added expense if implemented using an inline class. We could forego the the sugar and use the durations directly, but it's a little less clear.
for (date in today until 1.weeks) {
// ...
}
for (date in 1.weeks until today) {
// ...
}
Duration
splits into just nanoseconds for the sub-second component. The individual unit clases like IntMilliseconds
split into each applicable sub-second unit -- in this case, milliseconds.
Not sure if this is what one would expect. Could allow it to split into "seconds and nanoseconds" in addition to "seconds and milliseconds".
The current clock implementation is limited to millisecond precision, which is the best you can get with Android or Java 8 anyway. But on Java 9 and iOS/macOS, microsecond resolution is available.
Millisecond resolution may be fine for most and should be a bit faster -- certainly, it'll result in fewer allocations when getting the current time. The default should probably be nanosecond precision where available, but providing the option to use a faster millisecond precision clock would be the best of both worlds.
To support nanosecond precision time on Java 9, we'd want to introduce a PlatformInstant
expect class that is implemented by Java's Instant
class and wrap Java's Clock
. This should keep additional allocations to a minimum. Since we'd be using java.time classes under the hood, this implementation won't work on Android though, so it'll require a new target or project rearrangement of some sort.
Some of the following operators could be quite useful additions:
range1 union range2
range1 intersect range2
range1 span range2
range1.abuts(range2)
range1.encloses(range2)
range1.overlaps(range2)
range.mapEndpoints(start = { it.startOfWeek }, endInclusive = { it.endOfWeek })
range.mapEndpoints { it.startOfWeek }
- apply a function to both endpointsrange.shiftedBy((-1).days)
- specialization of mapEndpoints
that maps the start/end by a period or durationrange.copy(start = anotherDate)
ZonedDateTimeInterval
and OffsetDateTimeInterval
pose some challenges since the endpoints may have different zones or offsets. It might make sense to have them return InstantInterval
if you try to perform a union or intersection on them, for example.
I have been trying to fix this build error on iOS item and finally narrowed it down to island-time. I added Kotlin atomicfu and made various other attempts to fix it
Extracting dependency: /Users/runner/.konan/cache/clang-llvm-apple-8.0.0-darwin-macos.tar.gz into /Users/runner/.konan/dependencies
e: Could not find "atomicfu-cinterop-interop" in [/Users/runner/runners/2.262.1/work/ReimaginedLampMobileApps/ReimaginedLampMobileApps, /Users/runner/.konan/klib, /Users/runner/.konan/kotlin-native-macos-1.3.71/klib/common, /Users/runner/.konan/kotlin-native-macos-1.3.71/klib/platform/ios_x64].
Is there a sample project I could look at?
Here is a branch and build for repro
Thank you for making this library, hope to use it
Current inline classes that are candidates for being converted to regular classes:
Year
YearMonth
UtcOffset
TimeZone
While having Year
and YearMonth
as inline classes might reduce allocations in some situations, it creates issues with construction, validation, and Java/ObjC interop. I see these classes being more valuable once a base class is introduced that allows mixed precision dates and ranges to be represented (ie. 2004/2005-02-08 or 2018-04/2019). When used in that context, they'll be boxed anyway. But having Year
as a thin wrapper around an Int
that exposes appropriate operations can be nice.
val year = Year(2020)
val leapYear = year.isLeap
I'm leaning in favor of regular classes, but would like to hear other's thoughts.
As for UtcOffset
and TimeZone
, they could probably just use some form of caching to reduce allocations instead of being inline classes.
Edit: TimeZone
is no longer an inline class since it made consistent fixed zone behavior across platforms easier.
For the most part, Island Time detects and throws an exception on overflow, like java.time does. The unit classes are a bit inconsistent in that they aren't overflow safe by default.
Consider IntHours:
val totalHours = Int.MAX.hours + 1.hours // overflow
val minutes = Int.MAX.hours.inMinutes // overflow
val minutes2 = Int.MAX.hours.inMinutesExact() // exception
Internally, we have plusExact
and minusExact
infix functions to do overflow safe math. The question is whether or not these should be the default and instead, require a plusWithOverflow
, minusWithOverflow
, or inMinutesWithOverflow
. I'm leaning "yes", but curious to hear if others have an opinion on this.
On native, it seems like LLVM's UBSan could be used to detect overflow during debug as a possible alternative, though not sure if enabling it is possible with Kotlin Native. Doesn't help with JVM though, obviously.
Right now, if a DateTime
parser provides both DAY_OF_MONTH
and DAY_OF_YEAR
, it'll just take the day of month value first assuming it has a year and month. But the day of year value may conflict or otherwise be indicative of a poorly defined custom parser.
This sort of behavior needs to looked at across all date-time primitives and may require more sophisticated resolution behavior.
The manner in which objects are constructed isn't entirely consistent at the moment. The most "Kotlin" approach is a bit of a question mark though and certainly worthy of discussion.
We have classes like Duration and Period, which do construction like so:
val duration = durationOf(5.seconds, 5.nanoseconds)
val period = periodOf(2.years, 3.days)
This reads nicely and feels natural enough with the "of", though such syntax tends to be used with list arguments.
Then we have classes like Date or DateTime that use regular constructors or constructor functions that start with capitals.
val date = Date(2019, Month.May, 5)
val dateTime = DateTime(2019, 5, 5, 13, 0)
Then we have Time
, which uses invoke()
on the companion object so that it can do some object pooling prior to actual construction, making it a bit weird. Companion invokes all need to go, but what is the right construction method? timeOf()
? But then should all of the date-time representations use a similar style? What makes for the best syntax here?
It should be possible to increase the supported year range to match java.time. I capped it 1-9999 initially just to keep things simple.
This will require more tests to cover the expanded range and the ISO-8601 parsers should be modified to support "expanded" year formats.
Right now, each duration unit has a separate Long
and Int
class generated for it. For example:
val longNanoseconds: LongNanoseconds = 8L.nanoseconds
val intNanoseconds: IntNanoseconds = 8.nanoseconds
While it can be quite useful having both representations and it allows for choosing the appropriate backing primitive for different situations, it does result in an explosion of signatures in the public API. And it would be nice to just see Nanoseconds
as the name.
We could continue to use the Int
representations to optimize internally, but stop exposing them publicly. It would come at the cost of some efficiency, but it's definitely something to consider.
Hello
When will island-time support Wasm?
I'm using the latest version of the library (0.6.3). When I'm trying to obtain a current date with the Date.now()
application hangs when running it on the iOS Simulator. Is it a known issue?
I also tried Instant.now()
, and DateTime.now()
, and all of them hang the app.
It'd be good to have Android and iOS sample projects as part of the repo.
Thanks for this useful library! I'm trying to parse a string with AM/PM, but amPm()
doesn't seem to have any effect.
val parser = dateTimeParser {
year(4)
+'-'
monthNumber(2)
+'-'
dayOfMonth(2)
+' '
hourOfDay(2)
+':'
minuteOfHour(2)
+' '
amPm()
}
val hour = "2020-07-14 03:56 PM".toDateTime(parser).hour.toString()
println(hour)
I expected hour
to be 15, but it's 3. Is this a bug or am I missing something?
All parsing done by DateTimeParsers is currently case-sensitive, which is alright for strict ISO-8601 parsing, but could be an issue when dealing with custom formats.
We could add a global setting on DateTimeParserSettings
that allows any parser to be used in a case-sensitive or insensitive mode.
We could also add the ability to the change the case sensitivity of parsing within a given block. java.time allows that.
val parser = dateTimeParser {
caseInsensitive {
+" hours"
caseSensitive { +" of day" }
}
}
Empty and unbounded might be better handled with sealed classes. That would make it possible to do away with using MIN and MAX sentinels, which create some headaches currently -- and in the case of ZonedDateTime
, Android java.time desugaring can't support properly (#57).
It should be possible to define a custom format similar to how a custom parser can be defined. Something like:
val customFormat = dateTimeFormat {
monthNumber()
+'/'
dayOfMonth()
}
val date = Date(2019, 10, 28)
val string = date.toString(customFormat)
For the initial implementation, we can avoid adding support anything locale-specific. Locale will open up another can of worms.
Implementing this will probably require inserting an interface into the date-time primitives -- something akin to TemporalAccessor
in java.time, but Period
and Duration
should also implement it since we include parser support for them. This interface should provide access to the available date-time fields on the object.
interface Temporal {
fun get(field: DateTimeField): Long
}
Introducing a TemporalQuery
-like interface for access to non-Long fields may also be necessary.
Ultimately, I'm envisioning the abstractions added here will also tie in with and enable parsing of arbitrary ISO-compliant strings.
val temporal = genericIsoParser.parseAny(text)
if (temporal is Year) {
// ..
} else if (temporal is Date) {
// ..
}
This will be useful for working with intervals that have mixed-precision end points, like "2018/2019-05-01".
Obviously, these are more advanced use cases though, so the focus should initially be on what's necessary to support custom formatting.
Instant is an inline class right now that piggybacks on Duration and gives it a different interface. While that's convenient, it brings with it all the usual inline class limitations. This should be converted to a regular class that recycles Duration code like in java.time.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.