Giter Site home page Giter Site logo

davedelong / time Goto Github PK

View Code? Open in Web Editor NEW
2.3K 2.3K 78.0 680 KB

Robust and type-safe date and time calculations for Swift

License: MIT License

Swift 100.00%
calendar calendar-api calendars date date-formatting date-time dates datetime swift swift-library time timezone

time's People

Contributors

bigzaphod avatar danielctull avatar davedelong avatar elitalon avatar hatunike avatar iainsmith avatar ikenndac avatar jobinsjohn avatar junebash avatar mordil avatar sebj avatar tapsandswipes avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

time's Issues

Better support for weeks

Desired functionality:

  • given a Fixed<LTOEDay>, get the week (Array<Fixed<Day>>) that contains it
  • given a Fixed<GTOEMonth>, get an array of weeks in it, with an option to pad out week arrays so they're all 7 days
  • get the last day of a week

convert(to timeZone:) "feels wrong" for Absolute<Day>

convert(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?

Confirm `Locale.Weekday` conversion on ISO8601 calendars

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?

Bless You !

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.

Make types conform to Sendable when possible

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!

API: Change all error types to be structs

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.

Feature: Clock "chimes"

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:

  • starting from a provided Value, notify me every time this Difference has elapsed
  • starting from now, notify me every time this SISecond/Difference has elapsed
  • starting from now, notify me every time a value matching these DateComponents occurs

There will be some interesting challenges when it comes to clocks that scale time (ie, run slower or faster than real time).

Custom Formatting

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?

All zero-argument methods that return a value should be computed properties instead

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.

Precision tighter than milliseconds is problematic

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.

Improving rounding performance

Fixed.roundToMultiple(of:direction:) could be optimized by using modulo arithmetic to try and 'jump' to the rounded values

How to ask whether an event is more than n minutes ago?

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?

Make Template.template public

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.

Support floating values

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

Usage of "Unsafe"

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"

CocoaPods support

I’m still using CocoaPods mainly because of automatic Settings bundle generation.

Happy to make a PR adding a Podspec if you’re interested?

Initializing an Absolute<U> without an explicit era always throws

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! 😄

Perceived time difference

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.

Stack overflow (?) when calling `firstInstant`

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:

Screen Shot 2020-03-22 at 6 18 09 PM

(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.

Slow initialisation

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.

Screenshot 2020-06-23 at 13 13 11

Examples: iOS App

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.

Circular reference

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.

Infinite loop in `Absolute.range`

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:

  1. Is this the right way to solve my issue?
  2. Is this infinite loop a known bug?

Thanks in advance.

Carthage supports

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.

Allen's interval algebra

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. :-)

Tests

This seems fairly important.

Getting a Foundation `Date` from `Value`

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!

Typo in BasicUsage

The Reacting to Changing Time section should use .asyncValues, not .asyncValue

Fixed.description should conditionally omit time zone

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>.

Force formatting order

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.

Storing and retrieving future dates and times

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.!

Inaccurate timing with a very fast clock

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.

Improve Fixed.startOfWeek performance

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.

Add total unit values to TimeDifference

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?

Infinite recursion between Absolute.range and Absolute.approximateMidPoint

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.