davedelong / time Goto Github PK
View Code? Open in Web Editor NEWRobust and type-safe date and time calculations for Swift
License: MIT License
Robust and type-safe date and time calculations for Swift
License: MIT License
Desired functionality:
Fixed<LTOEDay>
, get the week (Array<Fixed<Day>>
) that contains itFixed<GTOEMonth>
, get an array of weeks in it, with an option to pad out week arrays so they're all 7 daysconvert(to timeZone:)
behaves consistently for all T
in Absolute<T>
and in all other cases besides <Day>
it behaves intuitively. For example, taking March 16 2020 at 12:00 PM EST (Absolute<Minute>
) and converting it to PST gives you a different set of date components, which "feels" right.
Converting March 16, 2020 (Absolute<Day>
) from UTC to NZDT (UTC+13) gives March 17, 2020 because internally it takes the approximate midpoint of March 16, 2020 UTC (noon on that day in UTC) and uses the date components of that instant in NZDT, which is 1:00AM March 17 NZDT. This "feels" wrong (to me).
The behaviour for anything smaller than Day
is intuitively correct (as in the first example above), and for anything bigger than Day
is intuitively correct because the midpoint of a month, year, or era will end up in the same month, year, or era (AFAIK, maybe this is one of those falsehoods programmers believe) because time zones are never several days apart from each other.
I suppose we can't change just the behaviour of convert(to timeZone:)
for Absolute<Day>
but I wonder if we could provide a sameDay(in timeZone:)
or something?
This realization made me rethink the way I was doing things and in my own app this isn't actually something I need, so maybe things are fine as-is, or maybe convert(to timeZone:)
just needs some docs explaining this?
The ISO 8601 calendar uses different day numberings than the Gregorian calendar. The Locale.Weekday
conversion assumes that 1 = Sunday, 2 = Monday, etc. However, on the ISO 8601 calendar, 1 = Monday, 2 = Tuesday, etc.
Is there a bug here? Does a Fixed<Day>
produce the wrong Locale.Weekday
value?
Not an 'issue', as such, so feel free to mark closed. Time is difficult, as you note, and my first time-related challenge in the iOS world was that, in the early days, the "wall clock" could be off true by up to two minutes. With a satellite tracking app in work, that was going to make life complicated; at 7Km/s, 100 seconds could be the wrong side of the sky!
My solution was to write and embed a simple Simple Network Time Protocol (SNTP) client in my app .. it wasn't nearly as rigorous as the NTP reference code, but it gave me sub-second accuracy. At some point in the evolution of iOS, NTP was adopted and "wall clock" became useful (typically, my iPhone's "wall clock" can be trusted to be within a few microseconds).
I lobbied for a while, at WWDCs and via Radar, for a Time analog to Location .. a CoreTime framework for example, that offered, among other things, an 'accuracy' negotiation. Your work here is aimed at a different target in the 'time' domain, nevertheless I'm excited .. I see things I will put to immediate use. Thank you, Dave.
Knowing if a region wants 12/24 hour time etc could be useful
When using StrictConcurrency
, there are some types that throw warnings when used in async contexts due to lack of Sendable
conformance (namely Fixed
). I believe that several types could likely conform to Sendable with little additional work.
I'd love to contribute by tackling this and opening a pull request!
Exporting an Error
-conforming type as an enum makes it difficult to expose new kinds of errors in the future.
In keeping with the idea that "enums are mathematically closed sets of values and structs are not", these Error types should be changed to structs.
Some careful thought will need to happen so that the struct can be properly introspected to handle the various cases.
It'd be nice to add a way for a Clock
to notify you about significant time changes, like how clocks chime in the real world.
There are a couple API scenarios to consider:
Value
, notify me every time this Difference
has elapsedSISecond
/Difference
has elapsedDateComponents
occursThere will be some interesting challenges when it comes to clocks that scale time (ie, run slower or faster than real time).
Hi, can I get a time from a string?
I would like to have more control over the formatting of the absolute value. For example I would like to get the day of the week. I would usually just use "E" as a date format in order to get the first letter of the day of the week. However it appears that Template
initialisers are internal and therefore unable to use a custom format. What would you suggest in this scenario?
What would you think about changing the method signatures such that as a general rule, all zero-argument methods that return a value were replaced with computed properties? For example, func now() -> Instant
would be replaced by var now: Instant
. I think this would make code that called those methods significantly more concise and easier to read.
It's difficult to deal with precision higher than milliseconds.
I'm attempting to write tests dealing with nanosecond precision and they're failing all over the place. Couple this with the face that DateFormatter
won't format anything smaller than milliseconds and maybe Time
should just cap things off there.
Region.autoupdatingCurrent
is defined here. Region.init()
is defined here. Region.init()
makes a copy of the calendar, and then sets the calendar's locale
and timeZone
to the passed in locale and timeZone. According to the docs here, after mutation the autoupdating calendar "will no longer track the user’s preferred calendar".
Fixed.roundToMultiple(of:direction:)
could be optimized by using modulo arithmetic to try and 'jump' to the rounded values
Hi,
I'm looking for a way to check, whether a previous call to my networking api was sent more than 5 minutes ago. I thought trying your project for this would be a nice start. On first launch, there was now call at all so I usually use Foundation.Date.distantPast
. I ended with the following code ...
let now = Clock.system.thisMinute()
let lastUpdate = Absolute<Minute>(region: .current, instant: .init(date: .distantPast))
let differenceSinceLastUpdate = now - lastUpdate
guard differenceSinceLastUpdate.minutes > 5 else { return }
... but this seems to be wrong as differenceSinceLastUpdate.minutes
is not the value I expected it to be.
What is the right way to get the number of minutes between two dates?
Hello, first of all, thank you for all your work on this library.
Apart from that, the title says it all, would be nice to be able to use Template
values for raw formatting/parsing. I thought about making a micro PR, but I feared there was a reason for it not being public, besides it's a change so small that you could make trough Github edit.
P.S.: Am I missing something or is it not possible to parse a date with the format y-MM-dd
without using the rawFormat
? I thought that using the iso8601
Calendar
would work, but it does not seem to be the case.
A floating value is an "any" value, such as "Any June". Its type would be Floating<Month, Month>
. "Any June 1st" would be Floating<Day, Month>
.
These would need:
- Approximate differences
- Formatting
- Parsing
- Extraction from Fixed values
- Initialization from components
- Enumeration
- Rounding
The discussion on the Swift Forums brought up some objections to the usage of unsafe
in the APIs:
init(region: Region, unsafeDateComponents: DateComponents) throws
struct UnsafeAdjustment<...> {
// ...
}
The primary objection is that "unsafe" is used exclusively in the standard library to refer to memory safety issues, and that having a method declared with throws
is enough indication that something can go wrong.
@belkadan brought up this point:
"Unsafe" is not supposed to mean "has preconditions which are the client's responsibility". In a narrow form, it would be "can break type safety, memory safety, or exhaustivity" (because breaking any one can lead to the others breaking), but if you were to make an argument for a broader form, I would say it would be "may corrupt data if you break the preconditions", which Time's usage is almost the opposite of.
These are all valid points.
I'm open to changing the usage of "Unsafe" in Time
, but here are some thoughts to consider:
Unsafe
is used for more than just a name in an initializer parameter; it's also used as the name of a type (currently internal, but will be public): UnsafeAdjustment
Given the two broad categories of adjustments, using the term Unsafe
has been nice, because it has a clear and familiar antonym of Safe
. I'd prefer any replacement word to also have a clear antonym, to maintain the strong pairing between "safe" and "unsafe" adjustments.
Any alternative would ideally be succinct, as dealing with MightPossiblyFailAdjustment
in code would be tiresome and aesthetically unpleasing.
Time
is not part of the standard library, thus it is not bound by the standard library's style and conventions.
"Unsafe" is a term familiar to Swift developers, even beyond the explicit context of "memory safety"
I’m still using CocoaPods mainly because of automatic Settings bundle generation.
Happy to make a PR adding a Podspec if you’re interested?
try! Absolute<Day>(region: .posix, year: 2019, month: 10, day: 14)
always throws because in Time.Calendar.exactDate(from:, matching:)
, restricted
does not include a value for era
, but proposedComponents
has era
set to 1
, so guard proposedComponents == restricted
fails.
Of course this isn't a big deal because I only use this initializer in my test target and I can easily add the era, but the API suggests it should work, since in the initializer era
is an Optional and defaults to nil
.
If I knew the correct fix I'd submit a PR...maybe this works?
var restrictedWithEra = restricted
if restrictedWithEra.era == nil && !isEraRelevant {
restrictedWithEra.era = proposed.era
}
guard proposedComponents == restrictedWithEra.era else {
throw AdjustmentError.invalidDateComponents(restricted)
}
BTW this library is everything I never knew I wanted. Thank you! 😄
There's no actual time difference between 1 PM Pacific Time and 4 PM Eastern Time, but there is a perceived difference based on the different time zones.
There should be an "easy" way to capture this perceived difference.
… so potential users & contributors know where they stand
I have the following code:
let utc = Clock.system.converting(to: TimeZone(identifier: "UTC")!).thisMinute()
let plusFiveMinutes = (utc + .minutes(5)).firstInstant.date
Running it causes the following error to be thrown in Xcode:
(Nothing useful in the console, blocked out because it had sensitive API routes)
Any ideas here? It looks like some kind of possible recursion or stack overflow bug because the entire stack is filled with the two same calls (Value.range.getter
and Value.approximateMidPoint.getter
) but not positive.
Let me know if there's anything I can do to help debug.
The README currently has an API overview, but it'd be good to have some small examples as well.
When initialising an Absolute TimePeriod we are seeing very slow performance. It is taking 90ms+ to initialise a time period. Also getting bad performance when trying to add or subtract from time periods. Please see the attached image of the time profile I ran which leads me to the TimePeriod initialiser and the subtracting of time period.
I'd like an /Examples
folder with some simple iOS apps showing how to use Time
.
The first one that comes to mind is a basic iOS app using a UICollectionView
to build a month calendar view.
It'd be nice to have jazzy
generate documentation whenever code is pushed.
This page has an overview of how to accomplish something similar: https://learningswift.brightdigit.com/swift-package-continuous-integration-guide/#documentation
Trying to compute a difference between two absolutes results in a EXC_BAD_ACCESS
error.
Absolute.range
is being computed from Absolute.approximateMidPoint
and vice-versa.
Having an "RFC"-style layout would make it easier to digest (should, should not, must, must not, ...)
I am woking on a project and name of my project is 'Time' too, so I can not import.
Is there a better way to solve this issue ?
Hi,
I'm looking for a way to check, whether a previous call to my networking api was sent more than 5 minutes ago. I thought trying your project for this would be a nice start. On first launch, there was now call at all so I usually use Foundation.Date.distantPast
. I ended with the following code ...
let now = Clock.system.thisMinute()
let lastUpdate = Absolute<Minute>(region: .current, instant: .init(date: .distantPast))
let differenceSinceLastUpdate = now - lastUpdate
guard differenceSinceLastUpdate.minutes > 5 else { return }
... and found that it is ending in an infinite loop in Absolute.range
.
Here my questions:
Thanks in advance.
Hi
Thanks for this great library.
The Chronology
release version is not available now.
I installed with master
branch by using Carthage
, but it is useful to release the current version.
Thanks.
You've probably come across this, but just in case you haven't:
Allen's interval algebra is a calculus for temporal reasoning that was introduced by James F. Allen in 1983.
The calculus defines possible relations between time intervals and provides a composition table that can be used as a basis for reasoning about temporal descriptions of events.
There's a Wikipedia summary here. Allen's original paper, Maintaining knowledge about temporal intervals, is here. I first heard about it in this talk, Modelling Time, by Eric Evans (the Domain-Driven Design guy).
Even if all of this is outside of the scope of what you're trying to do it seems like it might be a useful thing to be aware of. Just, you know, in case. :-)
This seems fairly important.
Hey there,
Thank you for this fantastic library!! Quick question that I couldn't find in the docs (hopefully I didn't just miss it).
I need to calculate a date 5 minutes from now in UTC. Here's what I'm doing:
let utc = Clock.system.converting(to: TimeZone(identifier: "UTC")!).thisMinute()
let plusFiveMinutes = (utc + .minutes(5)).firstInstant.date
I'm clear on everything up until .firstInstant.date
- that was the only way I could find to pull a Foundation Date
out of my addition operation. Does that seem right?
Thank you again!
Is this possible? If so, where in the documentation is this referenced?
The Reacting to Changing Time
section should use .asyncValues
, not .asyncValue
If you print a Fixed<Day>
, it awkwardly includes the time zone:
let today = Clocks.system.today
print("Today is \(today)")
// Today is Thursday, February 29, 2024 at MST
That "at MST" is because Fixed.naturalFormats(in:)
is unconditionally including the time zone in the list of templates. The time zone is almost never relevant for values that do not have time components, so it should be omitted for Fixed<Era>
, Fixed<Year>
, Fixed<Month>
, and Fixed<Day>
.
I have a Absolute<Day>
and would like to get a string like 2018-05-11
, no matter the user setting (the usage is for file output, not UI). Is there a way to accomplish this?
The format(year: , month: ., day:)
seems to always use a style like 3/27/20
(note the order and the /
). I've tried changing the locale of the date itself but doesn't seem to have any effect.
Given the purpose of this library it occurs to me that there's something else you could try to help users with: storing and retrieving future date-time values.
To motivate, I'll refer to this post:
which recommends that developers "preserve local time, using UTC as derived data to be recomputed" in specific use-cases. (There's HackerNews discussion here; one of the comments points to RFC5545. In particular, take a look at section 3.3.5.)
What I'm thinking is that this library could a) enumerate the different kinds of storage, different kinds of trade-offs; and b) provide appropriate encodings/decodings. Bonus points if the encodings/decodings aren't jut BLOBs—e.g., if they work well in CoreData, JSON, etc.!
Hi there
I really like this library, so I thought I'd mention some weird behaviour I encountered while unit testing.
I set my clock rate at x100 to speed everything up during tests, but noticed some inaccuracies while doing this. Maybe this is just an example of #42 ?
var cancels: [AnyCancellable] = []
let fixedStart = Clocks.system.nextHour().firstInstant
let fastClock = Clocks.custom(startingFrom: fixedStart, rate: 100)
fastClock
.chime(at: fastClock.thisSecond() + .seconds(10))
.sink { _ in
print("chimed at \(fastClock.now().date)")
}
.store(in: &cancels)
fastClock
.chime(at: fastClock.thisSecond() + .seconds(17))
.sink { _ in
print("chimed at \(fastClock.now().date)")
}
.store(in: &cancels)
// chimed at 2023-08-21 12:00:10 +0000
// chimed at 2023-08-21 12:00:47 +0000
The first chime works as expected, but for any value above 17 'seconds' the timing is off.
Fixed.startOfWeek
performs an O(n) search backwards to find the closest day with the proper .dayOfWeek
value. This could be short-circuited by attempting to calculate how far away the start of the week should be, jumping to it, and testing the value. Then the iterative approach could be used as a fallback.
Right now TimeDifference
provides concrete Int
properties of each unit component of a given difference, which leaves users to implement a way of getting a total of a specific unit.
For example, to find the total number of minutes between two TimePeriods (such as an event with a start/end), you need to do:
difference.hours * 60 + difference.minutes
I'd be appreciative to have an API like the following:
extension TimeDifference {
func total(_ unit: Calendar.Component) -> Double
}
let diff = start - end // hour: 1 minute: 3
print(diff.total(.minute))
// 63
print(diff.total(.hour))
// 1.05
I understand this can get very problematic at higher & lower precision, maybe a first release could just be seconds/minutes/hours?
The Absolute.approximateMidPoint
and Absolute.range
getters seem to call each other. I'm running an app which corroborates this.
In Time/5. Absolute Values/Absolute.swift:
public var range: Range<Instant> {
let date = approximateMidPoint.date <--
...
}
And in Time/5. Absolute Values/Absolute+Internal.swift:
...
internal var approximateMidPoint: Instant {
let r = self.range <--
...
}
One simply has to call Absolute(...).range
to get a hang and then a crash.
The implementations of these seem to make sense when considered independent of each other, but the fact that they depend on each other recursively is an issue.
I'm not enough of an expert yet on working with Foundation.Date
or Time
to know quite what to do to split these, but I can at least cobble together some test cases to demonstrate it.
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.