* Add an HTTP format style for both `Date` and `DateComponents`.
* Correct the comments to properly use DateComponents type
* Use default values instead of crashing when formatting date components with missing or invalid fields
* Use 59 seconds instead of 0 for leap second of 60 - this results in being off by only 1 second in this case instead of 59.
* Address some review feedback
Some app is passing in `value: -9223372036854775808` to the function below, and force unwrapping the result. It then crashes when the force unwrap fails.
```swift
/// - returns: A new date, or nil if a date could not be calculated with the given input.
public func date(byAdding component: Component, value: Int, to date: Date, wrappingComponents: Bool = false) -> Date?
```
This is caused by #1149. Prior to the change, we were using `_CalendarICU`'s implementation, where we truncate the input value to `Int32`, which becomes 0 in this case. That results in us returning the input `date` unchanged.
Now with #1149, we truthfully return `nil` because the calculation cannot be done.
We could restore the old behavior, but that implementation is incorrect. It's also clearly a wrong assumption on the client side. Add a compatibility check and restore the old behavior if needed.
Resolves 145862455
In recurrence rules without a strict matching policy, we'd sometimes come across
dates which don't exist (such as February 29, 2009). When adjusting these dates,
we were passing unadjusted date components. That resulted in adjusted dates with
the highest unspecified component different than the original date. For example,
adjusting February 29, 2009 to match components {month: 2, day: 29} would result
February 29, 2012, instead of Match 1 or February 28 of the same year.
Adjusting the date components (that is, setting the year to 2009 for the example
above) fixes this. This is what we already do in Calendar.dates(byMatching: ...)
This commit fixes the use of the `in:` bound in
Calendar.RecurrenceRule.recurrences(of:in:) to properly exclude the
upper bound as revealed by #1040.
Co-authored-by: = <=>
When filtering by weekdays, we match the dates against a set of date components.
These components happened to contain the hour, minute and second components from
the anchor date, so filtering would undo previous expansions by hour, minute and
second.
In recurrence rules that expand days or weekdays in a month, we first use a base
recurrence to calculate "anchors" in the month, and then change the day of month
or weekday to find results. Because the base recurrence used to match the day of
month of the start date, we could miss anchors if matching was set to `.strict`.
This change makes sure that if we know that the day of month is known to change,
we reset it to 1 in the base recurrence. Likewise, we reset the month in case of
leap month.
This change makes a fix to `Calendar.RecurrenceRule` with regards to leap months
and adds a few tests to verify correctness. When constructing the base sequence,
we include `isLeapMonth` of the start date in the date components. Before, start
dates falling on a leap month would have been treated the same as dates which do
fall on the non-leap month before.
The original implementation of `Calendar.RecurrenceRule` expanded recurrences of
dates using the Calendar APIs for matching date components. This would result in
multiple sequences for matching date components even when just a single sequence
would have sufficed, thus requiring more time and memory to complete enumeration
E.g: finding the dates for Thanksgivings (fourth Thursday of each November) took
~4 times as much time using RecurrenceRule when compared to simply matching date
components.
This commit optimizes how we expand dates for recurrences. Instead of creating a
sequence for each value of each component in the recurrence rule, we introduce a
new type of sequence closely resembling Calendar.DatesByMatching, but which also
allows multiple values per date component.
* Properly `throw` in case of numeric overflow in Calendar mathematics
We have applied multiple different strategies with regard to overflow during calendrical calculation, including capping input `Date` and clipping Julian day calculation with artificial bounds. Those are still insufficient as there are too many paths where numeric overflow may happen.
This patch patches those places by `throw`ing whenever applicable. Also adds fuzzing tests to cover many more Calendar API.
Resolves rdar://133558250
* Review feedback: Adopt typed throw and fix an oversight
* Update tests
`Calendar.RecurrenceRule.End` is de-facto an enum, but has been implemented as a
struct so we can more easily extend it in the future without breaking ABI. Sadly
the current API only provides a way to construct the struct, and does not let us
introspect the associated values. This change adds a couple read-only properties
to the struct to address this.
While at it, we also make the struct conform to `CustomStringConvertible`, so it
doesn't leak implementation details when it's printed in the debugger.
* Get FoundationEssentials building
Adding the missing musl imports to get FoundationEssentials building for
the Swift static SDKs again.
Also providing an option to disable building the macros. The macros
aren't necessary for building the library and will not be run as part of
the static SDK. No need to bloat the SDK or build times further. For
Swift 6, the macros should be provided by the toolchain since the
toolchain and SDK are current revlocked due to swiftmodules.
* Get FoundationInternationalization building
Adding the missing Musl imports to get FoundationInternationalization
building for the static SDK.
* Add `import WASILibc` statements to libc import chains
* Declare wasm32 arch as 32-bit environment
* Switch to _pointerBitWidth for architecture checks
This change switches the architecture checks in Data.swift to use the
_pointerBitWidth instead of the arch() checks for consistency with newer
platforms.
Gregorian calendar's implementation for `date(from: <DateComponents>)` is incorrect for a `DateComponents` when `year`, `weekOfYear`, `yearForWeekOfYear` are set. Restore the behavior of `CalendarICU`, where `yearForWeekOfYear` is preferred to `year` when both are set. This would allow the calculation to use the `weekOfYear` field, which is set in the date components, instead an unset `day` field.
Drive-by fix: give `weekdayOrdinal` a higher priority over other week fields when multiple of them are set. This isn't really an issue as this configuration is ambiguous by nature, but doing so makes it consistent with `CalendarICU`'s behavior.
Fixes 130203724
* Use dynamic replacement instead of _typeByName for internationalization upcalls
* Make FOUNDATION_FRAMEWORK function non-dynamic
* Fix build failures
This adds the necessary guards and includes for the Android modules.
While the module does not compile currently due to nullability
differences (and in some cases missing declarations), this at least
brings the module to a point where we can start working on the errors
and differences to create a maintainable codebase for Android.
Validate the given `DateComponents` values up front to align with the supported Calendar calculation date range, defined as `Date.validCalendarRange`.
We could alternatively guard all arithmetic operations with `...reportingOverflow`, but there are too many operations, and so it seems untenable. I opted for a more realistic approach instead. `_CalendarICU` unconditionally truncates values to `Int32`, so the results for `Calendar.date(from:)` have always been incorrect for distant dates anyways.
Fixed 129782208
This commit implements `Calendar.RecurrenceRule.recurrences(of:in:)` as pitched
in #422.
This type models a subset of RRULE as specified in RFC-5545, section 3.3.10. One
notable difference is that it doesn't support a frequency of "secondly", as that
was not part of the original proposal. It also implements RFC-7529 Non-Gregorian
Recurrence Rules and works with any instance of `Calendar`.
Recurrences are calculated according to our interpretation of the RFCs. As such,
the resulting dates may differ in certain edge cases when compared to other open
source implementations.
* Add components in the order of granularity in `date(byAdding:to:)`
We compute the differences in Calendar component values from the largest to the smallest in `dateComponents(_:from:to:)`, but we are not following the same order in `date(byAdding:to:)`. Change the order of the latter so we add the largest components first.
The results are the same for most cases regardless of the order. The only exception is when the addition moves the date across DST transition. The reason for this is that the implementation aims to maintain the clock time when adding units that are larger than `day` to a date, so that the time in the day remains unchanged even after time zone offset changes. However, we cannot hold this promise if the result lands in the "skipped hour" on the DST start date as that time does not actually exist. To adjust for this case, we adjust the time of the day to the correct timezone.
For example, the DST start of year 2024 in the Los Angeles time zone is 2024-03-10, where 02:00:00-0800 becomes 03:00:00-0700.
Here's what happens when we add 1 day first, then add 1 week, to 2024-03-09T02:34:36-0800, the day before DST start:
We start by adding 1 day:
2024-03-09T02:34:36-0800 + 1 day --> 2024-03-10T02:34:36-0800
This is not a valid date in this time zone, so we adjust it to -0700:
2024-03-10T02:34:36-0800 --> 2024-03-10T03:34:36-0700
Then we add a week, which is 7 days:
2024-03-10T03:34:36-0700 + 7 days -> 2024-03-17T03:34:36-0700
Notice that the local time is different in the result from that of the original, which changes from 02:34 to 03:34.
If we start by adding 1 week:
2024-03-09T02:34:36-0800 + 7 days -> 2024-03-16T02:34:36-0700
Then add a day:
2024-03-16T02:34:36-0700 + 1 day -> 2024-03-17T02:34:36-0700
The local time is the same as that of the original.
We go for the latter to allow round-tripping a date to the date by adding their differences back.
Resolves 124414807
* Update test and remove unreasonable test cases
Currently there are tests to verify the behavior of adding `DateComponents` that sets both `weekOfYear` and `weekOfMonth`. This configuration is ambiguous, and even more so when `wrapping` is `true` since the results vary depending on which component is added first.
Replace tests asserting the behavior of this unreasonable `DateComponents` with those that only uses one kind of `week` component.
* Review feedback
* Address review feedback
* Review feedback: add back a test case with updated expectation and change the test format
This allows us to get further into building FoundationEssentials once
again on Windows. Much of the file system work has resulted in this
module no longer being viable on Windows and will need to be replaced to
allow building on Windows which does not have the `fts` APIs.
* Provide public access for some internal functions, to enable swift-corelibs-foundation
* Add access to TimeZone internals for swift-corelibs-foundation
* Fix default TimeZone for Linux
* Remove unneeded private entry point
* Do not use a recursive definition of description for String.Encoding
* Merge in some WASI changes and other Data fixes
* Add temporary initializer to the stub URL
* Remove Hashable conformance for CocoaError. This allows userInfo to be Any instead of AnyHashable
* Remove some protocols which depend on NSError from swift-foundation -- they will live in swift-corelibs-foundation
* Adjust the debug description of the GMT ICU calendar to be a little less implementation-specific
* Use an English-only description for string encodings, for compatibility with existing SCL-F clients
* Use a more compatible definition of a backstop value for Bundle
When deciding the component values when calculating `Date` given a `DateComponent`, we incorrectly rewind to the previous year if `weekOfYear == 1` regardless of the passed-in `month`. This adjustment is incorrect and unncessary when `month == 1` -- being in the first month already presumes the start of the year, so doing so results in over-adjustment.
Fix this by skipping this adjustment if we're already in the first month.
Resolves 124661497
CalendarGregorian currently decomposes `Locale` into its identifer plus preferences, stores the two properties separately, and recreates everytime a `Locale` when needed. This results in us always creating new locales, and calling ICU whenever requesting its properties. While we do have cache in place for `Locale.init(identifier:preferences:)` for nil `preferences`, we are not hitting the cache because `preferences` is always non-nil when calendar is `.current`.
Fix this by storing a `Locale` inside calendar so we always hit the cache. This also allows `Locale.autoupdatingCurrent` to work properly -- previously you would not get the right locale back if you set `calendar.locale = Locale.autoupdatingCurrent`. This change fixes that as the identity of the `Locale` is now preserved.
Resolves 125169335
This commit adds the new `Calendar.RecurrenceRule` struct that was introduced in
SF-0009. It doesn't implement enumeration of recurrences, that will be done in a
separate commit. Resolves rdar://120559017.
When adding month or year to a `Date`, we convert a `Date` to a `DateComponent`, modify the component, and convert the modified `DateComponent` back to `Date`. However for dates falling into the "repeated" time frame on DST end date, we do have have the information of which time the `DateComponent` represents when converting it back to `Date`.
This has been the design, and we have always assumed the second occurrence of the same time. The only implementation difference is that in the ICU implementation, we return early when the value is 0 and do not proceed onto adding. We should do that for GregorianCalendar too.
Resolves 124231654
"First day of week" and "minimum days in first week" are locale specific. Move the logic of fetching the locale's preferred value into `LocaleProtocol`, so that calendar can return the value from its enclosed locale.
Resolves 123630636
When checking if a date matches components, we were checking whether isLeapMonth
has been set, but were not reading its actual value. This meant that this method
would sometimes return wrong results in calendars that have leap months.
We triggered a Swift runtime error when converting a `Double` to an `Int` because the value exceeds the value representable by an `Int`.
This patch works-around this issue with the following:
- Return a capped value if the date exceeds the bound before conversion.
- Ensure compatibility among various `_CalendarProtocol` observers by capping the input date upfront.
Resolves 123664232
Guard against infinite loop by validating each intermittent result. This is similar to what we're doing in Calendar enumeration functions.
Resolves 123660148
- Fix precision issues around nanoseconds calculation.
Update test expectations.
- Lessen the chance of overflowing `func add(:)` by using `Double`
We eventually convert to Double before return anyways.
- Disable TestDateComponentsDiscreteConformance.testRandomSamples temporarily for 32-bit platforms
This test triggers code path that has wrong assumption on Calendar API's return value, and overflows when the input dates are distant from each other. Disable it for now. Will track the fix in 123262305
Resolves 121677598
* CalendarGregorian search functions may loop indefinitely when reaching the capped date
We are capping the input `Date` to the internal upper bound in various internal functions. This caused the incremental search functions to loop indefinitely because the search results are always the same since the effective input never changes.
Remove the capping from internal functions entirely. The capping was added to improving performance when calling into ICU. We should instead move the capping up to Calendar.swift, the call site to reach consistent behavior, but not inside the implementation.
Also fix miscellaneous other problems found in testing
Resolves 122946218
* enable compatibility tests for CI to run
* Address review feedback
* Remove incorrect assumption -- we should not store locale's prefs as the calendar's custom first day because it may be copied accidentally