diff --git a/Examples/Examples/Calculator/CalcCalendarView.swift b/Examples/Examples/Calculator/CalcCalendarView.swift index 12433cb..a822200 100644 --- a/Examples/Examples/Calculator/CalcCalendarView.swift +++ b/Examples/Examples/Calculator/CalcCalendarView.swift @@ -9,41 +9,51 @@ import SwiftUI import Time struct CalcCalendarView: View { - @Binding var selectedSecond : Fixed - - var selectedDay: Binding> { - return Binding(get: { - selectedSecond.fixedDay - }, set: { newValue in - do { - selectedSecond = try newValue.setting(hour: selectedSecond.hour, - minute: selectedSecond.minute, - second: selectedSecond.second) - } catch { - print("Error: \(selectedSecond.format(time: .long)) does not exist on \(newValue.format(date: .long))") - } + @Binding var selectedSecond: Fixed + + var selectedDay: Binding> { + return Binding( + get: { + selectedSecond.fixedDay + }, + set: { newValue in + do { + selectedSecond = try newValue.setting( + hour: selectedSecond.hour, + minute: selectedSecond.minute, + second: selectedSecond.second) + } catch { + print( + "Error: \(selectedSecond.format(time: .long)) does not exist on \(newValue.format(date: .long))" + ) + } + }) + } + + var body: some View { + VStack { + DayPicker( + selection: selectedDay, consistentNumberOfWeeks: true, + label: { + YearMonthPicker(month: $0) }) - } - - var body: some View { - VStack { - DayPicker(selection: selectedDay, consistentNumberOfWeeks: true, label: { - YearMonthPicker(month: $0) - }) - - HStack { - TimePicker(selection: $selectedSecond) - Button("Now") { - let now = Clocks.system.currentSecond - do { - selectedSecond = try selectedSecond.setting(hour: now.hour, - minute: now.minute, - second: now.second) - } catch { - print("The time \(now.format(time: .long)) does not exist on \(selectedSecond.format(date: .long))") - } - } - } + + HStack { + TimePicker(selection: $selectedSecond) + Button("Now") { + let now = Clocks.system.currentSecond + do { + selectedSecond = try selectedSecond.setting( + hour: now.hour, + minute: now.minute, + second: now.second) + } catch { + print( + "The time \(now.format(time: .long)) does not exist on \(selectedSecond.format(date: .long))" + ) + } } + } } + } } diff --git a/Examples/Examples/Calculator/CalcDifferencesView.swift b/Examples/Examples/Calculator/CalcDifferencesView.swift index 2fdacb7..e594f45 100644 --- a/Examples/Examples/Calculator/CalcDifferencesView.swift +++ b/Examples/Examples/Calculator/CalcDifferencesView.swift @@ -9,115 +9,115 @@ import SwiftUI import Time struct CalcDifferencesView: View { - @Binding var startSecond : Fixed - @Binding var endSecond : Fixed - - @State var wholeDifference = true - - @State var yearDifference = 0 - @State var monthDifference = 0 - @State var dayDifference = 0 - @State var hourDifference = 0 - @State var minuteDifference = 0 - @State var secondDifference = 0 - - private var conjunction: String { wholeDifference ? "or" : "and" } - - var body: some View { - GroupBox("Differences") { - VStack { - Text("\(startSecond.description)") - .font(.headline) - - Text("to") - .font(.caption) - - Text("\(endSecond.description)") - .font(.headline) - - HStack { - Stepper("^[\(yearDifference) year](inflect: true)") { - endSecond = endSecond.nextYear - } onDecrement: { - endSecond = endSecond.previousYear - } - - Picker("Diffence kind", selection: $wholeDifference) { - Text("or").tag(true) - Text("and").tag(false) - } - .pickerStyle(.segmented) - .fixedSize() - .labelsHidden() - - Stepper("^[\(monthDifference) month](inflect: true)") { - endSecond = endSecond.nextMonth - } onDecrement: { - endSecond = endSecond.previousMonth - } - - Text(conjunction) - - Stepper("^[\(dayDifference) day](inflect: true)") { - endSecond = endSecond.nextDay - } onDecrement: { - endSecond = endSecond.previousDay - } - } - .padding(.top, 12) - - HStack { - Text(conjunction) - - Stepper("^[\(hourDifference) hour](inflect: true)") { - endSecond = endSecond.nextHour - } onDecrement: { - endSecond = endSecond.previousHour - } - - Text(conjunction) - - Stepper("^[\(minuteDifference) minute](inflect: true)") { - endSecond = endSecond.nextMinute - } onDecrement: { - endSecond = endSecond.previousMinute - } - - Text(conjunction) - - Stepper("^[\(secondDifference) second](inflect: true)") { - endSecond = endSecond.nextSecond - } onDecrement: { - endSecond = endSecond.previousSecond - } - } - .padding(.top, 12) - } - .frame(maxWidth: .infinity, alignment: .center) + @Binding var startSecond: Fixed + @Binding var endSecond: Fixed + + @State var wholeDifference = true + + @State var yearDifference = 0 + @State var monthDifference = 0 + @State var dayDifference = 0 + @State var hourDifference = 0 + @State var minuteDifference = 0 + @State var secondDifference = 0 + + private var conjunction: String { wholeDifference ? "or" : "and" } + + var body: some View { + GroupBox("Differences") { + VStack { + Text("\(startSecond.description)") + .font(.headline) + + Text("to") + .font(.caption) + + Text("\(endSecond.description)") + .font(.headline) + + HStack { + Stepper("^[\(yearDifference) year](inflect: true)") { + endSecond = endSecond.nextYear + } onDecrement: { + endSecond = endSecond.previousYear + } + + Picker("Diffence kind", selection: $wholeDifference) { + Text("or").tag(true) + Text("and").tag(false) + } + .pickerStyle(.segmented) + .fixedSize() + .labelsHidden() + + Stepper("^[\(monthDifference) month](inflect: true)") { + endSecond = endSecond.nextMonth + } onDecrement: { + endSecond = endSecond.previousMonth + } + + Text(conjunction) + + Stepper("^[\(dayDifference) day](inflect: true)") { + endSecond = endSecond.nextDay + } onDecrement: { + endSecond = endSecond.previousDay + } } - .onAppear { recompute() } - .onChange(of: startSecond) { _ in recompute() } - .onChange(of: endSecond) { _ in recompute() } - .onChange(of: wholeDifference) { _ in recompute() } - } - - private func recompute() { - if wholeDifference { - yearDifference = startSecond.differenceInWholeYears(to: endSecond).years - monthDifference = startSecond.differenceInWholeMonths(to: endSecond).months - dayDifference = startSecond.differenceInWholeDays(to: endSecond).days - hourDifference = startSecond.differenceInWholeHours(to: endSecond).hours - minuteDifference = startSecond.differenceInWholeMinutes(to: endSecond).minutes - secondDifference = startSecond.differenceInWholeSeconds(to: endSecond).seconds - } else { - let difference = startSecond.difference(to: endSecond) - - yearDifference = difference.years - monthDifference = difference.months - dayDifference = difference.days - hourDifference = difference.hours - minuteDifference = difference.minutes - secondDifference = difference.seconds + .padding(.top, 12) + + HStack { + Text(conjunction) + + Stepper("^[\(hourDifference) hour](inflect: true)") { + endSecond = endSecond.nextHour + } onDecrement: { + endSecond = endSecond.previousHour + } + + Text(conjunction) + + Stepper("^[\(minuteDifference) minute](inflect: true)") { + endSecond = endSecond.nextMinute + } onDecrement: { + endSecond = endSecond.previousMinute + } + + Text(conjunction) + + Stepper("^[\(secondDifference) second](inflect: true)") { + endSecond = endSecond.nextSecond + } onDecrement: { + endSecond = endSecond.previousSecond + } } + .padding(.top, 12) + } + .frame(maxWidth: .infinity, alignment: .center) + } + .onAppear { recompute() } + .onChange(of: startSecond) { _ in recompute() } + .onChange(of: endSecond) { _ in recompute() } + .onChange(of: wholeDifference) { _ in recompute() } + } + + private func recompute() { + if wholeDifference { + yearDifference = startSecond.differenceInWholeYears(to: endSecond).years + monthDifference = startSecond.differenceInWholeMonths(to: endSecond).months + dayDifference = startSecond.differenceInWholeDays(to: endSecond).days + hourDifference = startSecond.differenceInWholeHours(to: endSecond).hours + minuteDifference = startSecond.differenceInWholeMinutes(to: endSecond).minutes + secondDifference = startSecond.differenceInWholeSeconds(to: endSecond).seconds + } else { + let difference = startSecond.difference(to: endSecond) + + yearDifference = difference.years + monthDifference = difference.months + dayDifference = difference.days + hourDifference = difference.hours + minuteDifference = difference.minutes + secondDifference = difference.seconds } + } } diff --git a/Examples/Examples/Calculator/CalculatorView.swift b/Examples/Examples/Calculator/CalculatorView.swift index 61b042b..eeb9c1b 100644 --- a/Examples/Examples/Calculator/CalculatorView.swift +++ b/Examples/Examples/Calculator/CalculatorView.swift @@ -6,35 +6,35 @@ // import SwiftUI - import Time struct CalculatorView: View { - @State var startSecond = Clocks.system.currentSecond - @State var endSecond = Clocks.system.currentSecond + .days(1) - - var body: some View { - VStack(alignment: .center, spacing: 12) { - - HStack { - GroupBox("From") { - CalcCalendarView(selectedSecond: $startSecond) - .frame(maxWidth: .infinity) - } - - Image(systemName: "arrow.forward") - - GroupBox("To") { - CalcCalendarView(selectedSecond: $endSecond) - .frame(maxWidth: .infinity) - } - } - - CalcDifferencesView(startSecond: $startSecond, - endSecond: $endSecond) - - Spacer() + @State var startSecond = Clocks.system.currentSecond + @State var endSecond = Clocks.system.currentSecond + .days(1) + + var body: some View { + VStack(alignment: .center, spacing: 12) { + + HStack { + GroupBox("From") { + CalcCalendarView(selectedSecond: $startSecond) + .frame(maxWidth: .infinity) + } + + Image(systemName: "arrow.forward") + + GroupBox("To") { + CalcCalendarView(selectedSecond: $endSecond) + .frame(maxWidth: .infinity) } - .padding() + } + + CalcDifferencesView( + startSecond: $startSecond, + endSecond: $endSecond) + + Spacer() } + .padding() + } } diff --git a/Examples/Examples/CalendarView.swift b/Examples/Examples/CalendarView.swift index f6f3527..82bdd9b 100644 --- a/Examples/Examples/CalendarView.swift +++ b/Examples/Examples/CalendarView.swift @@ -8,25 +8,26 @@ import SwiftUI import Time struct CalendarView: View { - - @State var currentMonth = Clocks.system.currentMonth - @State var selectedDay: Fixed? - - @State var consistentNumberOfWeeks = false - - var body: some View { - VStack { - Toggle("Show the same number of weeks each month", isOn: $consistentNumberOfWeeks) - - DayPicker(selection: $selectedDay, - consistentNumberOfWeeks: consistentNumberOfWeeks) - - Divider() - - Text("The currently selected day is \(selectedDay?.format(date: .long) ?? "none")") - - Spacer() - } - + + @State var currentMonth = Clocks.system.currentMonth + @State var selectedDay: Fixed? + + @State var consistentNumberOfWeeks = false + + var body: some View { + VStack { + Toggle("Show the same number of weeks each month", isOn: $consistentNumberOfWeeks) + + DayPicker( + selection: $selectedDay, + consistentNumberOfWeeks: consistentNumberOfWeeks) + + Divider() + + Text("The currently selected day is \(selectedDay?.format(date: .long) ?? "none")") + + Spacer() } + + } } diff --git a/Examples/Examples/ClocksView.swift b/Examples/Examples/ClocksView.swift index 649dd48..e3d651f 100644 --- a/Examples/Examples/ClocksView.swift +++ b/Examples/Examples/ClocksView.swift @@ -8,111 +8,114 @@ import SwiftUI import Time struct ClocksView: View { - - @State var calendar = Calendar.current - @State var timeZone = TimeZone.current - @State var locale = Locale.current - - @State var now: Fixed? - - var clock: any RegionalClock { - Clocks.system(in: Region(calendar: calendar, timeZone: timeZone, locale: locale)) - } - - var body: some View { - VStack(alignment: .center) { - if let now { - Text(now.format(date: .full)) - .font(.title) - - Text(now.format(time: .long)) - .font(.title) - - Divider() - } - - Form { - CalendarPicker(calendar: $calendar) - TimeZonePicker(timeZone: $timeZone) - LocalePicker(locale: $locale) - } - - Spacer() - } - .onReceive(clock.strike(every: Second.self).publisher.receive(on: DispatchQueue.main), perform: { now = $0 }) + + @State var calendar = Calendar.current + @State var timeZone = TimeZone.current + @State var locale = Locale.current + + @State var now: Fixed? + + var clock: any RegionalClock { + Clocks.system(in: Region(calendar: calendar, timeZone: timeZone, locale: locale)) + } + + var body: some View { + VStack(alignment: .center) { + if let now { + Text(now.format(date: .full)) + .font(.title) + + Text(now.format(time: .long)) + .font(.title) + + Divider() + } + + Form { + CalendarPicker(calendar: $calendar) + TimeZonePicker(timeZone: $timeZone) + LocalePicker(locale: $locale) + } + + Spacer() } - + .onReceive( + clock.strike(every: Second.self).publisher.receive(on: DispatchQueue.main), + perform: { now = $0 }) + } + } struct CalendarPicker: View { - @Binding var calendar: Calendar - - let ids = [Calendar.Identifier.gregorian, - .buddhist, - .chinese, - .coptic, - .ethiopicAmeteMihret, - .ethiopicAmeteAlem, - .hebrew, - .iso8601, - .indian, - .islamic, - .islamicCivil, - .japanese, - .persian, - .republicOfChina, - .islamicTabular, - .islamicUmmAlQura - ] - - var body: some View { - Picker("Calendar", selection: $calendar) { - Text("System (\(Calendar.current.identifier.debugDescription))") - .tag(Calendar.current) - - ForEach(ids, id: \.self) { calendarID in - Text("\(calendarID.debugDescription)") - .tag(Calendar(identifier: calendarID)) - } - } + @Binding var calendar: Calendar + + let ids = [ + Calendar.Identifier.gregorian, + .buddhist, + .chinese, + .coptic, + .ethiopicAmeteMihret, + .ethiopicAmeteAlem, + .hebrew, + .iso8601, + .indian, + .islamic, + .islamicCivil, + .japanese, + .persian, + .republicOfChina, + .islamicTabular, + .islamicUmmAlQura, + ] + + var body: some View { + Picker("Calendar", selection: $calendar) { + Text("System (\(Calendar.current.identifier.debugDescription))") + .tag(Calendar.current) + + ForEach(ids, id: \.self) { calendarID in + Text("\(calendarID.debugDescription)") + .tag(Calendar(identifier: calendarID)) + } } + } } struct TimeZonePicker: View { - - @Binding var timeZone: TimeZone - - var body: some View { - - Picker("Time Zone", selection: $timeZone) { - Text("System (\(TimeZone.current.description))") - .tag(TimeZone.current) - - ForEach(TimeZone.knownTimeZoneIdentifiers.sorted(by: <), id: \.self) { id in - Text(id) - .tag(TimeZone(identifier: id)!) - } - } - + + @Binding var timeZone: TimeZone + + var body: some View { + + Picker("Time Zone", selection: $timeZone) { + Text("System (\(TimeZone.current.description))") + .tag(TimeZone.current) + + ForEach(TimeZone.knownTimeZoneIdentifiers.sorted(by: <), id: \.self) { id in + Text(id) + .tag(TimeZone(identifier: id)!) + } } - + + } + } struct LocalePicker: View { - - @Binding var locale: Locale - - var body: some View { - - Picker("Locale", selection: $locale) { - Text("System (\(Locale.current.description))") - .tag(Locale.current) - - ForEach(Locale.availableIdentifiers.sorted(by: <), id: \.self) { id in - Text(Locale.current.localizedString(forIdentifier: id) ?? id) - .tag(Locale(identifier: id)) - } - } - + + @Binding var locale: Locale + + var body: some View { + + Picker("Locale", selection: $locale) { + Text("System (\(Locale.current.description))") + .tag(Locale.current) + + ForEach(Locale.availableIdentifiers.sorted(by: <), id: \.self) { id in + Text(Locale.current.localizedString(forIdentifier: id) ?? id) + .tag(Locale(identifier: id)) + } } + + } } diff --git a/Examples/Examples/Components/DayPicker.swift b/Examples/Examples/Components/DayPicker.swift index a331fd5..8a066e0 100644 --- a/Examples/Examples/Components/DayPicker.swift +++ b/Examples/Examples/Components/DayPicker.swift @@ -10,177 +10,186 @@ import SwiftUI import Time struct DayPicker: View { - - @Binding var selection: Fixed? - - var consistentNumberOfWeeks: Bool - - var label: (Binding>) -> Label - - @State private var currentMonth: Fixed = Clocks.system.currentMonth - - init(selection: Binding>, consistentNumberOfWeeks: Bool = true, @ViewBuilder label: @escaping (Binding>) -> Label) { - self._selection = Binding(get: { selection.wrappedValue }, - set: { newValue in - guard let newValue else { return } - selection.wrappedValue = newValue - }) - self.consistentNumberOfWeeks = consistentNumberOfWeeks - self.label = label + + @Binding var selection: Fixed? + + var consistentNumberOfWeeks: Bool + + var label: (Binding>) -> Label + + @State private var currentMonth: Fixed = Clocks.system.currentMonth + + init( + selection: Binding>, consistentNumberOfWeeks: Bool = true, + @ViewBuilder label: @escaping (Binding>) -> Label + ) { + self._selection = Binding( + get: { selection.wrappedValue }, + set: { newValue in + guard let newValue else { return } + selection.wrappedValue = newValue + }) + self.consistentNumberOfWeeks = consistentNumberOfWeeks + self.label = label + } + + init( + selection: Binding?>, consistentNumberOfWeeks: Bool = true, + @ViewBuilder label: @escaping (Binding>) -> Label + ) { + self._selection = selection + self.consistentNumberOfWeeks = consistentNumberOfWeeks + self.label = label + } + + init(selection: Binding?>, consistentNumberOfWeeks: Bool = true) + where Label == DefaultFixedDayTitle { + self._selection = selection + self.consistentNumberOfWeeks = consistentNumberOfWeeks + self.label = { DefaultFixedDayTitle(month: $0) } + } + + private var weeksForCurrentMonth: [[Fixed]] { + var allDays = Array(currentMonth.days) + + // pad out the front of the array with any additional days + while allDays[0].dayOfWeek != currentMonth.calendar.firstWeekday { + allDays.insert(allDays[0].previous, at: 0) } - - init(selection: Binding?>, consistentNumberOfWeeks: Bool = true, @ViewBuilder label: @escaping (Binding>) -> Label) { - self._selection = selection - self.consistentNumberOfWeeks = consistentNumberOfWeeks - self.label = label + + if consistentNumberOfWeeks { + // Apple Calendar shows 6 weeks at a time, so all views have the same vertical height + // this eliminates complexity around dynamically resizing the month view + while allDays.count < 42 { + allDays.append(allDays.last!.next) + } + } else { + repeat { + let proposedNextDay = allDays.last!.next + if proposedNextDay.dayOfWeek != currentMonth.calendar.firstWeekday { + allDays.append(proposedNextDay) + } else { + break + } + } while true } - - init(selection: Binding?>, consistentNumberOfWeeks: Bool = true) where Label == DefaultFixedDayTitle { - self._selection = selection - self.consistentNumberOfWeeks = consistentNumberOfWeeks - self.label = { DefaultFixedDayTitle(month: $0) } + + // all supported calendars have weeks of seven days + assert(allDays.count.isMultiple(of: 7)) + + // slice the array into groups of seven + let numberOfWeeks = allDays.count / 7 + + return (0..]> { - var allDays = Array(currentMonth.days) - - // pad out the front of the array with any additional days - while allDays[0].dayOfWeek != currentMonth.calendar.firstWeekday { - allDays.insert(allDays[0].previous, at: 0) + } + + var body: some View { + let weeks = self.weeksForCurrentMonth + + return VStack { + // current month + movement controls + HStack { + label($currentMonth) + + Spacer() + .layoutPriority(-1) + + Button(action: { currentMonth = currentMonth.previous }) { + Image(systemName: "arrowtriangle.backward.fill") } - - if consistentNumberOfWeeks { - // Apple Calendar shows 6 weeks at a time, so all views have the same vertical height - // this eliminates complexity around dynamically resizing the month view - while allDays.count < 42 { - allDays.append(allDays.last!.next) - } - } else { - repeat { - let proposedNextDay = allDays.last!.next - if proposedNextDay.dayOfWeek != currentMonth.calendar.firstWeekday { - allDays.append(proposedNextDay) - } else { - break - } - } while true + + Button(action: { + currentMonth = Clocks.system.currentMonth + selection = Clocks.system.currentDay + }) { + Text("Today") } - - // all supported calendars have weeks of seven days - assert(allDays.count.isMultiple(of: 7)) - - // slice the array into groups of seven - let numberOfWeeks = allDays.count / 7 - - return (0 ..< numberOfWeeks).map { weekNumber in - let dayRange = (weekNumber * 7) ..< ((weekNumber + 1) * 7) - return Array(allDays[dayRange]) + + Button(action: { currentMonth = currentMonth.next }) { + Image(systemName: "arrowtriangle.forward.fill") } - } - - var body: some View { - let weeks = self.weeksForCurrentMonth - - return VStack { - // current month + movement controls - HStack { - label($currentMonth) - - Spacer() - .layoutPriority(-1) - - Button(action: { currentMonth = currentMonth.previous }) { - Image(systemName: "arrowtriangle.backward.fill") - } - - Button(action: { - currentMonth = Clocks.system.currentMonth - selection = Clocks.system.currentDay - }) { - Text("Today") - } - - Button(action: { currentMonth = currentMonth.next }) { - Image(systemName: "arrowtriangle.forward.fill") - } - } - .font(.headline) - - Grid(alignment: .centerFirstTextBaseline, horizontalSpacing: 2, verticalSpacing: 2) { - GridRow { - ForEach(weeks[0], id: \.self) { day in - Text(day.format(weekday: .abbreviatedName)) - .fixedSize() // prevent the text from wrapping - } - .font(.subheadline) - } - - Divider() - .gridCellUnsizedAxes(.horizontal) - - ForEach(weeks, id: \.self) { week in - GridRow { - ForEach(week, id: \.self) { day in - toggle(for: day) - } - } - } + } + .font(.headline) + + Grid(alignment: .centerFirstTextBaseline, horizontalSpacing: 2, verticalSpacing: 2) { + GridRow { + ForEach(weeks[0], id: \.self) { day in + Text(day.format(weekday: .abbreviatedName)) + .fixedSize() // prevent the text from wrapping + } + .font(.subheadline) + } + + Divider() + .gridCellUnsizedAxes(.horizontal) + + ForEach(weeks, id: \.self) { week in + GridRow { + ForEach(week, id: \.self) { day in + toggle(for: day) } -// .aspectRatio(7.0 / Double(weeks.count + 1), contentMode: .fit) + } } - .padding() + } + // .aspectRatio(7.0 / Double(weeks.count + 1), contentMode: .fit) } - - private func toggle(for day: Fixed) -> some View { - let isOn = Binding(get: { selection == day }, - set: { on in - if on { - selection = day - currentMonth = day.fixedMonth - } else { - selection = nil - } - }) - - return Toggle(isOn: isOn) { - Text(day.format(day: .naturalDigits)) - .fixedSize() // prevent the text from wrapping - .monospacedDigit() - .opacity(day.month == currentMonth.month ? 1.0 : 0.5) - .padding(2) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .aspectRatio(1.0, contentMode: .fit) + .padding() + } + + private func toggle(for day: Fixed) -> some View { + let isOn = Binding( + get: { selection == day }, + set: { on in + if on { + selection = day + currentMonth = day.fixedMonth + } else { + selection = nil } - .toggleStyle(DayToggleStyle()) + }) + + return Toggle(isOn: isOn) { + Text(day.format(day: .naturalDigits)) + .fixedSize() // prevent the text from wrapping + .monospacedDigit() + .opacity(day.month == currentMonth.month ? 1.0 : 0.5) + .padding(2) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .aspectRatio(1.0, contentMode: .fit) } + .toggleStyle(DayToggleStyle()) + } } struct DefaultFixedDayTitle: View { - - @Binding var month: Fixed - - var body: some View { - Text(month.format(year: .naturalDigits, month: .naturalName)) - .fixedSize() - } - + + @Binding var month: Fixed + + var body: some View { + Text(month.format(year: .naturalDigits, month: .naturalName)) + .fixedSize() + } + } struct DayToggleStyle: ToggleStyle { - - func makeBody(configuration: Configuration) -> some View { - Button(action: { configuration.$isOn.wrappedValue = !configuration.isOn }) { - configuration.label - .foregroundStyle(configuration.isOn ? AnyShapeStyle(.selection) : AnyShapeStyle(.primary)) - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .background { - if configuration.isOn { - RoundedRectangle(cornerRadius: 4) - .fill(Color.accentColor) - } - } + + func makeBody(configuration: Configuration) -> some View { + Button(action: { configuration.$isOn.wrappedValue = !configuration.isOn }) { + configuration.label + .foregroundStyle(configuration.isOn ? AnyShapeStyle(.selection) : AnyShapeStyle(.primary)) + .contentShape(Rectangle()) } - + .buttonStyle(.plain) + .background { + if configuration.isOn { + RoundedRectangle(cornerRadius: 4) + .fill(Color.accentColor) + } + } + } + } diff --git a/Examples/Examples/Components/MonthPicker.swift b/Examples/Examples/Components/MonthPicker.swift index 74b747f..157d9a6 100644 --- a/Examples/Examples/Components/MonthPicker.swift +++ b/Examples/Examples/Components/MonthPicker.swift @@ -10,26 +10,26 @@ import SwiftUI import Time struct YearMonthPicker: View { - @Binding var month: Fixed - - var body: some View { - HStack { - Picker("Month", selection: $month) { - ForEach(Array(month.fixedYear.months), id: \.self) { month in - Text(month.format(month: .naturalName)) - .tag(month) - } - } - .labelsHidden() - - Stepper { - Text(month.format(year: .naturalDigits)) - } onIncrement: { - month = month.nextYear - } onDecrement: { - month = month.previousYear - } + @Binding var month: Fixed + + var body: some View { + HStack { + Picker("Month", selection: $month) { + ForEach(Array(month.fixedYear.months), id: \.self) { month in + Text(month.format(month: .naturalName)) + .tag(month) } - .fixedSize() + } + .labelsHidden() + + Stepper { + Text(month.format(year: .naturalDigits)) + } onIncrement: { + month = month.nextYear + } onDecrement: { + month = month.previousYear + } } + .fixedSize() + } } diff --git a/Examples/Examples/Components/TimePicker.swift b/Examples/Examples/Components/TimePicker.swift index edb691e..f44ae6f 100644 --- a/Examples/Examples/Components/TimePicker.swift +++ b/Examples/Examples/Components/TimePicker.swift @@ -10,37 +10,37 @@ import SwiftUI import Time struct TimePicker: View { - - @Binding var selection: Fixed - - var body: some View { - HStack { - Stepper(selection.format(hour: .naturalDigits(dayPeriod: .none))) { - selection = selection.nextHour - } onDecrement: { - selection = selection.previousHour - } - - Text(":") - - Stepper(selection.format(minute: .twoDigits)) { - selection = selection.nextMinute - } onDecrement: { - selection = selection.previousMinute - } - - Text(":") - - Stepper(selection.format(second: .twoDigits)) { - selection = selection.nextSecond - } onDecrement: { - selection = selection.previousSecond - } - - if selection.locale.hourCycle == .oneToTwelve || selection.locale.hourCycle == .zeroToEleven { - Text(selection.hour < 12 ? selection.calendar.amSymbol : selection.calendar.pmSymbol) - } - } + + @Binding var selection: Fixed + + var body: some View { + HStack { + Stepper(selection.format(hour: .naturalDigits(dayPeriod: .none))) { + selection = selection.nextHour + } onDecrement: { + selection = selection.previousHour + } + + Text(":") + + Stepper(selection.format(minute: .twoDigits)) { + selection = selection.nextMinute + } onDecrement: { + selection = selection.previousMinute + } + + Text(":") + + Stepper(selection.format(second: .twoDigits)) { + selection = selection.nextSecond + } onDecrement: { + selection = selection.previousSecond + } + + if selection.locale.hourCycle == .oneToTwelve || selection.locale.hourCycle == .zeroToEleven { + Text(selection.hour < 12 ? selection.calendar.amSymbol : selection.calendar.pmSymbol) + } } - + } + } diff --git a/Examples/Examples/ContentView.swift b/Examples/Examples/ContentView.swift index af33043..735b0c5 100644 --- a/Examples/Examples/ContentView.swift +++ b/Examples/Examples/ContentView.swift @@ -6,23 +6,23 @@ import SwiftUI struct ContentView: View { - var body: some View { - TabView { - CalendarView() - .tabItem { - Label("Calendar", systemImage: "calendar") - } - - ClocksView() - .tabItem { - Label("Clocks", systemImage: "clock") - } - - CalculatorView() - .tabItem { - Label("Calculator", systemImage: "calendar.badge.clock") - } + var body: some View { + TabView { + CalendarView() + .tabItem { + Label("Calendar", systemImage: "calendar") + } + + ClocksView() + .tabItem { + Label("Clocks", systemImage: "clock") + } + + CalculatorView() + .tabItem { + Label("Calculator", systemImage: "calendar.badge.clock") } - .scenePadding() } + .scenePadding() + } } diff --git a/Examples/Examples/ExamplesApp.swift b/Examples/Examples/ExamplesApp.swift index f010b8d..0c39c25 100644 --- a/Examples/Examples/ExamplesApp.swift +++ b/Examples/Examples/ExamplesApp.swift @@ -7,9 +7,9 @@ import SwiftUI @main struct ExamplesApp: App { - var body: some Scene { - Window("Time Examples", id: "examples") { - ContentView() - } + var body: some Scene { + Window("Time Examples", id: "examples") { + ContentView() } + } } diff --git a/Package.swift b/Package.swift index 7c7ad35..d02c4ed 100644 --- a/Package.swift +++ b/Package.swift @@ -4,23 +4,23 @@ import PackageDescription let package = Package( - name: "Time", - platforms: [ - .macOS(.v13), - .iOS(.v16), - .tvOS(.v16), - .watchOS(.v9), - .macCatalyst(.v16), - ], - products: [ - .library(name: "Time", targets: ["Time"]) - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.1.0") - ], - targets: [ - .target(name: "Time", dependencies: []), - - .testTarget(name: "TimeTests", dependencies: ["Time"]), - ] + name: "Time", + platforms: [ + .macOS(.v12), + .iOS(.v15), + .tvOS(.v16), + .watchOS(.v9), + .macCatalyst(.v15), + ], + products: [ + .library(name: "Time", targets: ["Time"]) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.1.0") + ], + targets: [ + .target(name: "Time", dependencies: []), + + .testTarget(name: "TimeTests", dependencies: ["Time"]), + ] ) diff --git a/Sources/Time/1-Core Types/Duration.swift b/Sources/Time/1-Core Types/Duration.swift new file mode 100644 index 0000000..919c685 --- /dev/null +++ b/Sources/Time/1-Core Types/Duration.swift @@ -0,0 +1,237 @@ +import Foundation + +public struct Duration: Sendable { + public var secondsComponent: Int64 + public var attosecondsComponent: Int64 + + public init(secondsComponent: Int64, attosecondsComponent: Int64) { + self.secondsComponent = secondsComponent + self.attosecondsComponent = attosecondsComponent + } + + public static func ... (minimum: Duration, maximum: Duration) -> ClosedRange { + ClosedRange(uncheckedBounds: (minimum, maximum)) + } + + public static func += (lhs: inout Duration, rhs: Duration) { + lhs = lhs + rhs + } + + public static func -= (lhs: inout Duration, rhs: Duration) { + lhs = lhs - rhs + } + + prefix public static func + (x: Duration) -> Duration { + .zero + x + } + + public static func ..< (minimum: Duration, maximum: Duration) -> Range { + Range(uncheckedBounds: (minimum, maximum)) + } + + prefix public static func ..< (maximum: Duration) -> PartialRangeUpTo { + PartialRangeUpTo(maximum) + } + + prefix public static func ... (maximum: Duration) -> PartialRangeThrough { + PartialRangeThrough(maximum) + } + + postfix public static func ... (minimum: Duration) -> PartialRangeFrom { + PartialRangeFrom(minimum) + } + + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + var realDuration: Swift.Duration { + .init(secondsComponent: secondsComponent, attosecondsComponent: attosecondsComponent) + } +} + +extension Duration { + + public var components: (seconds: Int64, attoseconds: Int64) { + (seconds: secondsComponent, attoseconds: attosecondsComponent) + } +} + +extension Duration { + + @inlinable public static func seconds(_ seconds: T) -> Duration where T: BinaryInteger { + .init(secondsComponent: Int64(seconds), attosecondsComponent: 0) + } + + public static func seconds(_ seconds: Double) -> Duration { + let intSeconds = Int64(seconds) + let attoseconds = Int64((seconds - Double(intSeconds)) * 1_000_000_000_000_000_000) + return .init(secondsComponent: intSeconds, attosecondsComponent: attoseconds) + } + + @inlinable public static func milliseconds(_ milliseconds: T) -> Duration + where T: BinaryInteger { + let intSeconds = Int64(milliseconds - (milliseconds % 1000)) + let attoseconds = Int64((milliseconds % 1000) * 1_000_000_000_000_000) + return .init(secondsComponent: intSeconds, attosecondsComponent: attoseconds) + } + + public static func milliseconds(_ milliseconds: Double) -> Duration { + let intSeconds = Int64(milliseconds / 1_000) + let attoseconds = Int64((milliseconds / 1_000 - Double(intSeconds)) * 1_000_000_000_000_000_000) + return .init(secondsComponent: intSeconds, attosecondsComponent: attoseconds) + } + + @inlinable public static func microseconds(_ microseconds: T) -> Duration + where T: BinaryInteger { + let intSeconds = Int64(microseconds - (microseconds % 1000_000)) + let attoseconds = Int64((microseconds % 1_000_000) * 1_000_000_000_000) + return .init(secondsComponent: intSeconds, attosecondsComponent: attoseconds) + } + + public static func microseconds(_ microseconds: Double) -> Duration { + let intSeconds = Int64(microseconds / 1_000_000) + let attoseconds = Int64((microseconds / 1_000_000 - Double(intSeconds)) * 1_000_000_000_000_000) + return .init(secondsComponent: intSeconds, attosecondsComponent: attoseconds) + } + + @inlinable public static func nanoseconds(_ nanoseconds: T) -> Duration + where T: BinaryInteger { + let intSeconds = Int64(nanoseconds - (nanoseconds % 1_000_000_000)) + let attoseconds = Int64((nanoseconds % 1_000_000_000) * 1_000_000_000) + return .init(secondsComponent: intSeconds, attosecondsComponent: attoseconds) + } +} + +extension Duration { + public static func / (lhs: Duration, rhs: Double) -> Duration { + let total = + Double(lhs.secondsComponent) + Double(lhs.attosecondsComponent) / 1_000_000_000_000_000_000 + return .seconds(total / rhs) + } + + public static func /= (lhs: inout Duration, rhs: Double) { + let total = + Double(lhs.secondsComponent) + Double(lhs.attosecondsComponent) / 1_000_000_000_000_000_000 + lhs = .seconds(total / rhs) + } + + public static func / (lhs: Duration, rhs: T) -> Duration where T: BinaryInteger { + let total = + Double(lhs.secondsComponent) + Double(lhs.attosecondsComponent) / 1_000_000_000_000_000_000 + return .seconds(total / Double(rhs)) + } + + public static func /= (lhs: inout Duration, rhs: T) where T: BinaryInteger { + let total = + Double(lhs.secondsComponent) + Double(lhs.attosecondsComponent) / 1_000_000_000_000_000_000 + lhs = .seconds(total / Double(rhs)) + } + + public static func / (lhs: Duration, rhs: Duration) -> Double { + let total = + Double(lhs.secondsComponent) + Double(lhs.attosecondsComponent) / 1_000_000_000_000_000_000 + let total2 = + Double(rhs.secondsComponent) + Double(rhs.attosecondsComponent) / 1_000_000_000_000_000_000 + return total / total2 + } + + public static func * (lhs: Duration, rhs: Double) -> Duration { + let total = + Double(lhs.secondsComponent) + Double(lhs.attosecondsComponent) / 1_000_000_000_000_000_000 + return .seconds(total * rhs) + } + + public static func * (lhs: Duration, rhs: T) -> Duration where T: BinaryInteger { + let total = + Double(lhs.secondsComponent) + Double(lhs.attosecondsComponent) / 1_000_000_000_000_000_000 + return .seconds(total * Double(rhs)) + } + + public static func *= (lhs: inout Duration, rhs: T) where T: BinaryInteger { + let total = + Double(lhs.secondsComponent) + Double(lhs.attosecondsComponent) / 1_000_000_000_000_000_000 + lhs = .seconds(total * Double(rhs)) + } +} + +extension Duration { + public static func /= (lhs: inout Duration, rhs: Int) { + let total = + Double(lhs.secondsComponent) + Double(lhs.attosecondsComponent) / 1_000_000_000_000_000_000 + lhs = .seconds(total / Double(rhs)) + } + + public static func *= (lhs: inout Duration, rhs: Int) { + let total = + Double(lhs.secondsComponent) + Double(lhs.attosecondsComponent) / 1_000_000_000_000_000_000 + lhs = .seconds(total * Double(rhs)) + } +} + +extension Duration: Codable { + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let values = try container.decode([Int64].self) + self.secondsComponent = values[0] + self.attosecondsComponent = values[0] + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode([secondsComponent, attosecondsComponent]) + } +} + +extension Duration: Hashable {} + +extension Duration: Equatable {} + +extension Duration: Comparable { + + public static func < (lhs: Duration, rhs: Duration) -> Bool { + if lhs.secondsComponent == rhs.secondsComponent { + return lhs.attosecondsComponent < rhs.attosecondsComponent + } + return lhs.secondsComponent < rhs.secondsComponent + } +} + +extension Duration: AdditiveArithmetic { + + public static var zero: Duration { + .seconds(0) + } + + public static func + (lhs: Duration, rhs: Duration) -> Duration { + let total = + Double(lhs.secondsComponent) + Double(lhs.attosecondsComponent) / 1_000_000_000_000_000_000 + let total2 = + Double(rhs.secondsComponent) + Double(rhs.attosecondsComponent) / 1_000_000_000_000_000_000 + return .seconds(total + total2) + } + + public static func - (lhs: Duration, rhs: Duration) -> Duration { + let total = + Double(lhs.secondsComponent) + Double(lhs.attosecondsComponent) / 1_000_000_000_000_000_000 + let total2 = + Double(rhs.secondsComponent) + Double(rhs.attosecondsComponent) / 1_000_000_000_000_000_000 + return .seconds(total - total2) + } + + internal prefix static func - (rhs: Self) -> Self { + var copy = Duration.zero + copy -= rhs + return copy + } +} + +extension Duration: CustomStringConvertible { + + public var description: String { + let total = Double(secondsComponent) + Double(attosecondsComponent) / 1_000_000_000_000_000_000 + return "\(total)" + } +} + +extension Duration { + +} diff --git a/Sources/Time/1-Core Types/Epoch.swift b/Sources/Time/1-Core Types/Epoch.swift index 3059e43..03e265f 100644 --- a/Sources/Time/1-Core Types/Epoch.swift +++ b/Sources/Time/1-Core Types/Epoch.swift @@ -6,45 +6,45 @@ import Foundation /// All `Instant` values are defined as a number of seconds before or after their `Epoch`. public struct Epoch: Hashable, Sendable, CustomStringConvertible { - /// Determine if two Epochs are equivalent. - public static func ==(lhs: Epoch, rhs: Epoch) -> Bool { - return lhs.offsetFromReferenceDate == rhs.offsetFromReferenceDate - } - - /// The Reference epoch (fixed at 1 Jan 2001 00:00:00 UTC). - public static let reference = Epoch(0) - - /// The Unix epoch (fixed at 1 Jan 1970 00:00:00 UTC). - public static let unix = Epoch(-SISeconds.secondsBetweenUnixAndReferenceEpochs) - - internal let offsetFromReferenceDate: SISeconds - - internal init(_ offsetFromReferenceDate: SISeconds) { - self.offsetFromReferenceDate = offsetFromReferenceDate - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(offsetFromReferenceDate) - } - - public var description: String { - if self == .unix { return "unix" } - if self == .reference { return "reference" } - return "custom(\(offsetFromReferenceDate))" - } - + /// Determine if two Epochs are equivalent. + public static func == (lhs: Epoch, rhs: Epoch) -> Bool { + return lhs.offsetFromReferenceDate == rhs.offsetFromReferenceDate + } + + /// The Reference epoch (fixed at 1 Jan 2001 00:00:00 UTC). + public static let reference = Epoch(0) + + /// The Unix epoch (fixed at 1 Jan 1970 00:00:00 UTC). + public static let unix = Epoch(-SISeconds.secondsBetweenUnixAndReferenceEpochs) + + internal let offsetFromReferenceDate: SISeconds + + internal init(_ offsetFromReferenceDate: SISeconds) { + self.offsetFromReferenceDate = offsetFromReferenceDate + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(offsetFromReferenceDate) + } + + public var description: String { + if self == .unix { return "unix" } + if self == .reference { return "reference" } + return "custom(\(offsetFromReferenceDate))" + } + } extension Epoch: Codable { - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - self.init(try container.decode(SISeconds.self)) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(offsetFromReferenceDate) - } - + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.init(try container.decode(SISeconds.self)) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(offsetFromReferenceDate) + } + } diff --git a/Sources/Time/1-Core Types/Instant.swift b/Sources/Time/1-Core Types/Instant.swift index e596997..c02d1a1 100644 --- a/Sources/Time/1-Core Types/Instant.swift +++ b/Sources/Time/1-Core Types/Instant.swift @@ -2,133 +2,135 @@ import Foundation /// An `Instant` is an instantaneous point in time, relative to an `Epoch`. public struct Instant: Hashable, Comparable, InstantProtocol, Sendable { - public typealias Duration = SISeconds + public typealias Duration = SISeconds - /// Determine if two `Instant`s are equivalent. - public static func ==(lhs: Instant, rhs: Instant) -> Bool { - return lhs.intervalSinceReferenceEpoch == rhs.intervalSinceReferenceEpoch - } + /// Determine if two `Instant`s are equivalent. + public static func == (lhs: Instant, rhs: Instant) -> Bool { + return lhs.intervalSinceReferenceEpoch == rhs.intervalSinceReferenceEpoch + } - /// Determine if one `Instant` occurs before another `Instant`. - public static func <(lhs: Instant, rhs: Instant) -> Bool { - return lhs.intervalSinceReferenceEpoch < rhs.intervalSinceReferenceEpoch - } + /// Determine if one `Instant` occurs before another `Instant`. + public static func < (lhs: Instant, rhs: Instant) -> Bool { + return lhs.intervalSinceReferenceEpoch < rhs.intervalSinceReferenceEpoch + } - /// Determine the number of seconds between two `Instant`s. - public static func -(lhs: Instant, rhs: Instant) -> SISeconds { - return lhs.intervalSinceReferenceEpoch - rhs.intervalSinceReferenceEpoch - } + /// Determine the number of seconds between two `Instant`s. + public static func - (lhs: Instant, rhs: Instant) -> SISeconds { + return lhs.intervalSinceReferenceEpoch - rhs.intervalSinceReferenceEpoch + } - /// Apply an offset in seconds to an `Instant`. - public static func +(lhs: Instant, rhs: SISeconds) -> Instant { - return Instant(interval: lhs.intervalSinceEpoch + rhs, since: lhs.epoch) - } + /// Apply an offset in seconds to an `Instant`. + public static func + (lhs: Instant, rhs: SISeconds) -> Instant { + return Instant(interval: lhs.intervalSinceEpoch + rhs, since: lhs.epoch) + } - /// The Instant's `Epoch` value. - public let epoch: Epoch + /// The Instant's `Epoch` value. + public let epoch: Epoch - /// The number of seconds between the `Epoch` and the `Instant`. - public let intervalSinceEpoch: SISeconds - - /// The number seconds between the reference epoch and the `Instant` - public var intervalSinceReferenceEpoch: SISeconds { epoch.offsetFromReferenceDate + intervalSinceEpoch } + /// The number of seconds between the `Epoch` and the `Instant`. + public let intervalSinceEpoch: SISeconds - /// Convert the `Instant` into its `Foundation.Date` representation. - public var date: Foundation.Date { - Date(timeIntervalSinceReferenceDate: intervalSinceReferenceEpoch.timeInterval) - } + /// The number seconds between the reference epoch and the `Instant` + public var intervalSinceReferenceEpoch: SISeconds { + epoch.offsetFromReferenceDate + intervalSinceEpoch + } - /// Construct an `Instant` as the number of seconds since a particular `Epoch`. - public init(interval: SISeconds, since epoch: Epoch) { - self.epoch = epoch - self.intervalSinceEpoch = interval - } + /// Convert the `Instant` into its `Foundation.Date` representation. + public var date: Foundation.Date { + Date(timeIntervalSinceReferenceDate: intervalSinceReferenceEpoch.timeInterval) + } - /// Construct an `Instant` based on a `Foundation.Date`. This uses the Reference `Epoch`. - public init(date: Foundation.Date) { - self.init(interval: SISeconds(date.timeIntervalSinceReferenceDate), since: .reference) - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(intervalSinceReferenceEpoch) - } + /// Construct an `Instant` as the number of seconds since a particular `Epoch`. + public init(interval: SISeconds, since epoch: Epoch) { + self.epoch = epoch + self.intervalSinceEpoch = interval + } + + /// Construct an `Instant` based on a `Foundation.Date`. This uses the Reference `Epoch`. + public init(date: Foundation.Date) { + self.init(interval: SISeconds(date.timeIntervalSinceReferenceDate), since: .reference) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(intervalSinceReferenceEpoch) + } + + /// Convert an `Instant` from one `Epoch` to another. + /// + /// The resulting `Instant` still refers to the *same* point in time as the original `Instant`. + /// This method is used to retrieve an alternate *representation* of that instant. + public func converted(to epoch: Epoch) -> Instant { + if epoch == self.epoch { return self } + let epochOffset = epoch.offsetFromReferenceDate - self.epoch.offsetFromReferenceDate + let epochInterval = intervalSinceEpoch - epochOffset + return Instant(interval: epochInterval, since: epoch) + } + + /// Advance an `Instant` by a number of ``SISeconds`` + /// + /// Required by `InstantProtocol`. + /// + /// - Parameter duration: The number of seconds to advance the `Instant` + /// - Returns: A new `Instant` value + public func advanced(by duration: SISeconds) -> Self { + return self + duration + } + + /// Compute the duration in ``SISeconds`` between two `Instants` + /// + /// Required by `InstantProtocol`. + /// + /// - Parameter other: Another `Instant` + /// - Returns: The duration in ``SISeconds`` between the two `Instants` + public func duration(to other: Self) -> SISeconds { + return self - other + } - /// Convert an `Instant` from one `Epoch` to another. - /// - /// The resulting `Instant` still refers to the *same* point in time as the original `Instant`. - /// This method is used to retrieve an alternate *representation* of that instant. - public func converted(to epoch: Epoch) -> Instant { - if epoch == self.epoch { return self } - let epochOffset = epoch.offsetFromReferenceDate - self.epoch.offsetFromReferenceDate - let epochInterval = intervalSinceEpoch - epochOffset - return Instant(interval: epochInterval, since: epoch) - } - - /// Advance an `Instant` by a number of ``SISeconds`` - /// - /// Required by `InstantProtocol`. - /// - /// - Parameter duration: The number of seconds to advance the `Instant` - /// - Returns: A new `Instant` value - public func advanced(by duration: SISeconds) -> Self { - return self + duration - } - - /// Compute the duration in ``SISeconds`` between two `Instants` - /// - /// Required by `InstantProtocol`. - /// - /// - Parameter other: Another `Instant` - /// - Returns: The duration in ``SISeconds`` between the two `Instants` - public func duration(to other: Self) -> SISeconds { - return self - other - } - } extension Instant: Codable { - - private enum CodingKeys: String, CodingKey { - case epoch = "epoch" - case offset = "offset" - } - - public init(from decoder: Decoder) throws { - do { - // first, see if we can decode a Foundation.Date - let container = try decoder.singleValueContainer() - let date = try container.decode(Date.self) - self.init(date: date) - } catch { - // can't decode a Foundation.Date. Try decoding a proper Instant - - let container = try decoder.container(keyedBy: CodingKeys.self) - - let epoch = try container.decodeIfPresent(Epoch.self, forKey: .epoch) ?? .reference - let seconds = try container.decode(SISeconds.self, forKey: .offset) - - self.init(interval: seconds, since: epoch) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(epoch, forKey: .epoch) - try container.encode(intervalSinceEpoch, forKey: .offset) + + private enum CodingKeys: String, CodingKey { + case epoch = "epoch" + case offset = "offset" + } + + public init(from decoder: Decoder) throws { + do { + // first, see if we can decode a Foundation.Date + let container = try decoder.singleValueContainer() + let date = try container.decode(Date.self) + self.init(date: date) + } catch { + // can't decode a Foundation.Date. Try decoding a proper Instant + + let container = try decoder.container(keyedBy: CodingKeys.self) + + let epoch = try container.decodeIfPresent(Epoch.self, forKey: .epoch) ?? .reference + let seconds = try container.decode(SISeconds.self, forKey: .offset) + + self.init(interval: seconds, since: epoch) } - + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(epoch, forKey: .epoch) + try container.encode(intervalSinceEpoch, forKey: .offset) + } + } extension Instant: CustomStringConvertible, CustomDebugStringConvertible { - - public var description: String { - let direction = self.intervalSinceEpoch >= 0 ? "+" : "" - return "\(epoch)\(direction)\(intervalSinceEpoch)" - } - - public var debugDescription: String { - let direction = self.intervalSinceEpoch >= 0 ? "+" : "" - return "\(epoch)\(direction)\(intervalSinceEpoch.debugDescription)" - } - + + public var description: String { + let direction = self.intervalSinceEpoch >= 0 ? "+" : "" + return "\(epoch)\(direction)\(intervalSinceEpoch)" + } + + public var debugDescription: String { + let direction = self.intervalSinceEpoch >= 0 ? "+" : "" + return "\(epoch)\(direction)\(intervalSinceEpoch.debugDescription)" + } + } diff --git a/Sources/Time/1-Core Types/SISeconds.swift b/Sources/Time/1-Core Types/SISeconds.swift index 0e6993c..51df6bf 100644 --- a/Sources/Time/1-Core Types/SISeconds.swift +++ b/Sources/Time/1-Core Types/SISeconds.swift @@ -10,167 +10,174 @@ import Foundation /// of representation is needed. /// /// - SeeAlso: [https://en.wikipedia.org/wiki/SI_base_unit](https://en.wikipedia.org/wiki/SI_base_unit) -public struct SISeconds: RawRepresentable, Hashable, Comparable, DurationProtocol, Sendable, CustomStringConvertible, CustomDebugStringConvertible { - - internal static let secondsBetweenUnixAndReferenceEpochs = SISeconds(Date.timeIntervalBetween1970AndReferenceDate) - - /// Determine if two `SISeconds` values are equivalent. - public static func ==(lhs: SISeconds, rhs: SISeconds) -> Bool { return lhs.rawValue == rhs.rawValue } - - /// Determine if an `SISeconds` value is smaller than another. - public static func <(lhs: SISeconds, rhs: SISeconds) -> Bool { return lhs.rawValue < rhs.rawValue } - - /// Add two `SISeconds` values. - public static func +(lhs: SISeconds, rhs: SISeconds) -> SISeconds { - return SISeconds(rawValue: lhs.rawValue + rhs.rawValue) - } - - /// Add and assign two `SISeconds` values. - public static func +=(lhs: inout SISeconds, rhs: SISeconds) { - lhs = lhs + rhs - } - - /// Determine the difference between two `SISeconds` values. - public static func -(lhs: SISeconds, rhs: SISeconds) -> SISeconds { - return SISeconds(lhs.rawValue - rhs.rawValue) - } +public struct SISeconds: RawRepresentable, Hashable, Comparable, DurationProtocol, Sendable, + CustomStringConvertible, CustomDebugStringConvertible +{ - /// Assign the difference between two `SISeconds` values. - public static func -=(lhs: inout SISeconds, rhs: SISeconds) { - lhs = lhs - rhs - } - - /// Scale up an `SISeconds` value. - public static func *(lhs: SISeconds, rhs: Double) -> SISeconds { - return SISeconds(lhs.rawValue * rhs) - } - - /// Scale up and assign to an `SISeconds` value. - public static func *=(lhs: inout SISeconds, rhs: Double) { - lhs = lhs * rhs - } - - /// Scale down an `SISeconds` value. - public static func /(lhs: SISeconds, rhs: Double) -> SISeconds { - return SISeconds(lhs.rawValue / rhs) - } + internal static let secondsBetweenUnixAndReferenceEpochs = SISeconds( + Date.timeIntervalBetween1970AndReferenceDate) + + /// Determine if two `SISeconds` values are equivalent. + public static func == (lhs: SISeconds, rhs: SISeconds) -> Bool { + return lhs.rawValue == rhs.rawValue + } + + /// Determine if an `SISeconds` value is smaller than another. + public static func < (lhs: SISeconds, rhs: SISeconds) -> Bool { + return lhs.rawValue < rhs.rawValue + } + + /// Add two `SISeconds` values. + public static func + (lhs: SISeconds, rhs: SISeconds) -> SISeconds { + return SISeconds(rawValue: lhs.rawValue + rhs.rawValue) + } + + /// Add and assign two `SISeconds` values. + public static func += (lhs: inout SISeconds, rhs: SISeconds) { + lhs = lhs + rhs + } + + /// Determine the difference between two `SISeconds` values. + public static func - (lhs: SISeconds, rhs: SISeconds) -> SISeconds { + return SISeconds(lhs.rawValue - rhs.rawValue) + } + + /// Assign the difference between two `SISeconds` values. + public static func -= (lhs: inout SISeconds, rhs: SISeconds) { + lhs = lhs - rhs + } + + /// Scale up an `SISeconds` value. + public static func * (lhs: SISeconds, rhs: Double) -> SISeconds { + return SISeconds(lhs.rawValue * rhs) + } + + /// Scale up and assign to an `SISeconds` value. + public static func *= (lhs: inout SISeconds, rhs: Double) { + lhs = lhs * rhs + } + + /// Scale down an `SISeconds` value. + public static func / (lhs: SISeconds, rhs: Double) -> SISeconds { + return SISeconds(lhs.rawValue / rhs) + } + + /// Scale down and assign to an `SISeconds` value. + public static func /= (lhs: inout SISeconds, rhs: Double) { + lhs = lhs / rhs + } + + /// Scale down an `SISeconds` value. + public static func / (lhs: SISeconds, rhs: Int) -> SISeconds { + return SISeconds(lhs.rawValue / Double(rhs)) + } + + /// Scale up an `SISeconds` value. + public static func * (lhs: SISeconds, rhs: Int) -> SISeconds { + return SISeconds(lhs.rawValue * Double(rhs)) + } + + /// Find the ratio between two `SISeconds` values. + public static func / (lhs: SISeconds, rhs: SISeconds) -> Double { + return lhs.rawValue / rhs.rawValue + } + + /// Negate an `SISeconds` value. + public static prefix func - (rhs: SISeconds) -> SISeconds { + SISeconds(-rhs.rawValue) + } + + /// The underlying `Duration` representation of an `SISeconds` value. + public let rawValue: Duration + + /// The representation of the `rawValue` as a `TimeInterval`. + /// + /// - Note: This is potentially a lossy conversion, since `TimeInterval` is not as precise as `Duration`. + internal var timeInterval: Foundation.TimeInterval { + let (seconds, attoseconds) = rawValue.components + return Double(seconds) + (Double(attoseconds) / Double(1e18)) + } + + public init(rawValue: Duration) { + self.rawValue = rawValue + } + + public init(_ value: Duration) { + self.rawValue = value + } + + public init(_ value: Double) { + self.rawValue = .seconds(value) + } + + public init(_ value: Int) { + self.rawValue = .seconds(value) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(rawValue) + } + + public var magnitude: Self { + if rawValue >= .zero { return self } + return SISeconds(-rawValue) + } + + public var description: String { + return "\(timeInterval)" + } + + public var debugDescription: String { + let (s, a) = rawValue.components + return "{ seconds: \(s), attoseconds: \(a) }" + } - /// Scale down and assign to an `SISeconds` value. - public static func /=(lhs: inout SISeconds, rhs: Double) { - lhs = lhs / rhs - } - - /// Scale down an `SISeconds` value. - public static func / (lhs: SISeconds, rhs: Int) -> SISeconds { - return SISeconds(lhs.rawValue / Double(rhs)) - } - - /// Scale up an `SISeconds` value. - public static func * (lhs: SISeconds, rhs: Int) -> SISeconds { - return SISeconds(lhs.rawValue * Double(rhs)) - } - - /// Find the ratio between two `SISeconds` values. - public static func / (lhs: SISeconds, rhs: SISeconds) -> Double { - return lhs.rawValue / rhs.rawValue - } - - /// Negate an `SISeconds` value. - public static prefix func -(rhs: SISeconds) -> SISeconds { - SISeconds(-rhs.rawValue) - } - - /// The underlying `Duration` representation of an `SISeconds` value. - public let rawValue: Swift.Duration - - /// The representation of the `rawValue` as a `TimeInterval`. - /// - /// - Note: This is potentially a lossy conversion, since `TimeInterval` is not as precise as `Duration`. - internal var timeInterval: Foundation.TimeInterval { - let (seconds, attoseconds) = rawValue.components - return Double(seconds) + (Double(attoseconds) / Double(1e18)) - } - - public init(rawValue: Duration) { - self.rawValue = rawValue - } - - public init(_ value: Duration) { - self.rawValue = value - } - - public init(_ value: Double) { - self.rawValue = .seconds(value) - } - - public init(_ value: Int) { - self.rawValue = .seconds(value) - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(rawValue) - } - - public var magnitude: Self { - if rawValue >= .zero { return self } - return SISeconds(-rawValue) - } - - public var description: String { - return "\(timeInterval)" - } - - public var debugDescription: String { - let (s, a) = rawValue.components - return "{ seconds: \(s), attoseconds: \(a) }" - } - } extension SISeconds: ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral { - - public init(floatLiteral value: Double) { - self.init(value) - } - - public init(integerLiteral value: Int) { - self.init(value) - } + + public init(floatLiteral value: Double) { + self.init(value) + } + + public init(integerLiteral value: Int) { + self.init(value) + } } extension SISeconds: Codable { - - private enum CodingKeys: String, CodingKey { - case seconds = "seconds" - case attoseconds = "attoseconds" - } - - public init(from decoder: Decoder) throws { - do { - // try to decode a single TimeInterval - let container = try decoder.singleValueContainer() - self.init(try container.decode(TimeInterval.self)) - } catch { - // decode the separate components - let container = try decoder.container(keyedBy: CodingKeys.self) - let seconds = try container.decode(Int64.self, forKey: .seconds) - let attoseconds = try container.decodeIfPresent(Int64.self, forKey: .attoseconds) ?? 0 - - let duration = Duration(secondsComponent: seconds, attosecondsComponent: attoseconds) - self.init(rawValue: duration) - } - } - - public func encode(to encoder: Encoder) throws { - let (seconds, attoseconds) = rawValue.components - if attoseconds != 0 { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(seconds, forKey: .seconds) - try container.encode(attoseconds, forKey: .attoseconds) - } else { - var container = encoder.singleValueContainer() - try container.encode(seconds) - } + + private enum CodingKeys: String, CodingKey { + case seconds = "seconds" + case attoseconds = "attoseconds" + } + + public init(from decoder: Decoder) throws { + do { + // try to decode a single TimeInterval + let container = try decoder.singleValueContainer() + self.init(try container.decode(TimeInterval.self)) + } catch { + // decode the separate components + let container = try decoder.container(keyedBy: CodingKeys.self) + let seconds = try container.decode(Int64.self, forKey: .seconds) + let attoseconds = try container.decodeIfPresent(Int64.self, forKey: .attoseconds) ?? 0 + + let duration = Duration(secondsComponent: seconds, attosecondsComponent: attoseconds) + self.init(rawValue: duration) } - + } + + public func encode(to encoder: Encoder) throws { + let (seconds, attoseconds) = rawValue.components + if attoseconds != 0 { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(seconds, forKey: .seconds) + try container.encode(attoseconds, forKey: .attoseconds) + } else { + var container = encoder.singleValueContainer() + try container.encode(seconds) + } + } + } diff --git a/Sources/Time/10-Relations/Relations+Fixed.swift b/Sources/Time/10-Relations/Relations+Fixed.swift index a499bd2..310ebb9 100644 --- a/Sources/Time/10-Relations/Relations+Fixed.swift +++ b/Sources/Time/10-Relations/Relations+Fixed.swift @@ -1,52 +1,52 @@ extension Fixed { - - /// Find the relationship between any two `Fixed` values. - /// - /// - Parameter other: Any other `Fixed` value - public func relation(to other: Fixed) -> Relation { - let thisRange = self.range - let thatRange = other.range - - return thisRange.determineRelationship(to: thatRange) - } - - /// Determine if this fixed value occurs entirely before or meets another fixed value - /// - Parameter other: A fixed value - /// - Returns: `true` if the relation to `other` is `.before` or `.meets` - public func isBefore(_ other: Fixed) -> Bool { - let r = relation(to: other) - return r == .before || r == .meets - } - - /// Determine if this fixed value occurs entirely after or meets another fixed value - /// - Parameter other: A fixed value - /// - Returns: `true` if the relation to `other` is `.after` or `.isMetBy` - public func isAfter(_ other: Fixed) -> Bool { - let r = relation(to: other) - return r == .after || r == .isMetBy - } - - /// Determine if this fixed value contains another fixed value - /// - Parameter other: A fixed value - /// - Returns: `true` if the relation to `other` is `.contains`, `.isStartedBy`, or `.isFinishedBy` - public func contains(_ other: Fixed) -> Bool { - let r = relation(to: other) - return r == .contains || r == .isStartedBy || r == .isFinishedBy - } - - /// Determine if this fixed value is contained by another fixed value - /// - Parameter other: A fixed value - /// - Returns: `true` if the relation to `other` is `.during`, `.starts`, or `.finishes` - public func isDuring(_ other: Fixed) -> Bool { - let r = relation(to: other) - return r == .during || r == .starts || r == .finishes - } - - /// Determine if this fixed overlaps another fixed value - /// - Parameter other: A fixed value - /// - Returns: `true` if the relation to `other` is `.overlaps`, `.isOverlappedBy`, `.during`, or `.contains` - public func overlaps(_ other: Fixed) -> Bool { - return relation(to: other).isOverlapping - } - + + /// Find the relationship between any two `Fixed` values. + /// + /// - Parameter other: Any other `Fixed` value + public func relation(to other: Fixed) -> Relation { + let thisRange = self.range + let thatRange = other.range + + return thisRange.determineRelationship(to: thatRange) + } + + /// Determine if this fixed value occurs entirely before or meets another fixed value + /// - Parameter other: A fixed value + /// - Returns: `true` if the relation to `other` is `.before` or `.meets` + public func isBefore(_ other: Fixed) -> Bool { + let r = relation(to: other) + return r == .before || r == .meets + } + + /// Determine if this fixed value occurs entirely after or meets another fixed value + /// - Parameter other: A fixed value + /// - Returns: `true` if the relation to `other` is `.after` or `.isMetBy` + public func isAfter(_ other: Fixed) -> Bool { + let r = relation(to: other) + return r == .after || r == .isMetBy + } + + /// Determine if this fixed value contains another fixed value + /// - Parameter other: A fixed value + /// - Returns: `true` if the relation to `other` is `.contains`, `.isStartedBy`, or `.isFinishedBy` + public func contains(_ other: Fixed) -> Bool { + let r = relation(to: other) + return r == .contains || r == .isStartedBy || r == .isFinishedBy + } + + /// Determine if this fixed value is contained by another fixed value + /// - Parameter other: A fixed value + /// - Returns: `true` if the relation to `other` is `.during`, `.starts`, or `.finishes` + public func isDuring(_ other: Fixed) -> Bool { + let r = relation(to: other) + return r == .during || r == .starts || r == .finishes + } + + /// Determine if this fixed overlaps another fixed value + /// - Parameter other: A fixed value + /// - Returns: `true` if the relation to `other` is `.overlaps`, `.isOverlappedBy`, `.during`, or `.contains` + public func overlaps(_ other: Fixed) -> Bool { + return relation(to: other).isOverlapping + } + } diff --git a/Sources/Time/10-Relations/Relations+Ranges.swift b/Sources/Time/10-Relations/Relations+Ranges.swift index 629d7bd..9dd22c3 100644 --- a/Sources/Time/10-Relations/Relations+Ranges.swift +++ b/Sources/Time/10-Relations/Relations+Ranges.swift @@ -1,48 +1,49 @@ import Foundation -internal extension Range where Bound: Comparable { - - func determineRelationship(to other: Range) -> Relation { - if self.lowerBound < other.lowerBound { - if self.upperBound < other.lowerBound { return .before } - if self.upperBound == other.lowerBound { return .meets } - if self.upperBound < other.upperBound { return .overlaps } - if self.upperBound == other.upperBound { return .isFinishedBy } - /* self.upperBound > other.upperBound */ return .contains - } else if self.lowerBound == other.lowerBound { - if self.upperBound < other.upperBound { return .starts } - if self.upperBound == other.upperBound { return .equal } - /* self.upperBound > other.upperBound */ return .isStartedBy - } else /* self.lowerBound > other.lowerBound */ { - if self.lowerBound > other.upperBound { return .after } - if self.lowerBound == other.upperBound { return .isMetBy } - if self.upperBound < other.upperBound { return .during } - if self.upperBound == other.upperBound { return .finishes } - /* self.upperBound > other.upperBound */ return .isOverlappedBy - } +extension Range where Bound: Comparable { + + func determineRelationship(to other: Range) -> Relation { + if self.lowerBound < other.lowerBound { + if self.upperBound < other.lowerBound { return .before } + if self.upperBound == other.lowerBound { return .meets } + if self.upperBound < other.upperBound { return .overlaps } + if self.upperBound == other.upperBound { return .isFinishedBy } + /* self.upperBound > other.upperBound */ return .contains + } else if self.lowerBound == other.lowerBound { + if self.upperBound < other.upperBound { return .starts } + if self.upperBound == other.upperBound { return .equal } + /* self.upperBound > other.upperBound */ return .isStartedBy + } else /* self.lowerBound > other.lowerBound */ + { + if self.lowerBound > other.upperBound { return .after } + if self.lowerBound == other.upperBound { return .isMetBy } + if self.upperBound < other.upperBound { return .during } + if self.upperBound == other.upperBound { return .finishes } + /* self.upperBound > other.upperBound */ return .isOverlappedBy } - + } + } extension Range { - - /// Determine the relation between two ranges of fixed values - /// - Parameter other: A range of fixed values - /// - Returns: A ``Relation`` describing the relation between the two ranges. - public func relation(to other: Range>) -> Relation where Bound == Fixed { - // Ranges do _not_ contain their upper bound, so when converting to ranges of Instant, we should - // take the Range's upper bound's lowest Instant. - let thisRange = lowerBound.range.lowerBound ..< upperBound.range.lowerBound - let thatRange = other.lowerBound.range.lowerBound ..< other.upperBound.range.lowerBound - return thisRange.determineRelationship(to: thatRange) - } - - /// Determine the relation between this range of fixed values and another fixed value - /// - Parameter other: A fixed value - /// - Returns: A ``Relation`` describing the relation between this range and the fixed value. - public func relation(to other: Fixed
    ) -> Relation where Bound == Fixed { - let thisRange = lowerBound.range.lowerBound ..< upperBound.range.lowerBound - return thisRange.determineRelationship(to: other.range) - } - + + /// Determine the relation between two ranges of fixed values + /// - Parameter other: A range of fixed values + /// - Returns: A ``Relation`` describing the relation between the two ranges. + public func relation(to other: Range>) -> Relation where Bound == Fixed { + // Ranges do _not_ contain their upper bound, so when converting to ranges of Instant, we should + // take the Range's upper bound's lowest Instant. + let thisRange = lowerBound.range.lowerBound..(to other: Fixed
      ) -> Relation where Bound == Fixed { + let thisRange = lowerBound.range.lowerBound.. = [.meets, .isMetBy, .starts, .isStartedBy, .finishes, .isFinishedBy] - internal static let overlappings: Set = [.overlaps, .isOverlappedBy, .during, .contains, .equal] - - /// The first range occurs entirely before the second range - /// - /// - Example: Range `A` is before range `B`: - /// ```` - /// ●--A--○ - /// ●--B--○ - /// ```` - case before - - /// The first range occurs entirely after the second rage - /// - /// - Example: Range `A` is after range `B`: - /// ```` - /// ●--B--○ - /// ●--A--○ - /// ```` - case after - - /// The first range ends where the second range starts - /// - /// - Example: Range `A` meets range `B`: - /// ```` - /// ●--A--○ - /// ●--B--○ - /// ```` - case meets - - /// The first range starts where the second range ends - /// - /// - Example: Range `A` is met by range `B`: - /// ```` - /// ●--B--○ - /// ●--A--○ - /// ```` - case isMetBy - - /// The first range starts before the second range starts, and ends before the second range ends - /// - /// - Example: Range `A` overlaps range `B`: - /// ```` - /// ●--A--○ - /// ●--B--○ - /// ```` - case overlaps - - /// The first range starts after the second range starts, and ends after the second range ends - /// - /// - Example: Range `A` is overlapped by range `B`: - /// ```` - /// ●--B--○ - /// ●--A--○ - /// ```` - case isOverlappedBy - - /// The first range starts where the second range starts, and ends before the second range ends - /// - /// - Example: Range `A` starts range `B`: - /// ```` - /// ●--A--○ - /// ●----B----○ - /// ```` - case starts - - /// The first range starts where the second range starts, and ends after the second range ends - /// - /// - Example: Range `A` is started by range `B`: - /// ```` - /// ●--B--○ - /// ●----A----○ - /// ```` - case isStartedBy - - /// The first range starts after the second range starts, and ends before the second range ends - /// - /// - Example: Range `A` is during range `B`: - /// ```` - /// ●--A--○ - /// ●----B----○ - /// ```` - case during - - /// The first range starts before the second range starts, and ends after the second range ends - /// - /// - Example: Range `A` contains range `B`: - /// ```` - /// ●--B--○ - /// ●----A----○ - /// ```` - case contains - - /// The first range starts after the second range starts, and ends with the second range - /// - /// - Example: Range `A` finishes range `B`: - /// ```` - /// ●--A--○ - /// ●----B----○ - /// ```` - case finishes - - /// The first range starts before the second range starts, and ends with the second range - /// - /// - Example: Range `A` is finished by range `B`: - /// ```` - /// ●--B--○ - /// ●----A----○ - /// ```` - case isFinishedBy - - /// The first and second ranges start and end together - /// - /// - Example: Range `A` equals range `B`: - /// ```` - /// ●----A----○ - /// ●----B----○ - /// ```` - case equal - - /// Returns `true` if the relation describes two ranges that meet at any extreme - public var isMeeting: Bool { Relation.meetings.contains(self) } - - /// Returns `true` if the relation describes any kind of overlapping - public var isOverlapping: Bool { Relation.overlappings.contains(self) } - - /// Returns `true` if the relation describes disjointedness - public var isDisjoint: Bool { self == .before || self == .after } - - /// Returns `true` if the relation describes equality - public var isEqual: Bool { self == .equal } - + + internal static let meetings: Set = [ + .meets, .isMetBy, .starts, .isStartedBy, .finishes, .isFinishedBy, + ] + internal static let overlappings: Set = [ + .overlaps, .isOverlappedBy, .during, .contains, .equal, + ] + + /// The first range occurs entirely before the second range + /// + /// - Example: Range `A` is before range `B`: + /// ```` + /// ●--A--○ + /// ●--B--○ + /// ```` + case before + + /// The first range occurs entirely after the second rage + /// + /// - Example: Range `A` is after range `B`: + /// ```` + /// ●--B--○ + /// ●--A--○ + /// ```` + case after + + /// The first range ends where the second range starts + /// + /// - Example: Range `A` meets range `B`: + /// ```` + /// ●--A--○ + /// ●--B--○ + /// ```` + case meets + + /// The first range starts where the second range ends + /// + /// - Example: Range `A` is met by range `B`: + /// ```` + /// ●--B--○ + /// ●--A--○ + /// ```` + case isMetBy + + /// The first range starts before the second range starts, and ends before the second range ends + /// + /// - Example: Range `A` overlaps range `B`: + /// ```` + /// ●--A--○ + /// ●--B--○ + /// ```` + case overlaps + + /// The first range starts after the second range starts, and ends after the second range ends + /// + /// - Example: Range `A` is overlapped by range `B`: + /// ```` + /// ●--B--○ + /// ●--A--○ + /// ```` + case isOverlappedBy + + /// The first range starts where the second range starts, and ends before the second range ends + /// + /// - Example: Range `A` starts range `B`: + /// ```` + /// ●--A--○ + /// ●----B----○ + /// ```` + case starts + + /// The first range starts where the second range starts, and ends after the second range ends + /// + /// - Example: Range `A` is started by range `B`: + /// ```` + /// ●--B--○ + /// ●----A----○ + /// ```` + case isStartedBy + + /// The first range starts after the second range starts, and ends before the second range ends + /// + /// - Example: Range `A` is during range `B`: + /// ```` + /// ●--A--○ + /// ●----B----○ + /// ```` + case during + + /// The first range starts before the second range starts, and ends after the second range ends + /// + /// - Example: Range `A` contains range `B`: + /// ```` + /// ●--B--○ + /// ●----A----○ + /// ```` + case contains + + /// The first range starts after the second range starts, and ends with the second range + /// + /// - Example: Range `A` finishes range `B`: + /// ```` + /// ●--A--○ + /// ●----B----○ + /// ```` + case finishes + + /// The first range starts before the second range starts, and ends with the second range + /// + /// - Example: Range `A` is finished by range `B`: + /// ```` + /// ●--B--○ + /// ●----A----○ + /// ```` + case isFinishedBy + + /// The first and second ranges start and end together + /// + /// - Example: Range `A` equals range `B`: + /// ```` + /// ●----A----○ + /// ●----B----○ + /// ```` + case equal + + /// Returns `true` if the relation describes two ranges that meet at any extreme + public var isMeeting: Bool { Relation.meetings.contains(self) } + + /// Returns `true` if the relation describes any kind of overlapping + public var isOverlapping: Bool { Relation.overlappings.contains(self) } + + /// Returns `true` if the relation describes disjointedness + public var isDisjoint: Bool { self == .before || self == .after } + + /// Returns `true` if the relation describes equality + public var isEqual: Bool { self == .equal } + } diff --git a/Sources/Time/11-Rounding/Fixed+Rounding.swift b/Sources/Time/11-Rounding/Fixed+Rounding.swift index f44a085..ad05497 100644 --- a/Sources/Time/11-Rounding/Fixed+Rounding.swift +++ b/Sources/Time/11-Rounding/Fixed+Rounding.swift @@ -3,201 +3,203 @@ import Foundation /// An enum describing the direction to perform a rounding calculation public enum RoundingDirection: Sendable { - /// Perform a rounding calculation such that the rounded value is preferred to be forward in time - case forward - - /// Perform a rounding calculation such that the rounded value is preferred to be backwards in time - case backward - - /// Perform a rounding calculation towards the closer of two forwards and backwards values - case nearest + /// Perform a rounding calculation such that the rounded value is preferred to be forward in time + case forward + + /// Perform a rounding calculation such that the rounded value is preferred to be backwards in time + case backward + + /// Perform a rounding calculation towards the closer of two forwards and backwards values + case nearest } extension Fixed where Granularity: LTOEYear { - - /// Round the fixed value towards the nearest boundary that matches a whole multiple. - /// - /// Equivalent to `roundedToMultiple(of: match, direction: .nearest)` - /// - /// - Parameter match: A `TimeDifference` that describes the desired boundary - /// - Returns: A new fixed value - public func roundedToNearestMultiple(of match: TimeDifference) -> Self { - return self.roundToMultiple(of: match, direction: .nearest) - } - - /// Round the fixed value towards the nearest boundary that matches a whole multiple. - /// - /// This method rounds towards the nearest multiple of the `match`. Multiples are computed based on a a boundary. - /// - /// Example: consider a `Fixed` with time components of `hour: 14, minute: 38, second: 42`. Rounding - /// this value towards the nearest match of `.minutes(15)` will result in `hour: 14, minute: 45, second: 0`. The `minutes` - /// of the returned value is a integer multiple of the specified `match`. - /// - /// Example: rounding that same value towards the nearest multiple of `.minutes(32)` will result in - /// `hour: 15, minute: 0, second: 0`. When crossing the "boundary" of a larger unit (hours), the smaller units reset to their - /// zero values, and `0` is considered a whole multiple of any number. - /// - /// - Parameter match: A `TimeDifference` that describes the desired boundary - /// - Parameter direction: The preferred rounding direction - /// - Returns: A new fixed value - public func roundedToMultiple(of match: TimeDifference, direction: RoundingDirection) -> Self { - return self.roundToMultiple(of: match, direction: direction) - } - - /// Round this fixed value towards an Era boundary - /// - /// - Warning: This method may end up rounding backwards (down), even if the `direction` is `.forward`. - /// This happens if there is no "next" era. For example, on the Gregorian calendar, there are only two - /// eras: 0 (BC/BCE) and 1 (AD/CE). A `Fixed` value in the AD era that is asked to round forward will - /// always round *down* to the start of the AD era, because there is no known future era boundary. - /// - /// - Parameter direction: The suggested rounding direction - /// - Returns: A `Fixed` value whose `.firstInstant` is an Era boundary - public func roundedToEra(direction: RoundingDirection) -> Self { - round(to: Era.self, direction: direction) - } - - /// Round this fixed value towards the nearest Era boundary - /// - /// Equivalent to `.roundedToEra(direction: .nearest)` - /// - /// - Returns: A `Fixed` value whose `.firstInstant` is an Era boundary - public func roundedToNearestEra() -> Self { - round(to: Era.self, direction: .nearest) - } - - /// Retrieve the nearest `Fixed` - public var nearestEra: Fixed { roundedToNearestEra().fixedEra } + + /// Round the fixed value towards the nearest boundary that matches a whole multiple. + /// + /// Equivalent to `roundedToMultiple(of: match, direction: .nearest)` + /// + /// - Parameter match: A `TimeDifference` that describes the desired boundary + /// - Returns: A new fixed value + public func roundedToNearestMultiple(of match: TimeDifference) -> Self { + return self.roundToMultiple(of: match, direction: .nearest) + } + + /// Round the fixed value towards the nearest boundary that matches a whole multiple. + /// + /// This method rounds towards the nearest multiple of the `match`. Multiples are computed based on a a boundary. + /// + /// Example: consider a `Fixed` with time components of `hour: 14, minute: 38, second: 42`. Rounding + /// this value towards the nearest match of `.minutes(15)` will result in `hour: 14, minute: 45, second: 0`. The `minutes` + /// of the returned value is a integer multiple of the specified `match`. + /// + /// Example: rounding that same value towards the nearest multiple of `.minutes(32)` will result in + /// `hour: 15, minute: 0, second: 0`. When crossing the "boundary" of a larger unit (hours), the smaller units reset to their + /// zero values, and `0` is considered a whole multiple of any number. + /// + /// - Parameter match: A `TimeDifference` that describes the desired boundary + /// - Parameter direction: The preferred rounding direction + /// - Returns: A new fixed value + public func roundedToMultiple( + of match: TimeDifference, direction: RoundingDirection + ) -> Self { + return self.roundToMultiple(of: match, direction: direction) + } + + /// Round this fixed value towards an Era boundary + /// + /// - Warning: This method may end up rounding backwards (down), even if the `direction` is `.forward`. + /// This happens if there is no "next" era. For example, on the Gregorian calendar, there are only two + /// eras: 0 (BC/BCE) and 1 (AD/CE). A `Fixed` value in the AD era that is asked to round forward will + /// always round *down* to the start of the AD era, because there is no known future era boundary. + /// + /// - Parameter direction: The suggested rounding direction + /// - Returns: A `Fixed` value whose `.firstInstant` is an Era boundary + public func roundedToEra(direction: RoundingDirection) -> Self { + round(to: Era.self, direction: direction) + } + + /// Round this fixed value towards the nearest Era boundary + /// + /// Equivalent to `.roundedToEra(direction: .nearest)` + /// + /// - Returns: A `Fixed` value whose `.firstInstant` is an Era boundary + public func roundedToNearestEra() -> Self { + round(to: Era.self, direction: .nearest) + } + + /// Retrieve the nearest `Fixed` + public var nearestEra: Fixed { roundedToNearestEra().fixedEra } } extension Fixed where Granularity: LTOEMonth { - - /// Round this fixed value towards a year boundary - /// - Parameter direction: The preferred rounding direction - /// - Returns: A fixed value whose `.firstInstant` is a year boundary - public func roundedToYear(direction: RoundingDirection) -> Self { - round(to: Year.self, direction: direction) - } - - /// Round this fixed value towards the nearest year boundary - /// - /// Equivalent to `.roundedToYear(direction: .nearest)` - /// - /// - Returns: A fixed value whose `.firstInstant` is a year boundary - public func roundedToNearestYear() -> Self { - round(to: Year.self, direction: .nearest) - } - - /// Retrieve the nearest `Fixed` - public var nearestYear: Fixed { roundedToNearestYear().fixedYear } + + /// Round this fixed value towards a year boundary + /// - Parameter direction: The preferred rounding direction + /// - Returns: A fixed value whose `.firstInstant` is a year boundary + public func roundedToYear(direction: RoundingDirection) -> Self { + round(to: Year.self, direction: direction) + } + + /// Round this fixed value towards the nearest year boundary + /// + /// Equivalent to `.roundedToYear(direction: .nearest)` + /// + /// - Returns: A fixed value whose `.firstInstant` is a year boundary + public func roundedToNearestYear() -> Self { + round(to: Year.self, direction: .nearest) + } + + /// Retrieve the nearest `Fixed` + public var nearestYear: Fixed { roundedToNearestYear().fixedYear } } extension Fixed where Granularity: LTOEDay { - - /// Round this fixed value towards a month boundary - /// - Parameter direction: The preferred rounding direction - /// - Returns: A fixed value whose `.firstInstant` is a month boundary - public func roundedToMonth(direction: RoundingDirection) -> Self { - round(to: Month.self, direction: direction) - } - - /// Round this fixed value towards the nearest month boundary - /// - /// Equivalent to `.roundedToMonth(direction: .nearest)` - /// - /// - Returns: A fixed value whose `.firstInstant` is a month boundary - public func roundedToNearestMonth() -> Self { - round(to: Month.self, direction: .nearest) - } - - /// Retrieve the nearest `Fixed` - public var nearestMonth: Fixed { roundedToNearestMonth().fixedMonth } + + /// Round this fixed value towards a month boundary + /// - Parameter direction: The preferred rounding direction + /// - Returns: A fixed value whose `.firstInstant` is a month boundary + public func roundedToMonth(direction: RoundingDirection) -> Self { + round(to: Month.self, direction: direction) + } + + /// Round this fixed value towards the nearest month boundary + /// + /// Equivalent to `.roundedToMonth(direction: .nearest)` + /// + /// - Returns: A fixed value whose `.firstInstant` is a month boundary + public func roundedToNearestMonth() -> Self { + round(to: Month.self, direction: .nearest) + } + + /// Retrieve the nearest `Fixed` + public var nearestMonth: Fixed { roundedToNearestMonth().fixedMonth } } extension Fixed where Granularity: LTOEHour { - - /// Round this fixed value towards a day boundary - /// - Parameter direction: The preferred rounding direction - /// - Returns: A fixed value whose `.firstInstant` is a day boundary - public func roundedToDay(direction: RoundingDirection) -> Self { - round(to: Day.self, direction: direction) - } - - /// Round this fixed value towards the nearest day boundary - /// - /// Equivalent to `.roundedToDay(direction: .nearest)` - /// - /// - Returns: A fixed value whose `.firstInstant` is a day boundary - public func roundedToNearestDay() -> Self { - round(to: Day.self, direction: .nearest) - } - - /// Retrieve the nearest `Fixed` - public var nearestDay: Fixed { roundedToNearestDay().fixedDay } + + /// Round this fixed value towards a day boundary + /// - Parameter direction: The preferred rounding direction + /// - Returns: A fixed value whose `.firstInstant` is a day boundary + public func roundedToDay(direction: RoundingDirection) -> Self { + round(to: Day.self, direction: direction) + } + + /// Round this fixed value towards the nearest day boundary + /// + /// Equivalent to `.roundedToDay(direction: .nearest)` + /// + /// - Returns: A fixed value whose `.firstInstant` is a day boundary + public func roundedToNearestDay() -> Self { + round(to: Day.self, direction: .nearest) + } + + /// Retrieve the nearest `Fixed` + public var nearestDay: Fixed { roundedToNearestDay().fixedDay } } extension Fixed where Granularity: LTOEMinute { - - /// Round this fixed value towards an hour boundary - /// - Parameter direction: The preferred rounding direction - /// - Returns: A fixed value whose `.firstInstant` is an hour boundary - public func roundedToHour(direction: RoundingDirection) -> Self { - round(to: Hour.self, direction: direction) - } - - /// Round this fixed value towards the nearest hour boundary - /// - /// Equivalent to `.roundedToHour(direction: .nearest)` - /// - /// - Returns: A fixed value whose `.firstInstant` is an hour boundary - public func roundedToNearestHour() -> Self { - round(to: Hour.self, direction: .nearest) - } - - /// Retrieve the nearest `Fixed` - public var nearestHour: Fixed { roundedToNearestHour().fixedHour } + + /// Round this fixed value towards an hour boundary + /// - Parameter direction: The preferred rounding direction + /// - Returns: A fixed value whose `.firstInstant` is an hour boundary + public func roundedToHour(direction: RoundingDirection) -> Self { + round(to: Hour.self, direction: direction) + } + + /// Round this fixed value towards the nearest hour boundary + /// + /// Equivalent to `.roundedToHour(direction: .nearest)` + /// + /// - Returns: A fixed value whose `.firstInstant` is an hour boundary + public func roundedToNearestHour() -> Self { + round(to: Hour.self, direction: .nearest) + } + + /// Retrieve the nearest `Fixed` + public var nearestHour: Fixed { roundedToNearestHour().fixedHour } } extension Fixed where Granularity: LTOESecond { - - /// Round this fixed value towards a minute boundary - /// - Parameter direction: The preferred rounding direction - /// - Returns: A fixed value whose `.firstInstant` is a minute boundary - public func roundedToMinute(direction: RoundingDirection) -> Self { - round(to: Minute.self, direction: direction) - } - - /// Round this fixed value towards the nearest minute boundary - /// - /// Equivalent to `.roundedToMinute(direction: .nearest)` - /// - /// - Returns: A fixed value whose `.firstInstant` is a minute boundary - public func roundedToNearestMinute() -> Self { - round(to: Minute.self, direction: .nearest) - } - - /// Retrieve the nearest `Fixed` - public var nearestMinute: Fixed { roundedToNearestMinute().fixedMinute } + + /// Round this fixed value towards a minute boundary + /// - Parameter direction: The preferred rounding direction + /// - Returns: A fixed value whose `.firstInstant` is a minute boundary + public func roundedToMinute(direction: RoundingDirection) -> Self { + round(to: Minute.self, direction: direction) + } + + /// Round this fixed value towards the nearest minute boundary + /// + /// Equivalent to `.roundedToMinute(direction: .nearest)` + /// + /// - Returns: A fixed value whose `.firstInstant` is a minute boundary + public func roundedToNearestMinute() -> Self { + round(to: Minute.self, direction: .nearest) + } + + /// Retrieve the nearest `Fixed` + public var nearestMinute: Fixed { roundedToNearestMinute().fixedMinute } } extension Fixed where Granularity: LTOENanosecond { - - /// Round this fixed value towards a second boundary - /// - Parameter direction: The preferred rounding direction - /// - Returns: A fixed value whose `.firstInstant` is a second boundary - public func roundedToSecond(direction: RoundingDirection) -> Self { - round(to: Second.self, direction: direction) - } - - /// Round this fixed value towards the nearest second boundary - /// - /// Equivalent to `.roundedToSecond(direction: .nearest)` - /// - /// - Returns: A fixed value whose `.firstInstant` is a second boundary - public func roundedToNearestSecond() -> Self { - round(to: Second.self, direction: .nearest) - } - - /// Retrieve the nearest `Fixed` - public var nearestSecond: Fixed { roundedToNearestSecond().fixedSecond } + + /// Round this fixed value towards a second boundary + /// - Parameter direction: The preferred rounding direction + /// - Returns: A fixed value whose `.firstInstant` is a second boundary + public func roundedToSecond(direction: RoundingDirection) -> Self { + round(to: Second.self, direction: direction) + } + + /// Round this fixed value towards the nearest second boundary + /// + /// Equivalent to `.roundedToSecond(direction: .nearest)` + /// + /// - Returns: A fixed value whose `.firstInstant` is a second boundary + public func roundedToNearestSecond() -> Self { + round(to: Second.self, direction: .nearest) + } + + /// Retrieve the nearest `Fixed` + public var nearestSecond: Fixed { roundedToNearestSecond().fixedSecond } } diff --git a/Sources/Time/2-Core Calendar/Region.swift b/Sources/Time/2-Core Calendar/Region.swift index e0f4516..a64caa4 100644 --- a/Sources/Time/2-Core Calendar/Region.swift +++ b/Sources/Time/2-Core Calendar/Region.swift @@ -1,7 +1,7 @@ #if os(Linux) -@preconcurrency import Foundation + @preconcurrency import Foundation #else -import Foundation + import Foundation #endif /// A `Region` contains all of the information necessary to refer to a user's preferences for expressing calendrical values. @@ -11,112 +11,114 @@ import Foundation /// - a `TimeZone` value, which describes their local application of the calendar /// - a `Locale` value, which describes their preferences around formatting values public struct Region: Hashable, Sendable { - - public static func ==(lhs: Self, rhs: Self) -> Bool { - guard lhs.locale.isEquivalent(to: rhs.locale) else { return false } - guard lhs.timeZone.isEquivalent(to: rhs.timeZone) else { return false } - guard lhs.calendar.isEquivalent(to: rhs.calendar) else { return false } - - return true - } - - /// A snapshot of the user's current `Region`. - public static let current = Region(calendar: .current, timeZone: .current, locale: .current) - - /// The POSIX region: the Gregorian calendar in the UTC time zone, using the `en_US_POSIX` locale. - public static let posix = Region(calendar: Calendar(identifier: .gregorian), - timeZone: TimeZone(secondsFromGMT: 0)!, - locale: Locale(identifier: "en_US_POSIX")) - - /// The "autoupdating" current region. This Region will automatically track changes to the user's selected time zone, calendar, and locale. - public static let autoupdatingCurrent = Region(autoupdating: ()) - - /// The `Calendar` used in this `Region`. - public let calendar: Calendar - - /// The `TimeZone` used in this `Region`. - public let timeZone: TimeZone - - /// The `Locale` used in this `Region`. - public let locale: Locale - - internal let isAutoupdating: Bool - - private init(autoupdating: Void = ()) { - self.calendar = .autoupdatingCurrent - self.timeZone = .autoupdatingCurrent - self.locale = .autoupdatingCurrent - self.isAutoupdating = true - } - - /// Construct a `Region` given a `Calendar`, `TimeZone`, and `Locale`. - /// - /// The constructed region *always* uses a snapshot of the provided calendar, locale, and time zone. This means that - /// if you attempt to pass in any of the `.autoupdatingCurrent` values, they will be "frozen" and the region will not - /// automatically update to reflect any changes the user makes to their time zone, locale, or calendar - /// while the process is running. - /// - /// The only way to get a `Region` with autoupdating values is to use ``Region/autoupdatingCurrent``. - /// - /// - Parameters: - /// - calendar: The region's `Calendar` - /// - timeZone: The region's `TimeZone` - /// - locale: The region's `Locale` - public init(calendar: Calendar, timeZone: TimeZone, locale: Locale) { - if calendar.timeZone != timeZone || calendar.locale != locale { - var actualCalendar = calendar.snapshot(forcedCopy: false) - actualCalendar.timeZone = timeZone - actualCalendar.locale = locale - - self.calendar = actualCalendar - } else { - self.calendar = calendar.snapshot(forcedCopy: false) - } - self.timeZone = timeZone.snapshot(forcedCopy: false) - self.locale = locale.snapshot(forcedCopy: false) - self.isAutoupdating = false - } - /// Create a "deep" copy of the receiver. - /// - /// This method is useful if you're on a platform that doesn't provide thread safety for the underlying date - /// primatives, most notably Linux at the time of writing (mid-2023). If you're using `Region` objects in a - /// multithreaded environment and are seeing odd behaviour, you may need to work with copies. - /// - /// For more detail, see the discussion on `Fixed._forcedCopy()`. - public func _forcedCopy() -> Self { - return self.snapshot(forced: true) - } - - /// Indicates whether time values in this region will be formatted using 12-hour ("1:00 PM") or 24-hour ("13:00") time. - public var wants24HourTime: Bool { locale.wants24HourTime } - - public func setTimeZone(_ timeZone: TimeZone) -> Region { - if timeZone == self.timeZone { return self } - return Region(calendar: self.calendar, timeZone: timeZone, locale: self.locale) - } - - public func setCalendar(_ calendar: Calendar) -> Region { - if calendar == self.calendar { return self } - return Region(calendar: calendar, timeZone: self.timeZone, locale: self.locale) - } - - public func setLocale(_ locale: Locale) -> Region { - if locale == self.locale { return self } - return Region(calendar: self.calendar, timeZone: self.timeZone, locale: locale) - } - - internal func snapshot(forced: Bool) -> Region { - if forced == false && self.isAutoupdating == false { return self } - - return Region(calendar: calendar.snapshot(forcedCopy: forced), - timeZone: timeZone.snapshot(forcedCopy: forced), - locale: locale.snapshot(forcedCopy: forced)) - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(calendar.identifier) - hasher.combine(timeZone.identifier) - hasher.combine(locale.identifier) + public static func == (lhs: Self, rhs: Self) -> Bool { + guard lhs.locale.isEquivalent(to: rhs.locale) else { return false } + guard lhs.timeZone.isEquivalent(to: rhs.timeZone) else { return false } + guard lhs.calendar.isEquivalent(to: rhs.calendar) else { return false } + + return true + } + + /// A snapshot of the user's current `Region`. + public static let current = Region(calendar: .current, timeZone: .current, locale: .current) + + /// The POSIX region: the Gregorian calendar in the UTC time zone, using the `en_US_POSIX` locale. + public static let posix = Region( + calendar: Calendar(identifier: .gregorian), + timeZone: TimeZone(secondsFromGMT: 0)!, + locale: Locale(identifier: "en_US_POSIX")) + + /// The "autoupdating" current region. This Region will automatically track changes to the user's selected time zone, calendar, and locale. + public static let autoupdatingCurrent = Region(autoupdating: ()) + + /// The `Calendar` used in this `Region`. + public let calendar: Calendar + + /// The `TimeZone` used in this `Region`. + public let timeZone: TimeZone + + /// The `Locale` used in this `Region`. + public let locale: Locale + + internal let isAutoupdating: Bool + + private init(autoupdating: Void = ()) { + self.calendar = .autoupdatingCurrent + self.timeZone = .autoupdatingCurrent + self.locale = .autoupdatingCurrent + self.isAutoupdating = true + } + + /// Construct a `Region` given a `Calendar`, `TimeZone`, and `Locale`. + /// + /// The constructed region *always* uses a snapshot of the provided calendar, locale, and time zone. This means that + /// if you attempt to pass in any of the `.autoupdatingCurrent` values, they will be "frozen" and the region will not + /// automatically update to reflect any changes the user makes to their time zone, locale, or calendar + /// while the process is running. + /// + /// The only way to get a `Region` with autoupdating values is to use ``Region/autoupdatingCurrent``. + /// + /// - Parameters: + /// - calendar: The region's `Calendar` + /// - timeZone: The region's `TimeZone` + /// - locale: The region's `Locale` + public init(calendar: Calendar, timeZone: TimeZone, locale: Locale) { + if calendar.timeZone != timeZone || calendar.locale != locale { + var actualCalendar = calendar.snapshot(forcedCopy: false) + actualCalendar.timeZone = timeZone + actualCalendar.locale = locale + + self.calendar = actualCalendar + } else { + self.calendar = calendar.snapshot(forcedCopy: false) } + self.timeZone = timeZone.snapshot(forcedCopy: false) + self.locale = locale.snapshot(forcedCopy: false) + self.isAutoupdating = false + } + + /// Create a "deep" copy of the receiver. + /// + /// This method is useful if you're on a platform that doesn't provide thread safety for the underlying date + /// primatives, most notably Linux at the time of writing (mid-2023). If you're using `Region` objects in a + /// multithreaded environment and are seeing odd behaviour, you may need to work with copies. + /// + /// For more detail, see the discussion on `Fixed._forcedCopy()`. + public func _forcedCopy() -> Self { + return self.snapshot(forced: true) + } + + /// Indicates whether time values in this region will be formatted using 12-hour ("1:00 PM") or 24-hour ("13:00") time. + public var wants24HourTime: Bool { locale.wants24HourTime } + + public func setTimeZone(_ timeZone: TimeZone) -> Region { + if timeZone == self.timeZone { return self } + return Region(calendar: self.calendar, timeZone: timeZone, locale: self.locale) + } + + public func setCalendar(_ calendar: Calendar) -> Region { + if calendar == self.calendar { return self } + return Region(calendar: calendar, timeZone: self.timeZone, locale: self.locale) + } + + public func setLocale(_ locale: Locale) -> Region { + if locale == self.locale { return self } + return Region(calendar: self.calendar, timeZone: self.timeZone, locale: locale) + } + + internal func snapshot(forced: Bool) -> Region { + if forced == false && self.isAutoupdating == false { return self } + + return Region( + calendar: calendar.snapshot(forcedCopy: forced), + timeZone: timeZone.snapshot(forcedCopy: forced), + locale: locale.snapshot(forcedCopy: forced)) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(calendar.identifier) + hasher.combine(timeZone.identifier) + hasher.combine(locale.identifier) + } } diff --git a/Sources/Time/2-Core Calendar/TimeError.swift b/Sources/Time/2-Core Calendar/TimeError.swift index b672994..fcb3715 100644 --- a/Sources/Time/2-Core Calendar/TimeError.swift +++ b/Sources/Time/2-Core Calendar/TimeError.swift @@ -1,159 +1,176 @@ #if os(Linux) -@preconcurrency import Foundation + @preconcurrency import Foundation #else -import Foundation + import Foundation #endif /// A type describing the possible ways that a calendrical operation might fail public struct TimeError: Error, Sendable, CustomStringConvertible { - - /// The specific operation that failed - public enum Reason: Hashable, Sendable { - - /// An operation failed because some units that are required were missing - case missingUnits - - /// An operation failed because the supplied components were not valid - case invalidComponents - - /// An operation failed because a supplied format string was incorrect - case invalidFormatString - - /// An operation failed because a supplied date string was incorrect - case invalidValueString - - /// An error occured while decoding a fixed value - case decodingError - - /// An error occured while encoding a fixed value - case encodingError - } - - /// The reason the operation failed - public let reason: Reason - - /// Any units that were incorrect - public let units: Set? - - /// The incorrect `DateComponents`, if any - public let dateComponents: DateComponents? - - /// The region used as part of the operation that failed, if any - public let region: Region? - - /// The format string used in the failing operation, if any - public let formatString: String? - - /// The date string used in the failing operation, if any - public let valueString: String? - - /// Another error that occurred during the operation, if any - public let underlyingError: Error? - - /// An unlocalized and developer-readable description of the error - public let description: String - + + /// The specific operation that failed + public enum Reason: Hashable, Sendable { + + /// An operation failed because some units that are required were missing + case missingUnits + + /// An operation failed because the supplied components were not valid + case invalidComponents + + /// An operation failed because a supplied format string was incorrect + case invalidFormatString + + /// An operation failed because a supplied date string was incorrect + case invalidValueString + + /// An error occured while decoding a fixed value + case decodingError + + /// An error occured while encoding a fixed value + case encodingError + } + + /// The reason the operation failed + public let reason: Reason + + /// Any units that were incorrect + public let units: Set? + + /// The incorrect `DateComponents`, if any + public let dateComponents: DateComponents? + + /// The region used as part of the operation that failed, if any + public let region: Region? + + /// The format string used in the failing operation, if any + public let formatString: String? + + /// The date string used in the failing operation, if any + public let valueString: String? + + /// Another error that occurred during the operation, if any + public let underlyingError: Error? + + /// An unlocalized and developer-readable description of the error + public let description: String + } extension TimeError { - - internal static func missingCalendarComponents(_ units: Set, - in dateComponents: DateComponents? = nil, - description: String? = nil) -> TimeError { - - let resolvedDescription: String - if let description { - resolvedDescription = description - } else if let dateComponents { - resolvedDescription = "The date components \(dateComponents) is missing units: \(units)" - } else { - resolvedDescription = "Required units are missing: \(units)" - } - - return .init(reason: .missingUnits, - units: units, - dateComponents: dateComponents, - region: nil, - formatString: nil, - valueString: nil, - underlyingError: nil, - description: resolvedDescription) - } - - internal static func invalidDateComponents(_ components: DateComponents, - units: Set? = nil, - in region: Region, - description: String? = nil) -> TimeError { - - return .init(reason: .invalidComponents, - units: units ?? components.representedComponents, - dateComponents: components, - region: region, - formatString: nil, - valueString: nil, - underlyingError: nil, - description: description ?? "The provided date components (\(components)) cannot be correctly interpreted in the \(region) region") - } - - internal static func invalidFormatString(_ string: String, - units: Set? = nil, - description: String? = nil) -> TimeError { - - return .init(reason: .invalidFormatString, - units: units, - dateComponents: nil, - region: nil, - formatString: string, - valueString: nil, - underlyingError: nil, - description: description ?? "Invalid format string: '\(string)'") - } - - internal static func cannotParseString(_ valueString: String, - formatString: String? = nil, - units: Set? = nil, - in region: Region, - description: String? = nil) -> TimeError { - - let resolvedDescription: String - if let description { - resolvedDescription = description - } else if let formatString { - resolvedDescription = "Cannot parse string '\(valueString)' with format '\(formatString)' in the \(region) region" - } else { - resolvedDescription = "Cannot parse string '\(valueString)' in the \(region) region" - } - - return .init(reason: .invalidValueString, - units: units, - dateComponents: nil, - region: region, - formatString: formatString, - valueString: valueString, - underlyingError: nil, - description: resolvedDescription) - } - - internal static func decodingError(_ fixedError: Error) -> TimeError { - return .init(reason: .decodingError, - units: nil, - dateComponents: nil, - region: nil, - formatString: nil, - valueString: nil, - underlyingError: fixedError, - description: "Cannot decode value from provided data: \(fixedError)") + + internal static func missingCalendarComponents( + _ units: Set, + in dateComponents: DateComponents? = nil, + description: String? = nil + ) -> TimeError { + + let resolvedDescription: String + if let description { + resolvedDescription = description + } else if let dateComponents { + resolvedDescription = "The date components \(dateComponents) is missing units: \(units)" + } else { + resolvedDescription = "Required units are missing: \(units)" } - - internal static func encodingError(_ fixedError: Error) -> TimeError { - return .init(reason: .encodingError, - units: nil, - dateComponents: nil, - region: nil, - formatString: nil, - valueString: nil, - underlyingError: fixedError, - description: "Cannot encode value: \(fixedError)") + + return .init( + reason: .missingUnits, + units: units, + dateComponents: dateComponents, + region: nil, + formatString: nil, + valueString: nil, + underlyingError: nil, + description: resolvedDescription) + } + + internal static func invalidDateComponents( + _ components: DateComponents, + units: Set? = nil, + in region: Region, + description: String? = nil + ) -> TimeError { + + return .init( + reason: .invalidComponents, + units: units ?? components.representedComponents, + dateComponents: components, + region: region, + formatString: nil, + valueString: nil, + underlyingError: nil, + description: description + ?? "The provided date components (\(components)) cannot be correctly interpreted in the \(region) region" + ) + } + + internal static func invalidFormatString( + _ string: String, + units: Set? = nil, + description: String? = nil + ) -> TimeError { + + return .init( + reason: .invalidFormatString, + units: units, + dateComponents: nil, + region: nil, + formatString: string, + valueString: nil, + underlyingError: nil, + description: description ?? "Invalid format string: '\(string)'") + } + + internal static func cannotParseString( + _ valueString: String, + formatString: String? = nil, + units: Set? = nil, + in region: Region, + description: String? = nil + ) -> TimeError { + + let resolvedDescription: String + if let description { + resolvedDescription = description + } else if let formatString { + resolvedDescription = + "Cannot parse string '\(valueString)' with format '\(formatString)' in the \(region) region" + } else { + resolvedDescription = "Cannot parse string '\(valueString)' in the \(region) region" } - + + return .init( + reason: .invalidValueString, + units: units, + dateComponents: nil, + region: region, + formatString: formatString, + valueString: valueString, + underlyingError: nil, + description: resolvedDescription) + } + + internal static func decodingError(_ fixedError: Error) -> TimeError { + return .init( + reason: .decodingError, + units: nil, + dateComponents: nil, + region: nil, + formatString: nil, + valueString: nil, + underlyingError: fixedError, + description: "Cannot decode value from provided data: \(fixedError)") + } + + internal static func encodingError(_ fixedError: Error) -> TimeError { + return .init( + reason: .encodingError, + units: nil, + dateComponents: nil, + region: nil, + formatString: nil, + valueString: nil, + underlyingError: fixedError, + description: "Cannot encode value: \(fixedError)") + } + } diff --git a/Sources/Time/2-Core Calendar/Units.swift b/Sources/Time/2-Core Calendar/Units.swift index 85182bb..52ddb7a 100644 --- a/Sources/Time/2-Core Calendar/Units.swift +++ b/Sources/Time/2-Core Calendar/Units.swift @@ -1,11 +1,11 @@ import Foundation /** - + These types form the basis of how the library defines calendrical values. - + For the most part, you should never need to interact with anything in this file. - + */ // the @_documentation attribute is from https://github.com/apple/swift/pull/60242 @@ -14,192 +14,190 @@ import Foundation /// /// - Warning: You may not implement this protocol. public protocol Unit { - #if swift(>=5.8) - @_documentation(visibility: internal) - #endif - static var _closer: ProtocolCloser { get } - static var component: Calendar.Component { get } + #if swift(>=5.8) + @_documentation(visibility:internal) + #endif + static var _closer: ProtocolCloser { get } + static var component: Calendar.Component { get } } /// The representation of nanoseconds to the Swift type system /// /// This type is always used as a generic parameter, such as in `Fixed`. public enum Nanosecond: Unit, LTOENanosecond, GTOENanosecond { - #if swift(>=5.8) - @_documentation(visibility: internal) - #endif - public static let _closer: ProtocolCloser = ProtocolCloser() - - /// This type represents the `.nanosecond` calendar component - public static let component: Calendar.Component = .nanosecond + #if swift(>=5.8) + @_documentation(visibility:internal) + #endif + public static let _closer: ProtocolCloser = ProtocolCloser() + + /// This type represents the `.nanosecond` calendar component + public static let component: Calendar.Component = .nanosecond } /// The representation of seconds to the Swift type system public enum Second: Unit, LTOESecond, GTOESecond { - #if swift(>=5.8) - @_documentation(visibility: internal) - #endif - public static let _closer: ProtocolCloser = ProtocolCloser() - - /// This type represents the `.second` calendar component - public static let component: Calendar.Component = .second + #if swift(>=5.8) + @_documentation(visibility:internal) + #endif + public static let _closer: ProtocolCloser = ProtocolCloser() + + /// This type represents the `.second` calendar component + public static let component: Calendar.Component = .second } /// The representation of minutes to the Swift type system public enum Minute: Unit, LTOEMinute, GTOEMinute { - #if swift(>=5.8) - @_documentation(visibility: internal) - #endif - public static let _closer: ProtocolCloser = ProtocolCloser() - - /// This type represents the `.minute` calendar component - public static let component: Calendar.Component = .minute + #if swift(>=5.8) + @_documentation(visibility:internal) + #endif + public static let _closer: ProtocolCloser = ProtocolCloser() + + /// This type represents the `.minute` calendar component + public static let component: Calendar.Component = .minute } /// The representation of hours to the Swift type system public enum Hour: Unit, LTOEHour, GTOEHour { - #if swift(>=5.8) - @_documentation(visibility: internal) - #endif - public static let _closer: ProtocolCloser = ProtocolCloser() - - /// This type represents the `.hour` calendar component - public static let component: Calendar.Component = .hour + #if swift(>=5.8) + @_documentation(visibility:internal) + #endif + public static let _closer: ProtocolCloser = ProtocolCloser() + + /// This type represents the `.hour` calendar component + public static let component: Calendar.Component = .hour } /// The representation of days to the Swift type system public enum Day: Unit, LTOEDay, GTOEDay { - #if swift(>=5.8) - @_documentation(visibility: internal) - #endif - public static let _closer: ProtocolCloser = ProtocolCloser() - - /// This type represents the `.day` calendar component - public static let component: Calendar.Component = .day + #if swift(>=5.8) + @_documentation(visibility:internal) + #endif + public static let _closer: ProtocolCloser = ProtocolCloser() + + /// This type represents the `.day` calendar component + public static let component: Calendar.Component = .day } /// The representation of months to the Swift type system public enum Month: Unit, LTOEMonth, GTOEMonth { - #if swift(>=5.8) - @_documentation(visibility: internal) - #endif - public static let _closer: ProtocolCloser = ProtocolCloser() - - /// This type represents the `.month` calendar component - public static let component: Calendar.Component = .month + #if swift(>=5.8) + @_documentation(visibility:internal) + #endif + public static let _closer: ProtocolCloser = ProtocolCloser() + + /// This type represents the `.month` calendar component + public static let component: Calendar.Component = .month } /// The representation of years to the Swift type system public enum Year: Unit, LTOEYear, GTOEYear { - #if swift(>=5.8) - @_documentation(visibility: internal) - #endif - public static let _closer: ProtocolCloser = ProtocolCloser() - - /// This type represents the `.year` calendar component - public static let component: Calendar.Component = .year + #if swift(>=5.8) + @_documentation(visibility:internal) + #endif + public static let _closer: ProtocolCloser = ProtocolCloser() + + /// This type represents the `.year` calendar component + public static let component: Calendar.Component = .year } /// The representation of eras to the Swift type system public enum Era: Unit, LTOEEra, GTOEEra { - #if swift(>=5.8) - @_documentation(visibility: internal) - #endif - public static let _closer: ProtocolCloser = ProtocolCloser() - - /// This type represents the `.era` calendar component - public static let component: Calendar.Component = .era + #if swift(>=5.8) + @_documentation(visibility:internal) + #endif + public static let _closer: ProtocolCloser = ProtocolCloser() + + /// This type represents the `.era` calendar component + public static let component: Calendar.Component = .era } // protocols used to define relative unit durations #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol LTOEEra: Unit { } +public protocol LTOEEra: Unit {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol LTOEYear: LTOEEra { } +public protocol LTOEYear: LTOEEra {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol LTOEMonth: LTOEYear { } +public protocol LTOEMonth: LTOEYear {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol LTOEDay: LTOEMonth { } +public protocol LTOEDay: LTOEMonth {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol LTOEHour: LTOEDay { } +public protocol LTOEHour: LTOEDay {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol LTOEMinute: LTOEHour { } +public protocol LTOEMinute: LTOEHour {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol LTOESecond: LTOEMinute { } +public protocol LTOESecond: LTOEMinute {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol LTOENanosecond: LTOESecond { } - +public protocol LTOENanosecond: LTOESecond {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol GTOENanosecond: Unit { } +public protocol GTOENanosecond: Unit {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol GTOESecond: GTOENanosecond { } +public protocol GTOESecond: GTOENanosecond {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol GTOEMinute: GTOESecond { } +public protocol GTOEMinute: GTOESecond {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol GTOEHour: GTOEMinute { } +public protocol GTOEHour: GTOEMinute {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol GTOEDay: GTOEHour { } +public protocol GTOEDay: GTOEHour {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol GTOEMonth: GTOEDay { } +public protocol GTOEMonth: GTOEDay {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol GTOEYear: GTOEMonth { } +public protocol GTOEYear: GTOEMonth {} #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif -public protocol GTOEEra: GTOEYear { } +public protocol GTOEEra: GTOEYear {} // A type used to prevent external types from adopting the Unit protocol #if swift(>=5.8) -@_documentation(visibility: internal) + @_documentation(visibility:internal) #endif public struct ProtocolCloser { - fileprivate init() { } + fileprivate init() {} } - diff --git a/Sources/Time/3-RegionalClock/Clocks.swift b/Sources/Time/3-RegionalClock/Clocks.swift index f48314c..db59f56 100644 --- a/Sources/Time/3-RegionalClock/Clocks.swift +++ b/Sources/Time/3-RegionalClock/Clocks.swift @@ -1,50 +1,57 @@ import Foundation /// A namespace for retrieving commonly-used clocks +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) public enum Clocks { - - /// The system clock. This clock uses the current `Region` and follows the current device time. - public static let system: any RegionalClock = SystemClock(region: .autoupdatingCurrent) - - /// A POSIX clock. This clock uses the POSIX `Region` and follows the current device time. - public static let posix: any RegionalClock = SystemClock(region: .posix) - - /// Create a clock that follows the current device time, - /// but produces calendar values according to the specified `Region`. - public static func system(in region: Region) -> any RegionalClock { - return SystemClock(region: region) - } - - /// Create a ``RegionalClock`` with a custom start time and flow rate. - /// - /// - Parameter referenceDate: The instantaneous "now" from which the clock will start counting. - /// - Parameter rate: The rate at which time progresses in the clock, relative to the supplied calendar. - /// `1.0` (the default) means one second on the system clock correlates to a second passing in the clock. - /// `2.0` would mean that every second elapsing on the system clock would be 2 seconds on this clock (ie, time progresses twice as fast). - /// - Parameter region: The ``Region`` in which calendar values are produced. - /// - /// - Note: The `rate` must be strictly greater than `0`. A value less than or equal to `0` will cause a crash. - public static func custom(startingFrom referenceInstant: Instant, rate: Double = 1.0, region: Region = .autoupdatingCurrent) -> any RegionalClock { - guard rate > 0 else { - fatalError("You cannot create a clock where time has stopped or flows backwards") - } - - return CustomClock(referenceInstant: referenceInstant, - rate: rate, - region: region) - } - - /// Create a clock with a custom start time and flow rate. - /// - /// - Parameter referenceEpoch: The instantaneous "now" from which the clock will start counting. - /// - Parameter rate: The rate at which time progresses in the clock, relative to the supplied calendar. - /// `1.0` (the default) means one second on the system clock correlates to a second passing in the clock. - /// `2.0` would mean that every second elapsing on the system clock would be 2 seconds on this clock (ie, time progresses twice as fast). - /// - Parameter region: The `Region` in which calendar values are produced. - /// - /// - Note: The `rate` must be strictly greater than `0`. A value less than or equal to `0` will cause a crash. - public static func custom(startingFrom referenceEpoch: Epoch, rate: Double = 1.0, region: Region = .autoupdatingCurrent) -> any RegionalClock { - let referenceInstant = Instant(interval: 0, since: referenceEpoch) - return self.custom(startingFrom: referenceInstant, rate: rate, region: region) + + /// The system clock. This clock uses the current `Region` and follows the current device time. + public static let system: any RegionalClock = SystemClock(region: .autoupdatingCurrent) + + /// A POSIX clock. This clock uses the POSIX `Region` and follows the current device time. + public static let posix: any RegionalClock = SystemClock(region: .posix) + + /// Create a clock that follows the current device time, + /// but produces calendar values according to the specified `Region`. + public static func system(in region: Region) -> any RegionalClock { + return SystemClock(region: region) + } + + /// Create a ``RegionalClock`` with a custom start time and flow rate. + /// + /// - Parameter referenceDate: The instantaneous "now" from which the clock will start counting. + /// - Parameter rate: The rate at which time progresses in the clock, relative to the supplied calendar. + /// `1.0` (the default) means one second on the system clock correlates to a second passing in the clock. + /// `2.0` would mean that every second elapsing on the system clock would be 2 seconds on this clock (ie, time progresses twice as fast). + /// - Parameter region: The ``Region`` in which calendar values are produced. + /// + /// - Note: The `rate` must be strictly greater than `0`. A value less than or equal to `0` will cause a crash. + public static func custom( + startingFrom referenceInstant: Instant, rate: Double = 1.0, + region: Region = .autoupdatingCurrent + ) -> any RegionalClock { + guard rate > 0 else { + fatalError("You cannot create a clock where time has stopped or flows backwards") } + + return CustomClock( + referenceInstant: referenceInstant, + rate: rate, + region: region) + } + + /// Create a clock with a custom start time and flow rate. + /// + /// - Parameter referenceEpoch: The instantaneous "now" from which the clock will start counting. + /// - Parameter rate: The rate at which time progresses in the clock, relative to the supplied calendar. + /// `1.0` (the default) means one second on the system clock correlates to a second passing in the clock. + /// `2.0` would mean that every second elapsing on the system clock would be 2 seconds on this clock (ie, time progresses twice as fast). + /// - Parameter region: The `Region` in which calendar values are produced. + /// + /// - Note: The `rate` must be strictly greater than `0`. A value less than or equal to `0` will cause a crash. + public static func custom( + startingFrom referenceEpoch: Epoch, rate: Double = 1.0, region: Region = .autoupdatingCurrent + ) -> any RegionalClock { + let referenceInstant = Instant(interval: 0, since: referenceEpoch) + return self.custom(startingFrom: referenceInstant, rate: rate, region: region) + } } diff --git a/Sources/Time/3-RegionalClock/RegionalClock+CurrentValues.swift b/Sources/Time/3-RegionalClock/RegionalClock+CurrentValues.swift index 88b4735..a1c6b05 100644 --- a/Sources/Time/3-RegionalClock/RegionalClock+CurrentValues.swift +++ b/Sources/Time/3-RegionalClock/RegionalClock+CurrentValues.swift @@ -1,81 +1,82 @@ import Foundation +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) extension RegionalClock { - - /// Retrieve the current `Fixed` calendrical value, accurate down to the specified unit. - public func current(_ unit: U.Type = U.self) -> Fixed { - return Fixed(region: self.region, instant: self.now) - } - - /// Retrieve the current calendar day of the `RegionalClock`. - /// - /// This is equivalent to `.current(Day.self)` - public var today: Fixed { return current() } - - /// Retrieve the next calendar day of this `RegionalClock` - public var tomorrow: Fixed { return current().next } - - /// Retrieve the previous calendar day of this `RegionalClock` - public var yesterday: Fixed { return current().previous } - - /// Retrieve the current calendar era of the `RegionalClock`. - public var currentEra: Fixed { return current() } - - /// Retrieve the current calendar year of the `RegionalClock`. - public var currentYear: Fixed { return current() } - - /// Retrieve the current calendar month of the `RegionalClock`. - public var currentMonth: Fixed { return current() } - - /// Retrieve the current calendar day of the `RegionalClock`. - public var currentDay: Fixed { return current() } - - /// Retrieve the current calendar hour of the `RegionalClock`. - public var currentHour: Fixed { return current() } - - /// Retrieve the current calendar minute of the `RegionalClock`. - public var currentMinute: Fixed { return current() } - - /// Retrieve the current calendar second of the `RegionalClock`. - public var currentSecond: Fixed { return current() } - - /// Retrieve the current calendar nanosecond of the `RegionalClock`. - public var currentNanosecond: Fixed { return current() } - - /// Retrieve the next calendar year of the `RegionalClock` - public var nextYear: Fixed { return current().next } - - /// Retrieve the next calendar month of the `RegionalClock` - public var nextMonth: Fixed { return current().next } - - /// Retrieve the next calendar day of the `RegionalClock` - public var nextDay: Fixed { return current().next } - - /// Retrieve the next calendar hour of the `RegionalClock` - public var nextHour: Fixed { return current().next } - - /// Retrieve the next calendar minute of the `RegionalClock` - public var nextMinute: Fixed { return current().next } - - /// Retrieve the next calendar second of the `RegionalClock` - public var nextSecond: Fixed { return current().next } - - /// Retrieve the previous calendar year of the `RegionalClock` - public var previousYear: Fixed { return current().previous } - - /// Retrieve the previous calendar month of the `RegionalClock` - public var previousMonth: Fixed { return current().previous } - - /// Retrieve the previous calendar day of the `RegionalClock` - public var previousDay: Fixed { return current().previous } - - /// Retrieve the previous calendar hour of the `RegionalClock` - public var previousHour: Fixed { return current().previous } - - /// Retrieve the previous calendar minute of the `RegionalClock` - public var previousMinute: Fixed { return current().previous } - - /// Retrieve the previous calendar second of the `RegionalClock` - public var previousSecond: Fixed { return current().previous } - + + /// Retrieve the current `Fixed` calendrical value, accurate down to the specified unit. + public func current(_ unit: U.Type = U.self) -> Fixed { + return Fixed(region: self.region, instant: self.now) + } + + /// Retrieve the current calendar day of the `RegionalClock`. + /// + /// This is equivalent to `.current(Day.self)` + public var today: Fixed { return current() } + + /// Retrieve the next calendar day of this `RegionalClock` + public var tomorrow: Fixed { return current().next } + + /// Retrieve the previous calendar day of this `RegionalClock` + public var yesterday: Fixed { return current().previous } + + /// Retrieve the current calendar era of the `RegionalClock`. + public var currentEra: Fixed { return current() } + + /// Retrieve the current calendar year of the `RegionalClock`. + public var currentYear: Fixed { return current() } + + /// Retrieve the current calendar month of the `RegionalClock`. + public var currentMonth: Fixed { return current() } + + /// Retrieve the current calendar day of the `RegionalClock`. + public var currentDay: Fixed { return current() } + + /// Retrieve the current calendar hour of the `RegionalClock`. + public var currentHour: Fixed { return current() } + + /// Retrieve the current calendar minute of the `RegionalClock`. + public var currentMinute: Fixed { return current() } + + /// Retrieve the current calendar second of the `RegionalClock`. + public var currentSecond: Fixed { return current() } + + /// Retrieve the current calendar nanosecond of the `RegionalClock`. + public var currentNanosecond: Fixed { return current() } + + /// Retrieve the next calendar year of the `RegionalClock` + public var nextYear: Fixed { return current().next } + + /// Retrieve the next calendar month of the `RegionalClock` + public var nextMonth: Fixed { return current().next } + + /// Retrieve the next calendar day of the `RegionalClock` + public var nextDay: Fixed { return current().next } + + /// Retrieve the next calendar hour of the `RegionalClock` + public var nextHour: Fixed { return current().next } + + /// Retrieve the next calendar minute of the `RegionalClock` + public var nextMinute: Fixed { return current().next } + + /// Retrieve the next calendar second of the `RegionalClock` + public var nextSecond: Fixed { return current().next } + + /// Retrieve the previous calendar year of the `RegionalClock` + public var previousYear: Fixed { return current().previous } + + /// Retrieve the previous calendar month of the `RegionalClock` + public var previousMonth: Fixed { return current().previous } + + /// Retrieve the previous calendar day of the `RegionalClock` + public var previousDay: Fixed { return current().previous } + + /// Retrieve the previous calendar hour of the `RegionalClock` + public var previousHour: Fixed { return current().previous } + + /// Retrieve the previous calendar minute of the `RegionalClock` + public var previousMinute: Fixed { return current().previous } + + /// Retrieve the previous calendar second of the `RegionalClock` + public var previousSecond: Fixed { return current().previous } + } diff --git a/Sources/Time/3-RegionalClock/RegionalClock+Implementations.swift b/Sources/Time/3-RegionalClock/RegionalClock+Implementations.swift index 14ad688..8dff67c 100644 --- a/Sources/Time/3-RegionalClock/RegionalClock+Implementations.swift +++ b/Sources/Time/3-RegionalClock/RegionalClock+Implementations.swift @@ -1,112 +1,117 @@ import Foundation /// The device's wall clock +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) internal struct SystemClock: RegionalClock { - internal let region: Region - - internal init(region: Region) { - self.region = region - } - - internal var now: Instant { - return Instant(date: Date()) - } - + internal let region: Region + + internal init(region: Region) { + self.region = region + } + + internal var now: Instant { + return Instant(date: Date()) + } + } /// A clock that offsets another clock by a specified interval +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) internal struct OffsetClock: RegionalClock { - - internal let base: any RegionalClock - internal let offset: SISeconds - - internal var region: Region { base.region } - internal var SISecondsPerClockSecond: Double { base.SISecondsPerClockSecond } - - internal init(offset: SISeconds, from base: any RegionalClock) { - self.base = base - self.offset = offset - } - - internal var now: Instant { - return base.now + offset - } - + + internal let base: any RegionalClock + internal let offset: SISeconds + + internal var region: Region { base.region } + internal var SISecondsPerClockSecond: Double { base.SISecondsPerClockSecond } + + internal init(offset: SISeconds, from base: any RegionalClock) { + self.base = base + self.offset = offset + } + + internal var now: Instant { + return base.now + offset + } + } /// A clock that scales another clock by a specified rate +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) internal struct ScaledClock: RegionalClock { - - internal let base: any RegionalClock - internal let scale: Double - - internal var region: Region { base.region } - internal var SISecondsPerClockSecond: Double { scale * base.SISecondsPerClockSecond } - - private let startTime: Instant - - internal init(scale: Double, from base: any RegionalClock) { - guard scale > 0 else { - fatalError("You cannot create a clock where time has stopped or flows backwards") - } - self.base = base - self.scale = scale - self.startTime = base.now - } - - internal var now: Instant { - let baseNow = base.now - - // these are the number of seconds that have elapsed on the base clock - let elapsedTime = baseNow - startTime - - let scaledTime = elapsedTime * scale - return Instant(interval: scaledTime, since: startTime.epoch) + + internal let base: any RegionalClock + internal let scale: Double + + internal var region: Region { base.region } + internal var SISecondsPerClockSecond: Double { scale * base.SISecondsPerClockSecond } + + private let startTime: Instant + + internal init(scale: Double, from base: any RegionalClock) { + guard scale > 0 else { + fatalError("You cannot create a clock where time has stopped or flows backwards") } - + self.base = base + self.scale = scale + self.startTime = base.now + } + + internal var now: Instant { + let baseNow = base.now + + // these are the number of seconds that have elapsed on the base clock + let elapsedTime = baseNow - startTime + + let scaledTime = elapsedTime * scale + return Instant(interval: scaledTime, since: startTime.epoch) + } + } /// A clock that starts from a specific instant and moves at a specific rate. +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) internal struct CustomClock: RegionalClock { - - let systemStart: Instant - let clockStart: Instant - let rate: Double - let region: Region - let SISecondsPerClockSecond: Double - - init(referenceInstant: Instant, rate: Double, region: Region) { - self.systemStart = Clocks.system.now - self.clockStart = referenceInstant - self.region = region - self.rate = rate - self.SISecondsPerClockSecond = rate * region.calendar.SISecondsPerSecond - } - - internal var now: Instant { - let systemNow = Clocks.system.now - let elapsedTime = systemNow - systemStart - let scaledElapsedTime = elapsedTime * SISecondsPerClockSecond - return clockStart + scaledElapsedTime - } - + + let systemStart: Instant + let clockStart: Instant + let rate: Double + let region: Region + let SISecondsPerClockSecond: Double + + init(referenceInstant: Instant, rate: Double, region: Region) { + self.systemStart = Clocks.system.now + self.clockStart = referenceInstant + self.region = region + self.rate = rate + self.SISecondsPerClockSecond = rate * region.calendar.SISecondsPerSecond + } + + internal var now: Instant { + let systemNow = Clocks.system.now + let elapsedTime = systemNow - systemStart + let scaledElapsedTime = elapsedTime * SISecondsPerClockSecond + return clockStart + scaledElapsedTime + } + } /// A clock that overrides another clock's region +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) internal struct CustomRegionClock: RegionalClock { - - private let base: any RegionalClock - internal let region: Region - - init(base: any RegionalClock, region: Region) { - self.base = base - self.region = region - } - - internal var now: Instant { - base.now - } - - var SISecondsPerClockSecond: Double { base.SISecondsPerClockSecond } + + private let base: any RegionalClock + internal let region: Region + + init(base: any RegionalClock, region: Region) { + self.base = base + self.region = region + } + + internal var now: Instant { + base.now + } + + var SISecondsPerClockSecond: Double { base.SISecondsPerClockSecond } } diff --git a/Sources/Time/3-RegionalClock/RegionalClock+Strikes.swift b/Sources/Time/3-RegionalClock/RegionalClock+Strikes.swift index 6932cc3..ba80378 100644 --- a/Sources/Time/3-RegionalClock/RegionalClock+Strikes.swift +++ b/Sources/Time/3-RegionalClock/RegionalClock+Strikes.swift @@ -1,71 +1,78 @@ import Foundation +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) extension RegionalClock { - - /// Sets up a repeating strike (ex: "every 5 minutes"), optionally starting at a time other than the present instant. - /// - /// - Note: The first "strike" will not occur until *after* `interval` has passed beyond `startTime` (or - /// the immediate present if `startTime` is `nil`). - /// - /// - Parameters: - /// - interval: The amount of time that should elapse before the next strike occurs. - /// - startTime: The time to start counting at before the first strike occurs. - /// - Returns: A ``ClockStrikes`` which emits fixed time values at the moment of each strike. - public func strike(producing unit: U.Type = U.self, - every interval: TimeDifference, - startingFrom startTime: Fixed? = nil) -> ClockStrikes { - return ClockStrikes(clock: self, interval: interval, startTime: startTime) - } - - /// Sets up a repeating strike, optionally starting at a time other than the present instant. - /// - /// ``` - /// // Strike every minute - /// clock.strike(every: Minute.self) - /// ``` - /// - /// - Parameters: - /// - every: The unit of time that should elapse before the next strike occurs. - /// - startTime: The time to start counting at before the first strike occurs. - /// - Returns: A ``ClockStrikes`` which emits fixed time values at the moment of each strike. - public func strike(every unit: U.Type, - startingFrom startTime: Fixed? = nil) -> ClockStrikes { - let interval = TimeDifference(value: 1, unit: U.component) - return ClockStrikes(clock: self, interval: interval, startTime: startTime) - } - - /// Sets up a repeating strike for each unit that matches the given closure. - /// - /// ``` - /// // Strike every hour at *:00, *:13, *:26, *:39, *:52 - /// clock.strike(when: { (time: Fixed) in - /// time.minute % 13 == 0 - /// }) - /// ``` - /// - /// - Parameters: - /// - matches: A closure which is called when each prospective unit elapses to determine - /// whether it should be published. - /// - time: A prospective time value. - /// - /// - Returns: A ``ClockStrikes`` which emits fixed time values at the moment of each strike. - public func strike(producing unit: U.Type = U.self, - when matches: @escaping (_ time: Fixed) -> Bool) -> ClockStrikes { - return ClockStrikes(clock: self, when: matches) - } - - /// Sets up a single strike (ex: "at 12:00 PM"). - /// - /// Useful, for example, when you want the `RegionalClock` to "tell me when it's 3:00 PM." - /// If the time has already passed, then the `ClockStrikes` completes immediately without sending a value. - /// - /// - Parameter time: The time at which the strike should occur. - /// - /// - Returns: A ``ClockStrikes`` which emits the current fixed time and then completes. - public func strike(at time: Fixed) -> ClockStrikes { - return ClockStrikes(clock: self, at: time) - } - + + /// Sets up a repeating strike (ex: "every 5 minutes"), optionally starting at a time other than the present instant. + /// + /// - Note: The first "strike" will not occur until *after* `interval` has passed beyond `startTime` (or + /// the immediate present if `startTime` is `nil`). + /// + /// - Parameters: + /// - interval: The amount of time that should elapse before the next strike occurs. + /// - startTime: The time to start counting at before the first strike occurs. + /// - Returns: A ``ClockStrikes`` which emits fixed time values at the moment of each strike. + public func strike( + producing unit: U.Type = U.self, + every interval: TimeDifference, + startingFrom startTime: Fixed? = nil + ) -> ClockStrikes { + return ClockStrikes(clock: self, interval: interval, startTime: startTime) + } + + /// Sets up a repeating strike, optionally starting at a time other than the present instant. + /// + /// ``` + /// // Strike every minute + /// clock.strike(every: Minute.self) + /// ``` + /// + /// - Parameters: + /// - every: The unit of time that should elapse before the next strike occurs. + /// - startTime: The time to start counting at before the first strike occurs. + /// - Returns: A ``ClockStrikes`` which emits fixed time values at the moment of each strike. + public func strike( + every unit: U.Type, + startingFrom startTime: Fixed? = nil + ) -> ClockStrikes { + let interval = TimeDifference(value: 1, unit: U.component) + return ClockStrikes(clock: self, interval: interval, startTime: startTime) + } + + /// Sets up a repeating strike for each unit that matches the given closure. + /// + /// ``` + /// // Strike every hour at *:00, *:13, *:26, *:39, *:52 + /// clock.strike(when: { (time: Fixed) in + /// time.minute % 13 == 0 + /// }) + /// ``` + /// + /// - Parameters: + /// - matches: A closure which is called when each prospective unit elapses to determine + /// whether it should be published. + /// - time: A prospective time value. + /// + /// - Returns: A ``ClockStrikes`` which emits fixed time values at the moment of each strike. + public func strike( + producing unit: U.Type = U.self, + when matches: @escaping (_ time: Fixed) -> Bool + ) -> ClockStrikes { + return ClockStrikes(clock: self, when: matches) + } + + /// Sets up a single strike (ex: "at 12:00 PM"). + /// + /// Useful, for example, when you want the `RegionalClock` to "tell me when it's 3:00 PM." + /// If the time has already passed, then the `ClockStrikes` completes immediately without sending a value. + /// + /// - Parameter time: The time at which the strike should occur. + /// + /// - Returns: A ``ClockStrikes`` which emits the current fixed time and then completes. + public func strike(at time: Fixed) -> ClockStrikes { + return ClockStrikes(clock: self, at: time) + } + } /// A type representing zero or more times at which a ``RegionalClock`` will "strike". @@ -91,223 +98,237 @@ extension RegionalClock { /// ``` /// /// - SeeAlso: [Striking Clocks (Wikipedia)](https://en.wikipedia.org/wiki/Striking_clock) +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) public struct ClockStrikes { - - fileprivate let clock: any RegionalClock - fileprivate let iterator: AnyIterator> - - private init(clock: any RegionalClock, iterator: I) where I.Element == Fixed { - self.clock = clock - self.iterator = AnyIterator(iterator) - } - - /// Create a `ClockStrikes` that will emit a value after a specified calendar interval, - /// when that value matches the provided predicate. - /// - /// - Parameters: - /// - clock: The `RegionalClock` to use for producing calendar values - /// - interval: The calendar interval to wait between each subsequent value - /// - startTime: The first time at which a value should be emitted. If this value is `nil`, the clock's current time is used. - /// - predicate: Only values matching this predicate will be emitted. By default, all emitted values match. - public init(clock: any RegionalClock, - interval: TimeDifference, - startTime: Fixed?, - predicate: @escaping (_ time: Fixed) -> Bool = { _ in true }) { - - let start = startTime ?? clock.current() - let i = FixedSequence(start: start, stride: interval).lazy.filter(predicate).makeIterator() - self.init(clock: clock, iterator: i) - } - - /// Create a `ClockStrikes` that emits values that match a provided predicate. - /// - Parameters: - /// - clock: The `RegionalClock` to use for producing calendar values - /// - matches: Only values matching this predicate will be emitted. - public init(clock: any RegionalClock, when matches: @escaping (Fixed) -> Bool) { - let interval = TimeDifference(value: 1, unit: U.component) - self.init(clock: clock, interval: interval, startTime: nil, predicate: matches) - } - - /// Create a `ClockStrikes` that emits at most one value at the specified time. - /// - Parameters: - /// - clock: The `RegionalClock` to use for producing calendar values. - /// - time: The time at which to emit the value. If this value is in the past, then the `ClockStrikes` emits no values. - public init(clock: any RegionalClock, at time: Fixed) { - let current = clock.current(U.self) - var values = Array>() - if time >= current { - values = [time] - } - self.init(clock: clock, iterator: values.makeIterator()) + + fileprivate let clock: any RegionalClock + fileprivate let iterator: AnyIterator> + + private init(clock: any RegionalClock, iterator: I) + where I.Element == Fixed { + self.clock = clock + self.iterator = AnyIterator(iterator) + } + + /// Create a `ClockStrikes` that will emit a value after a specified calendar interval, + /// when that value matches the provided predicate. + /// + /// - Parameters: + /// - clock: The `RegionalClock` to use for producing calendar values + /// - interval: The calendar interval to wait between each subsequent value + /// - startTime: The first time at which a value should be emitted. If this value is `nil`, the clock's current time is used. + /// - predicate: Only values matching this predicate will be emitted. By default, all emitted values match. + public init( + clock: any RegionalClock, + interval: TimeDifference, + startTime: Fixed?, + predicate: @escaping (_ time: Fixed) -> Bool = { _ in true } + ) { + + let start = startTime ?? clock.current() + let i = FixedSequence(start: start, stride: interval).lazy.filter(predicate).makeIterator() + self.init(clock: clock, iterator: i) + } + + /// Create a `ClockStrikes` that emits values that match a provided predicate. + /// - Parameters: + /// - clock: The `RegionalClock` to use for producing calendar values + /// - matches: Only values matching this predicate will be emitted. + public init(clock: any RegionalClock, when matches: @escaping (Fixed) -> Bool) { + let interval = TimeDifference(value: 1, unit: U.component) + self.init(clock: clock, interval: interval, startTime: nil, predicate: matches) + } + + /// Create a `ClockStrikes` that emits at most one value at the specified time. + /// - Parameters: + /// - clock: The `RegionalClock` to use for producing calendar values. + /// - time: The time at which to emit the value. If this value is in the past, then the `ClockStrikes` emits no values. + public init(clock: any RegionalClock, at time: Fixed) { + let current = clock.current(U.self) + var values = [Fixed]() + if time >= current { + values = [time] } - + self.init(clock: clock, iterator: values.makeIterator()) + } + } +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) extension ClockStrikes { - - /// The values at which the clock will strike, as a synchronous sequence - public struct Values: Sequence { - public typealias Element = Fixed - - let strikes: ClockStrikes - - public func makeIterator() -> AnyIterator> { - return strikes.iterator - } - - } - - /// Retrieve the values at which the clock will strike - public var values: Values { - Values(strikes: self) + + /// The values at which the clock will strike, as a synchronous sequence + public struct Values: Sequence { + public typealias Element = Fixed + + let strikes: ClockStrikes + + public func makeIterator() -> AnyIterator> { + return strikes.iterator } - + + } + + /// Retrieve the values at which the clock will strike + public var values: Values { + Values(strikes: self) + } + } +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) extension ClockStrikes { - /// The values at which the clock will strikes, as an asynchronous sequence - public struct AsyncValues: AsyncSequence { - - public typealias Element = Fixed - - internal let strikes: ClockStrikes - - public struct AsyncIterator: AsyncIteratorProtocol { - public typealias Element = Fixed - - fileprivate let clock: any RegionalClock - fileprivate var baseIterator: AnyIterator - - public mutating func next() async throws -> Fixed? { - var nextTime: Fixed? = baseIterator.next() - let now = clock.current(U.self) - while let next = nextTime, next < now { - nextTime = baseIterator.next() - } - - guard let next = nextTime else { return nil } - - try await clock.sleep(until: next.firstInstant, tolerance: nil) - return next - } - - } - - public func makeAsyncIterator() -> AsyncIterator { - return AsyncIterator(clock: self.strikes.clock, baseIterator: self.strikes.iterator) + /// The values at which the clock will strikes, as an asynchronous sequence + public struct AsyncValues: AsyncSequence { + + public typealias Element = Fixed + + internal let strikes: ClockStrikes + + public struct AsyncIterator: AsyncIteratorProtocol { + public typealias Element = Fixed + + fileprivate let clock: any RegionalClock + fileprivate var baseIterator: AnyIterator + + public mutating func next() async throws -> Fixed? { + var nextTime: Fixed? = baseIterator.next() + let now = clock.current(U.self) + while let next = nextTime, next < now { + nextTime = baseIterator.next() } - + + guard let next = nextTime else { return nil } + + try await clock.sleep(until: next.firstInstant, tolerance: nil) + return next + } + } - - /// Retrieve the asynchronous values at which the clock will strike - /// - /// This value can be used in an async loop, such as: - /// - /// ```swift - /// let strikes = someClock.strike(every: Minute.self) - /// - /// for try await currentTime in strikes.asyncValues { - /// print("The time is now \(currentTime)") - /// } - /// ``` - public var asyncValues: AsyncValues { - return AsyncValues(strikes: self) + + public func makeAsyncIterator() -> AsyncIterator { + return AsyncIterator(clock: self.strikes.clock, baseIterator: self.strikes.iterator) } - + + } + + /// Retrieve the asynchronous values at which the clock will strike + /// + /// This value can be used in an async loop, such as: + /// + /// ```swift + /// let strikes = someClock.strike(every: Minute.self) + /// + /// for try await currentTime in strikes.asyncValues { + /// print("The time is now \(currentTime)") + /// } + /// ``` + public var asyncValues: AsyncValues { + return AsyncValues(strikes: self) + } + } #if canImport(Combine) -import Combine -import Dispatch + import Combine + import Dispatch + + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) + extension ClockStrikes { -extension ClockStrikes { - /// The values at which the clock will strikes, as a Combine publisher /// /// - Warning: This publisher produces values on an undefined scheduler. If you need to receive - /// updates on a particular Scheduler, use the `.receive(on:)` operator. + /// updates on a particular Scheduler, use the `.receive(on:)` operator. public struct Publisher: Combine.Publisher { - - internal let strikes: ClockStrikes - - public typealias Output = Fixed - public typealias Failure = Never - - /// Set up a new Combine subscription for this `ClockStrikes` - /// - Parameter subscriber: The subscriber that receives strike events - public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { - let subscription = StrikesSubscription(subscriber: subscriber, - clock: strikes.clock, - iterator: strikes.iterator) - subscriber.receive(subscription: subscription) - } + + internal let strikes: ClockStrikes + + public typealias Output = Fixed + public typealias Failure = Never + + /// Set up a new Combine subscription for this `ClockStrikes` + /// - Parameter subscriber: The subscriber that receives strike events + public func receive(subscriber: S) + where S: Subscriber, Failure == S.Failure, Output == S.Input { + let subscription = StrikesSubscription( + subscriber: subscriber, + clock: strikes.clock, + iterator: strikes.iterator) + subscriber.receive(subscription: subscription) + } } - + /// Retrieve the publisher which emits the values at which the clock will strike /// /// - Warning: This publisher produces values on an undefined scheduler. If you need to receive /// updates on a particular Scheduler, use the `.receive(on:)` operator. public var publisher: Publisher { - return Publisher(strikes: self) + return Publisher(strikes: self) } - -} -private class StrikesSubscription: Subscription - where U: Unit, - SubscriberType: Subscriber, - SubscriberType.Failure == ClockStrikes.Publisher.Failure, - SubscriberType.Input == ClockStrikes.Publisher.Output { - + } + + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) + private class StrikesSubscription: Subscription + where + U: Unit, + SubscriberType: Subscriber, + SubscriberType.Failure == ClockStrikes.Publisher.Failure, + SubscriberType.Input == ClockStrikes.Publisher.Output + { + private var subscriber: SubscriberType? private let clock: any RegionalClock private var timeIterator: AnyIterator> private var nextStrike: CancellationToken? - + init(subscriber: SubscriberType, clock: any RegionalClock, iterator: AnyIterator>) { - self.subscriber = subscriber - self.clock = clock - self.timeIterator = iterator - - scheduleNextStrike() + self.subscriber = subscriber + self.clock = clock + self.timeIterator = iterator + + scheduleNextStrike() } - + private func scheduleNextStrike() { - var nextTime: Fixed? = timeIterator.next() - let now = clock.current(U.self) - while let next = nextTime, next < now { - // make sure we never strike anything in the past - nextTime = timeIterator.next() - } - - guard let nextStrikeTime = nextTime else { - subscriber?.receive(completion: .finished) - cancel() - return - } - - let strikeInstant = nextStrikeTime.firstInstant - self.nextStrike = clock.wait(until: strikeInstant, tolerance: nil, strike: { [weak self] in - self?.performStrike(at: nextStrikeTime) + var nextTime: Fixed? = timeIterator.next() + let now = clock.current(U.self) + while let next = nextTime, next < now { + // make sure we never strike anything in the past + nextTime = timeIterator.next() + } + + guard let nextStrikeTime = nextTime else { + subscriber?.receive(completion: .finished) + cancel() + return + } + + let strikeInstant = nextStrikeTime.firstInstant + self.nextStrike = clock.wait( + until: strikeInstant, tolerance: nil, + strike: { [weak self] in + self?.performStrike(at: nextStrikeTime) }) } - + private func performStrike(at time: Fixed) { - nextStrike = nil - _ = subscriber?.receive(time) - scheduleNextStrike() + nextStrike = nil + _ = subscriber?.receive(time) + scheduleNextStrike() } - + public func request(_ demand: Subscribers.Demand) { - // We ignore this, since time doesn't care when we're looking. + // We ignore this, since time doesn't care when we're looking. } - + public func cancel() { - nextStrike?.cancel() - nextStrike = nil - subscriber = nil + nextStrike?.cancel() + nextStrike = nil + subscriber = nil } -} + } #endif diff --git a/Sources/Time/3-RegionalClock/RegionalClock.swift b/Sources/Time/3-RegionalClock/RegionalClock.swift index dac2c42..ad59bf6 100644 --- a/Sources/Time/3-RegionalClock/RegionalClock.swift +++ b/Sources/Time/3-RegionalClock/RegionalClock.swift @@ -6,115 +6,118 @@ import Foundation /// All `RegionalClocks` use the same types for their instant and duration values (``Instant`` and ``SISeconds`` respectively). /// /// When implementing a custom `RegionalClock`, the two things that must be implemented are `.region` and `.now`. +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) public protocol RegionalClock: Clock where Instant == Time.Instant, Duration == Time.SISeconds { - - /// The clock's `Region`, used for creating calendrical values. - var region: Region { get } - - /// The number of `SISeconds` that pass for every clock second. - /// - /// This is used in situations where you wish to "speed up" or "slow down" clock time. A clock that moves - /// twice as fast as real time would return `2.0` for this value. A clock that moves half as fast as real time - /// would return `0.5` for this value. - /// - /// The default value is `1.0`, indicating that the clock advances 1 second for every elapsed `SISecond` - /// in real time. - var SISecondsPerClockSecond: Double { get } - + + /// The clock's `Region`, used for creating calendrical values. + var region: Region { get } + + /// The number of `SISeconds` that pass for every clock second. + /// + /// This is used in situations where you wish to "speed up" or "slow down" clock time. A clock that moves + /// twice as fast as real time would return `2.0` for this value. A clock that moves half as fast as real time + /// would return `0.5` for this value. + /// + /// The default value is `1.0`, indicating that the clock advances 1 second for every elapsed `SISecond` + /// in real time. + var SISecondsPerClockSecond: Double { get } + } +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) extension RegionalClock { - - /// The default implementation - public var SISecondsPerClockSecond: Double { return 1.0 } - - /// The default implementation; one nanosecond (1e-9) - public var minimumResolution: SISeconds { return SISeconds(1.0 / Double(1e9)) } - - /// Suspend the current concurrency task until the specified deadline, relative to this clock - /// - Parameter deadline: The `Instant` at which this task should wake up again, relative to this clock - /// - Parameter tolerance: How much leeway there is in missing the deadline - public func sleep(until deadline: Instant, tolerance: Instant.Duration?) async throws { - try await self.sleep(until: deadline, tolerance: tolerance, token: nil) - } - + + /// The default implementation + public var SISecondsPerClockSecond: Double { return 1.0 } + + /// The default implementation; one nanosecond (1e-9) + public var minimumResolution: SISeconds { return SISeconds(1.0 / Double(1e9)) } + + /// Suspend the current concurrency task until the specified deadline, relative to this clock + /// - Parameter deadline: The `Instant` at which this task should wake up again, relative to this clock + /// - Parameter tolerance: How much leeway there is in missing the deadline + public func sleep(until deadline: Instant, tolerance: Instant.Duration?) async throws { + try await self.sleep(until: deadline, tolerance: tolerance, token: nil) + } + } +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) extension RegionalClock { - - /// The `Calendar` used by the `RegionalClock`, as defined by its `region`. - public var calendar: Calendar { region.calendar } - - /// The `TimeZone` used by the `RegionalClock`, as defined by its `region`. - public var timeZone: TimeZone { region.timeZone } - - /// The `Locale` used by the `RegionalClock`, as defined by its `region`. - public var locale: Locale { region.locale } - - /// Offset a clock. - /// - /// - Parameter by: the `SISeconds` by which to create an offset clock. - /// - Returns: A new `RegionalClock` that is offset by the specified `SISeconds` from the receiver. - public func offset(by delta: SISeconds) -> any RegionalClock { - return OffsetClock(offset: delta, from: self) - } - - /// Scale a clock. - /// - /// - Parameter by: the `Double` by which to speed up or slow down time - /// - Returns: A new `RegionalClock` that is scaled from the receiver by the specified `Double` factor. - public func scaled(by factor: Double) -> any RegionalClock { - guard factor > 0 else { - fatalError("You cannot create a clock where time has stopped or flows backwards") - } - return ScaledClock(scale: factor, from: self) - } - - /// Retrieve the `Instant` of the next daylight saving time transition on this Clock. - /// - /// - Parameter after: The `Instant` after which to find the next daylight saving time transition. If omitted, it will be assumed to be the current instant. - /// - Returns: The `Instant` of the next daylight saving time transition, or `nil` if the time zone does not currently observe daylight saving time. - public func nextDaylightSavingTimeTransition(after instant: Instant? = nil) -> Instant? { - let afterInstant = instant ?? now - return timeZone.nextDaylightSavingTimeTransition(after: afterInstant.date).map(Instant.init) - } - - /// Convert a clock to a new time zone. - /// - /// - Parameter timeZone: The `TimeZone` of the new `RegionalClock`. - /// - Returns: A new `RegionalClock` that reports values in the specified `TimeZone`. - public func converted(to timeZone: TimeZone) -> any RegionalClock { - if timeZone.isEquivalent(to: self.timeZone) { return self } - let newRegion = self.region.setTimeZone(timeZone) - return self.converted(to: newRegion) - } - - /// Convert a clock to a new calendar. - /// - /// - Parameter calendar: The `Calendar` of the new `RegionalClock`. - /// - Returns: A new `RegionalClock` that reports values in the specified `Calendar`. - public func converted(to calendar: Calendar) -> any RegionalClock { - if calendar.isEquivalent(to: self.calendar) { return self } - let newRegion = self.region.setCalendar(calendar) - return self.converted(to: newRegion) - } - - /// Convert a clock to a new locale. - /// - /// - Parameter locale: The `Locale` of the new `RegionalClock`. - /// - Returns: A new `RegionalClock` that reports values in the specified `Locale`. - public func converted(to locale: Locale) -> any RegionalClock { - if locale.isEquivalent(to: self.locale) { return self } - let newRegion = self.region.setLocale(locale) - return self.converted(to: newRegion) - } - - /// Convert a clock to a new region. - /// - /// - Parameter region: The `Region` of the new `RegionalClock`. - /// - Returns: A new `RegionalClock` that reports values in the specified `Region`. - public func converted(to newRegion: Region) -> any RegionalClock { - if newRegion.isEquivalent(to: self.region) { return self } - return CustomRegionClock(base: self, region: newRegion) + + /// The `Calendar` used by the `RegionalClock`, as defined by its `region`. + public var calendar: Calendar { region.calendar } + + /// The `TimeZone` used by the `RegionalClock`, as defined by its `region`. + public var timeZone: TimeZone { region.timeZone } + + /// The `Locale` used by the `RegionalClock`, as defined by its `region`. + public var locale: Locale { region.locale } + + /// Offset a clock. + /// + /// - Parameter by: the `SISeconds` by which to create an offset clock. + /// - Returns: A new `RegionalClock` that is offset by the specified `SISeconds` from the receiver. + public func offset(by delta: SISeconds) -> any RegionalClock { + return OffsetClock(offset: delta, from: self) + } + + /// Scale a clock. + /// + /// - Parameter by: the `Double` by which to speed up or slow down time + /// - Returns: A new `RegionalClock` that is scaled from the receiver by the specified `Double` factor. + public func scaled(by factor: Double) -> any RegionalClock { + guard factor > 0 else { + fatalError("You cannot create a clock where time has stopped or flows backwards") } + return ScaledClock(scale: factor, from: self) + } + + /// Retrieve the `Instant` of the next daylight saving time transition on this Clock. + /// + /// - Parameter after: The `Instant` after which to find the next daylight saving time transition. If omitted, it will be assumed to be the current instant. + /// - Returns: The `Instant` of the next daylight saving time transition, or `nil` if the time zone does not currently observe daylight saving time. + public func nextDaylightSavingTimeTransition(after instant: Instant? = nil) -> Instant? { + let afterInstant = instant ?? now + return timeZone.nextDaylightSavingTimeTransition(after: afterInstant.date).map(Instant.init) + } + + /// Convert a clock to a new time zone. + /// + /// - Parameter timeZone: The `TimeZone` of the new `RegionalClock`. + /// - Returns: A new `RegionalClock` that reports values in the specified `TimeZone`. + public func converted(to timeZone: TimeZone) -> any RegionalClock { + if timeZone.isEquivalent(to: self.timeZone) { return self } + let newRegion = self.region.setTimeZone(timeZone) + return self.converted(to: newRegion) + } + + /// Convert a clock to a new calendar. + /// + /// - Parameter calendar: The `Calendar` of the new `RegionalClock`. + /// - Returns: A new `RegionalClock` that reports values in the specified `Calendar`. + public func converted(to calendar: Calendar) -> any RegionalClock { + if calendar.isEquivalent(to: self.calendar) { return self } + let newRegion = self.region.setCalendar(calendar) + return self.converted(to: newRegion) + } + + /// Convert a clock to a new locale. + /// + /// - Parameter locale: The `Locale` of the new `RegionalClock`. + /// - Returns: A new `RegionalClock` that reports values in the specified `Locale`. + public func converted(to locale: Locale) -> any RegionalClock { + if locale.isEquivalent(to: self.locale) { return self } + let newRegion = self.region.setLocale(locale) + return self.converted(to: newRegion) + } + + /// Convert a clock to a new region. + /// + /// - Parameter region: The `Region` of the new `RegionalClock`. + /// - Returns: A new `RegionalClock` that reports values in the specified `Region`. + public func converted(to newRegion: Region) -> any RegionalClock { + if newRegion.isEquivalent(to: self.region) { return self } + return CustomRegionClock(base: self, region: newRegion) + } } diff --git a/Sources/Time/4-Fixed Values/Fixed+Codable.swift b/Sources/Time/4-Fixed Values/Fixed+Codable.swift index 23323a4..a8a8efa 100644 --- a/Sources/Time/4-Fixed Values/Fixed+Codable.swift +++ b/Sources/Time/4-Fixed Values/Fixed+Codable.swift @@ -1,160 +1,161 @@ import Foundation extension Region: Codable { - private enum CodingKeys: String, CodingKey { - case timeZone - case locale - case calendar - } + private enum CodingKeys: String, CodingKey { + case timeZone + case locale + case calendar + } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let calendarContainer = try container.decode(CodableCalendar.self, forKey: .calendar) - let timeZoneContainer = try container.decode(CodableTimeZone.self, forKey: .timeZone) - let localeContainer = try container.decode(CodableLocale.self, forKey: .locale) - - self.init(calendar: calendarContainer.calendar, - timeZone: timeZoneContainer.timeZone, - locale: localeContainer.locale) - } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(CodableTimeZone(timeZone: timeZone), forKey: .timeZone) - try container.encode(CodableLocale(locale: locale), forKey: .locale) - try container.encode(CodableCalendar(calendar: calendar), forKey: .calendar) - } + let calendarContainer = try container.decode(CodableCalendar.self, forKey: .calendar) + let timeZoneContainer = try container.decode(CodableTimeZone.self, forKey: .timeZone) + let localeContainer = try container.decode(CodableLocale.self, forKey: .locale) + + self.init( + calendar: calendarContainer.calendar, + timeZone: timeZoneContainer.timeZone, + locale: localeContainer.locale) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(CodableTimeZone(timeZone: timeZone), forKey: .timeZone) + try container.encode(CodableLocale(locale: locale), forKey: .locale) + try container.encode(CodableCalendar(calendar: calendar), forKey: .calendar) + } } extension Fixed: Codable { - private enum CodingKeys: String, CodingKey { - case value - case region - - // old key - case components - } + private enum CodingKeys: String, CodingKey { + case value + case region - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let region = try container.decode(Region.self, forKey: .region) - - do { - let instant = try container.decode(Instant.self, forKey: .value) - self.init(region: region, instant: instant) - } catch { - // older format, does not have an instant; look for date components - let components = try container.decode(DateComponents.self, forKey: .components) - try self.init(region: region, strictDateComponents: components) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(region, forKey: .region) - try container.encode(instant, forKey: .value) + // old key + case components + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let region = try container.decode(Region.self, forKey: .region) + + do { + let instant = try container.decode(Instant.self, forKey: .value) + self.init(region: region, instant: instant) + } catch { + // older format, does not have an instant; look for date components + let components = try container.decode(DateComponents.self, forKey: .components) + try self.init(region: region, strictDateComponents: components) } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(region, forKey: .region) + try container.encode(instant, forKey: .value) + } } private struct CodableLocale: Codable { - - let locale: Locale - - init(locale: Locale) { - self.locale = locale - } - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - do { - let identifier = try container.decode(String.self) - self.locale = Locale.standard(identifier) - } catch { - self.locale = try container.decode(Locale.self) - } + + let locale: Locale + + init(locale: Locale) { + self.locale = locale + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + do { + let identifier = try container.decode(String.self) + self.locale = Locale.standard(identifier) + } catch { + self.locale = try container.decode(Locale.self) } - - func encode(to encoder: Encoder) throws { - let standard = Locale.standard(locale.identifier) - - if standard.isEquivalent(to: locale) { - var single = encoder.singleValueContainer() - try single.encode(locale.identifier) - } else { - try locale.encode(to: encoder) - } + } + + func encode(to encoder: Encoder) throws { + let standard = Locale.standard(locale.identifier) + + if standard.isEquivalent(to: locale) { + var single = encoder.singleValueContainer() + try single.encode(locale.identifier) + } else { + try locale.encode(to: encoder) } - + } + } private struct CodableTimeZone: Codable { - - let timeZone: TimeZone - - init(timeZone: TimeZone) { - self.timeZone = timeZone - } - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - do { - let identifier = try container.decode(String.self) - self.timeZone = TimeZone.standard(identifier) - } catch { - self.timeZone = try container.decode(TimeZone.self) - } + + let timeZone: TimeZone + + init(timeZone: TimeZone) { + self.timeZone = timeZone + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + do { + let identifier = try container.decode(String.self) + self.timeZone = TimeZone.standard(identifier) + } catch { + self.timeZone = try container.decode(TimeZone.self) } - - func encode(to encoder: Encoder) throws { - let standard = TimeZone.standard(timeZone.identifier) - - if standard.isEquivalent(to: timeZone) { - var single = encoder.singleValueContainer() - try single.encode(timeZone.identifier) - } else { - try timeZone.encode(to: encoder) - } + } + + func encode(to encoder: Encoder) throws { + let standard = TimeZone.standard(timeZone.identifier) + + if standard.isEquivalent(to: timeZone) { + var single = encoder.singleValueContainer() + try single.encode(timeZone.identifier) + } else { + try timeZone.encode(to: encoder) } - + } + } private struct CodableCalendar: Codable { - - let calendar: Calendar - - init(calendar: Calendar) { - self.calendar = calendar - } - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - do { - // on Linux, Calendar.Identifier does not appear to be Codable - // Also, encoding a Calendar.Identifier appears to include extra empty objects - // in the JSON; so instead, we'll decode the bare identifier manually - let encodingIdentifier = try container.decode(String.self) - let identifier = try Calendar.Identifier(encodingIdentifier: encodingIdentifier) - self.calendar = Calendar.standard(identifier) - } catch { - self.calendar = try container.decode(Calendar.self) - } + + let calendar: Calendar + + init(calendar: Calendar) { + self.calendar = calendar + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + do { + // on Linux, Calendar.Identifier does not appear to be Codable + // Also, encoding a Calendar.Identifier appears to include extra empty objects + // in the JSON; so instead, we'll decode the bare identifier manually + let encodingIdentifier = try container.decode(String.self) + let identifier = try Calendar.Identifier(encodingIdentifier: encodingIdentifier) + self.calendar = Calendar.standard(identifier) + } catch { + self.calendar = try container.decode(Calendar.self) } - - func encode(to encoder: Encoder) throws { - let standard = Calendar.standard(calendar.identifier) - - if standard.isEquivalent(to: calendar) { - var single = encoder.singleValueContainer() - // on Linux, Calendar.Identifier does not appear to be Codable - // Also, encoding a Calendar.Identifier appears to include extra empty objects - // in the JSON; so instead, we'll encode the bare identifier manually - let encodingIdentifier = try calendar.identifier.encodingIdentifier - try single.encode(encodingIdentifier) - } else { - try calendar.encode(to: encoder) - } + } + + func encode(to encoder: Encoder) throws { + let standard = Calendar.standard(calendar.identifier) + + if standard.isEquivalent(to: calendar) { + var single = encoder.singleValueContainer() + // on Linux, Calendar.Identifier does not appear to be Codable + // Also, encoding a Calendar.Identifier appears to include extra empty objects + // in the JSON; so instead, we'll encode the bare identifier manually + let encodingIdentifier = try calendar.identifier.encodingIdentifier + try single.encode(encodingIdentifier) + } else { + try calendar.encode(to: encoder) } - + } + } diff --git a/Sources/Time/4-Fixed Values/Fixed+Components.swift b/Sources/Time/4-Fixed Values/Fixed+Components.swift index a43e558..fdaa89d 100644 --- a/Sources/Time/4-Fixed Values/Fixed+Components.swift +++ b/Sources/Time/4-Fixed Values/Fixed+Components.swift @@ -1,70 +1,90 @@ import Foundation extension Fixed { - - /// Retrieve the `Range` of `Instants` described by this `Fixed` value. - /// - /// All fixed values contain many possible `Instants`. This property allows you - /// to retrieve that range to use in calculations, such as knowing things like: - /// - do these two calendar values overlap? - /// - is this calendar value contained within this other calendar value? - /// - etc - public var range: Range { - let range = calendar.range(containing: self.instant.date, in: self.representedComponents) - - return Instant(date: range.lowerBound) ..< Instant(date: range.upperBound) - } - - /// Retrieve the first `Instant` known to occur within this `Value`. - public var firstInstant: Instant { return range.lowerBound } - - @available(*, unavailable, message: "It's impossible to know the last instant of a calendar value, just like it's impossible to know the last number before 1.0") - public var lastInstant: Instant { fatalError() } - - /// Retrieve the numeric era of a fixed calendrical value. - /// - /// This value is typically very low (`0` or `1`), but some calendars use eras extensively and can return values much larger. - public var era: Int { dateComponents.era.unwrap("A Fixed<\(Granularity.self)> must have an era value") } + + /// Retrieve the `Range` of `Instants` described by this `Fixed` value. + /// + /// All fixed values contain many possible `Instants`. This property allows you + /// to retrieve that range to use in calculations, such as knowing things like: + /// - do these two calendar values overlap? + /// - is this calendar value contained within this other calendar value? + /// - etc + public var range: Range { + let range = calendar.range(containing: self.instant.date, in: self.representedComponents) + + return Instant(date: range.lowerBound).. must have an era value") + } } extension Fixed where Granularity: LTOEYear { - - /// Retrieve the numeric year of a fixed calendrical value. - public var year: Int { dateComponents.year.unwrap("A Fixed<\(Granularity.self)> must have a year value") } + + /// Retrieve the numeric year of a fixed calendrical value. + public var year: Int { + dateComponents.year.unwrap("A Fixed<\(Granularity.self)> must have a year value") + } } extension Fixed where Granularity: LTOEMonth { - - /// Retrieve the numeric month of a fixed calendrical value. - public var month: Int { dateComponents.month.unwrap("A Fixed<\(Granularity.self)> must have a month value") } + + /// Retrieve the numeric month of a fixed calendrical value. + public var month: Int { + dateComponents.month.unwrap("A Fixed<\(Granularity.self)> must have a month value") + } } extension Fixed where Granularity: LTOEDay { - - /// Retrieve the numeric day of a fixed calendrical value. - public var day: Int { dateComponents.day.unwrap("A Fixed<\(Granularity.self)> must have a day value") } + + /// Retrieve the numeric day of a fixed calendrical value. + public var day: Int { + dateComponents.day.unwrap("A Fixed<\(Granularity.self)> must have a day value") + } } extension Fixed where Granularity: LTOEHour { - - /// Retrieve the numeric hour of a fixed calendrical value. - public var hour: Int { dateComponents.hour.unwrap("A Fixed<\(Granularity.self)> must have an hour value") } + + /// Retrieve the numeric hour of a fixed calendrical value. + public var hour: Int { + dateComponents.hour.unwrap("A Fixed<\(Granularity.self)> must have an hour value") + } } extension Fixed where Granularity: LTOEMinute { - - /// Retrieve the numeric minute of a fixed calendrical value - public var minute: Int { dateComponents.minute.unwrap("A Fixed<\(Granularity.self)> must have a minute value") } + + /// Retrieve the numeric minute of a fixed calendrical value + public var minute: Int { + dateComponents.minute.unwrap("A Fixed<\(Granularity.self)> must have a minute value") + } } extension Fixed where Granularity: LTOESecond { - - /// Retrieve the numeric second of a fixed calendrical value. - public var second: Int { dateComponents.second.unwrap("A Fixed<\(Granularity.self)> must have a second value") } + + /// Retrieve the numeric second of a fixed calendrical value. + public var second: Int { + dateComponents.second.unwrap("A Fixed<\(Granularity.self)> must have a second value") + } } extension Fixed where Granularity: LTOENanosecond { - - /// Retrieve the numeric nanosecond of a fixed calendrical value. - public var nanosecond: Int { dateComponents.nanosecond.unwrap("A Fixed<\(Granularity.self)> must have a nanosecond value") } + + /// Retrieve the numeric nanosecond of a fixed calendrical value. + public var nanosecond: Int { + dateComponents.nanosecond.unwrap("A Fixed<\(Granularity.self)> must have a nanosecond value") + } } diff --git a/Sources/Time/4-Fixed Values/Fixed+Conversion.swift b/Sources/Time/4-Fixed Values/Fixed+Conversion.swift index f582187..0324d4c 100644 --- a/Sources/Time/4-Fixed Values/Fixed+Conversion.swift +++ b/Sources/Time/4-Fixed Values/Fixed+Conversion.swift @@ -6,150 +6,150 @@ import Foundation /// - SeeAlso: ``Fixed/converted(to:behavior:)-3fufq`` /// - SeeAlso: ``Fixed/converted(to:behavior:)-3meoh`` public enum ConversionBehavior { - - /// When converting a fixed value, the [components]() (day, hour, minute, etc) should be preserved. - /// - /// This is a failable operation and may result in a ``TimeError`` being thrown, as the value's - /// components may not exist in the new region. For example, a 2 AM value in a European region may - /// throw an error when converted to a US region, if that fixed value happens to occur during a daylight - /// saving time transition. - /// - /// Similarly, a fixed value representing a 13th month on a lunisolar calendar would fail - /// when converted to a gregorian region. - /// - /// - Warning: In general, this operation only makes sense to perform on fixed values that represent a day (or larger) range. - case preservingComponents - - /// When converting a fixed value, the ``Fixed/range`` should be preserved. - /// - /// This operation represents answering the question "if it's 2 PM in Los Angeles, what time is it in Rome?". - /// - /// - Warning: In general, this operation only makes sense to perform on fixed values that represent a day (or smaller) range. - /// Attempting to use this on larger units (months, years, and eras) will likely result in a thrown ``TimeError``, since most calendars - /// do not have the same month, year, or era boundaries as other calendars. - case preservingRange - + + /// When converting a fixed value, the [components]() (day, hour, minute, etc) should be preserved. + /// + /// This is a failable operation and may result in a ``TimeError`` being thrown, as the value's + /// components may not exist in the new region. For example, a 2 AM value in a European region may + /// throw an error when converted to a US region, if that fixed value happens to occur during a daylight + /// saving time transition. + /// + /// Similarly, a fixed value representing a 13th month on a lunisolar calendar would fail + /// when converted to a gregorian region. + /// + /// - Warning: In general, this operation only makes sense to perform on fixed values that represent a day (or larger) range. + case preservingComponents + + /// When converting a fixed value, the ``Fixed/range`` should be preserved. + /// + /// This operation represents answering the question "if it's 2 PM in Los Angeles, what time is it in Rome?". + /// + /// - Warning: In general, this operation only makes sense to perform on fixed values that represent a day (or smaller) range. + /// Attempting to use this on larger units (months, years, and eras) will likely result in a thrown ``TimeError``, since most calendars + /// do not have the same month, year, or era boundaries as other calendars. + case preservingRange + } extension Fixed { - - /// Convert a fixed value to a new region. - /// - /// - Parameters: - /// - region: The region to which the new fixed value should belong - /// - behavior: The ``ConversionBehavior`` specifying how the conversion should happen - /// - Returns: A new fixed value that has been converted to the specified time zone. - /// - Throws: A ``TimeError`` if the conversion could not be completed - /// - Warning: This operation may fail for many possible reasons and should be used with care. For full details, see ``ConversionBehavior``. - public func converted(to newRegion: Region, behavior: ConversionBehavior) throws -> Self { - if newRegion.isEquivalent(to: self.region) { return self } - - switch behavior { - case .preservingComponents: - return try Fixed(region: newRegion, strictDateComponents: self.dateComponents) - - case .preservingRange: - let currentRange = self.range - - let midPointValue = Fixed(region: newRegion, instant: self.approximateMidPoint) - if midPointValue.range == currentRange { return midPointValue } - - let startValue = Fixed(region: newRegion, instant: currentRange.lowerBound) - if startValue.range == currentRange { return startValue } - - if self.instant != currentRange.lowerBound { - let instantValue = Fixed(region: newRegion, instant: self.instant) - if instantValue.range == currentRange { return instantValue } - } - - throw TimeError.invalidDateComponents(self.dateComponents, in: newRegion) - } - } - - /// Convert a fixed value to a new time zone. - /// - /// - Parameters: - /// - timeZone: The time zone to which the new fixed value should belong - /// - behavior: The ``ConversionBehavior`` specifying how the conversion should happen - /// - Returns: A new fixed value that has been converted to the specified time zone. - /// - Throws: A ``TimeError`` if the conversion could not be completed - /// - Warning: This operation may fail for many possible reasons and should be used with care. For full details, see ``ConversionBehavior``. - public func converted(to timeZone: TimeZone, behavior: ConversionBehavior) throws -> Self { - let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale) - return try self.converted(to: newRegion, behavior: behavior) - } - - /// Convert a fixed value to a new calendar. - /// - /// - Parameters: - /// - calendar: The calendar to which the new fixed value should belong - /// - behavior: The ``ConversionBehavior`` specifying how the conversion should happen - /// - Returns: A new fixed value that has been converted to the specified calendar. - /// - Throws: A ``TimeError`` if the conversion could not be completed - /// - Warning: This operation may fail for many possible reasons and should be used with care. For full details, see ``ConversionBehavior``. - public func converted(to calendar: Calendar, behavior: ConversionBehavior) throws -> Self { - let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale) - return try self.converted(to: newRegion, behavior: behavior) - } - - /// Construct a new `Fixed` value by converting this fixed value to a new `Locale`. - /// - /// Changing a fixed value's locale affects how the value is formatted. It does not change the underlying components. - public func converted(to locale: Locale) -> Self { - let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale) - return Self(region: newRegion, instant: self.instant, components: self.dateComponents) + + /// Convert a fixed value to a new region. + /// + /// - Parameters: + /// - region: The region to which the new fixed value should belong + /// - behavior: The ``ConversionBehavior`` specifying how the conversion should happen + /// - Returns: A new fixed value that has been converted to the specified time zone. + /// - Throws: A ``TimeError`` if the conversion could not be completed + /// - Warning: This operation may fail for many possible reasons and should be used with care. For full details, see ``ConversionBehavior``. + public func converted(to newRegion: Region, behavior: ConversionBehavior) throws -> Self { + if newRegion.isEquivalent(to: self.region) { return self } + + switch behavior { + case .preservingComponents: + return try Fixed(region: newRegion, strictDateComponents: self.dateComponents) + + case .preservingRange: + let currentRange = self.range + + let midPointValue = Fixed(region: newRegion, instant: self.approximateMidPoint) + if midPointValue.range == currentRange { return midPointValue } + + let startValue = Fixed(region: newRegion, instant: currentRange.lowerBound) + if startValue.range == currentRange { return startValue } + + if self.instant != currentRange.lowerBound { + let instantValue = Fixed(region: newRegion, instant: self.instant) + if instantValue.range == currentRange { return instantValue } + } + + throw TimeError.invalidDateComponents(self.dateComponents, in: newRegion) } - + } + + /// Convert a fixed value to a new time zone. + /// + /// - Parameters: + /// - timeZone: The time zone to which the new fixed value should belong + /// - behavior: The ``ConversionBehavior`` specifying how the conversion should happen + /// - Returns: A new fixed value that has been converted to the specified time zone. + /// - Throws: A ``TimeError`` if the conversion could not be completed + /// - Warning: This operation may fail for many possible reasons and should be used with care. For full details, see ``ConversionBehavior``. + public func converted(to timeZone: TimeZone, behavior: ConversionBehavior) throws -> Self { + let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale) + return try self.converted(to: newRegion, behavior: behavior) + } + + /// Convert a fixed value to a new calendar. + /// + /// - Parameters: + /// - calendar: The calendar to which the new fixed value should belong + /// - behavior: The ``ConversionBehavior`` specifying how the conversion should happen + /// - Returns: A new fixed value that has been converted to the specified calendar. + /// - Throws: A ``TimeError`` if the conversion could not be completed + /// - Warning: This operation may fail for many possible reasons and should be used with care. For full details, see ``ConversionBehavior``. + public func converted(to calendar: Calendar, behavior: ConversionBehavior) throws -> Self { + let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale) + return try self.converted(to: newRegion, behavior: behavior) + } + + /// Construct a new `Fixed` value by converting this fixed value to a new `Locale`. + /// + /// Changing a fixed value's locale affects how the value is formatted. It does not change the underlying components. + public func converted(to locale: Locale) -> Self { + let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale) + return Self(region: newRegion, instant: self.instant, components: self.dateComponents) + } + } extension Fixed where Granularity: GTOEDay { - - /// Convert a fixed date to another time zone - /// - /// This works by transitioning the underlying *components* to a new time zone. If successful, the resulting value - /// will have the same `.year`, `.month`, etc as the original value. However, the resulting `.range` will be different. - /// - /// - Parameter timeZone: The new time zone of the resulting fixed value - /// - Returns: A new fixed value with the same underlying components - /// - Throws: Throws a ``TimeError`` if the underlying components do not exist in the specified `timeZone`. For example, - /// converting "30 December 2011" to the `Pacific/Apia` time zone throws an error, because that day did not exist in that time zone. - public func converted(to timeZone: TimeZone) throws -> Self { - let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale) - return try Self(region: newRegion, strictDateComponents: self.dateComponents) - } - + + /// Convert a fixed date to another time zone + /// + /// This works by transitioning the underlying *components* to a new time zone. If successful, the resulting value + /// will have the same `.year`, `.month`, etc as the original value. However, the resulting `.range` will be different. + /// + /// - Parameter timeZone: The new time zone of the resulting fixed value + /// - Returns: A new fixed value with the same underlying components + /// - Throws: Throws a ``TimeError`` if the underlying components do not exist in the specified `timeZone`. For example, + /// converting "30 December 2011" to the `Pacific/Apia` time zone throws an error, because that day did not exist in that time zone. + public func converted(to timeZone: TimeZone) throws -> Self { + let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale) + return try Self(region: newRegion, strictDateComponents: self.dateComponents) + } + } extension Fixed where Granularity: LTOEDay { - - /// Construct a new `Fixed` value by converting this fixed value to a new `Calendar`. - /// - /// - Note: This functionality is only available when dealing with fixed values that represent a day or smaller. All - /// supported calendars have the same basic definition of a day, being roughly `00:00:00 ... 23:59:59.999`. - /// Therefore, converting such a value to another calendar will result in the old temporal range being equivalent to - /// the new temporal range. This is not true for eras, years, and months: calendars have different definitions of when years start - /// and when months change, etc. Therefore, it is not possible to map "February 2024" to a non-gregorian calendar, since - /// there is not a guaranteed correspondance between their underlying `Range` values. - public func converted(to calendar: Calendar) -> Self { - let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale) - return Self(region: newRegion, instant: self.approximateMidPoint) - } - + + /// Construct a new `Fixed` value by converting this fixed value to a new `Calendar`. + /// + /// - Note: This functionality is only available when dealing with fixed values that represent a day or smaller. All + /// supported calendars have the same basic definition of a day, being roughly `00:00:00 ... 23:59:59.999`. + /// Therefore, converting such a value to another calendar will result in the old temporal range being equivalent to + /// the new temporal range. This is not true for eras, years, and months: calendars have different definitions of when years start + /// and when months change, etc. Therefore, it is not possible to map "February 2024" to a non-gregorian calendar, since + /// there is not a guaranteed correspondance between their underlying `Range` values. + public func converted(to calendar: Calendar) -> Self { + let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale) + return Self(region: newRegion, instant: self.approximateMidPoint) + } + } extension Fixed where Granularity: LTOEHour { - - /// Convert a fixed time to another time zone. - /// - /// This works by transitioning the underlying time range to the new time zone. Therefore, the resulting components - /// (`.hour`, `.minute`, etc) will be *different* from the original components. However, the resulting `.range` will - /// be the same. - /// - /// - Parameter timeZone: The new time zone of the resulting fixed value - /// - Returns: A fixed value representing the same range of time in a different `TimeZone`. - public func converted(to timeZone: TimeZone) -> Self { - let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale) - return Self(region: newRegion, instant: self.firstInstant) - } - + + /// Convert a fixed time to another time zone. + /// + /// This works by transitioning the underlying time range to the new time zone. Therefore, the resulting components + /// (`.hour`, `.minute`, etc) will be *different* from the original components. However, the resulting `.range` will + /// be the same. + /// + /// - Parameter timeZone: The new time zone of the resulting fixed value + /// - Returns: A fixed value representing the same range of time in a different `TimeZone`. + public func converted(to timeZone: TimeZone) -> Self { + let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale) + return Self(region: newRegion, instant: self.firstInstant) + } + } diff --git a/Sources/Time/4-Fixed Values/Fixed+Day.swift b/Sources/Time/4-Fixed Values/Fixed+Day.swift index b347463..0a5cc8a 100644 --- a/Sources/Time/4-Fixed Values/Fixed+Day.swift +++ b/Sources/Time/4-Fixed Values/Fixed+Day.swift @@ -1,111 +1,120 @@ import Foundation extension Fixed where Granularity: GTOEDay, Granularity: LTOEYear { - - /// Retrieve the first hour of this fixed value - public var firstHour: Fixed { return first() } - - /// Retrieve the last hour of this fixed value - public var lastHour: Fixed { return last() } - - /// Retrieve a specific 1-based hour from this fixed value. - /// - /// Example: - /// ``` - /// let firstHour = try thisFixedDay.nthHour(1) - /// let secondHour = try thisFixedDay.nthHour(2) - /// ``` - /// - /// - Parameter ordinal: The offset of the desired hour, as measured from the start of this value's range - /// - Returns: a fixed hour - /// - Throws: This method throws a ``TimeError`` if `ordinal` is outside the range of values allowed by the `.calendar`. - /// - /// - Note: The allowable values for `ordinal` depends on the fixed value's `.calendar` and granularity. - /// For example, getting the `.nthHour(50)` of a `Fixed` will throw an error, because no supported calendar has 50 hours in a day. - /// However, getting the `.nthHour(50)` of a `Fixed` is fine, because months typically have many more than 50 hours in it. - /// - /// - Warning: This method does not guarantee a correspondance between the `ordinal` and the returned value's `.hour`. Offsetting - /// and missing hours (or extra hours) may mean that the `.hour` value may be less than, equal to, or greater than the `ordinal` parameter. - public func nthHour(_ ordinal: Int) throws -> Fixed { return try nth(ordinal) } - - /// Get a sequence of all the hours in this fixed value. - /// - /// - If this is a `Fixed`, the sequence will produce approximately 24 `Fixed` values. - /// - If this is a `Fixed`, the sequence will produce between about 100 and 750 `Fixed` values, - /// depending on the length of the month. - /// - If this value is a `Fixed`, the sequence will produce all the hours in the year - public var hours: FixedSequence { - return FixedSequence(parent: self) - } - + + /// Retrieve the first hour of this fixed value + public var firstHour: Fixed { return first() } + + /// Retrieve the last hour of this fixed value + public var lastHour: Fixed { return last() } + + /// Retrieve a specific 1-based hour from this fixed value. + /// + /// Example: + /// ``` + /// let firstHour = try thisFixedDay.nthHour(1) + /// let secondHour = try thisFixedDay.nthHour(2) + /// ``` + /// + /// - Parameter ordinal: The offset of the desired hour, as measured from the start of this value's range + /// - Returns: a fixed hour + /// - Throws: This method throws a ``TimeError`` if `ordinal` is outside the range of values allowed by the `.calendar`. + /// + /// - Note: The allowable values for `ordinal` depends on the fixed value's `.calendar` and granularity. + /// For example, getting the `.nthHour(50)` of a `Fixed` will throw an error, because no supported calendar has 50 hours in a day. + /// However, getting the `.nthHour(50)` of a `Fixed` is fine, because months typically have many more than 50 hours in it. + /// + /// - Warning: This method does not guarantee a correspondance between the `ordinal` and the returned value's `.hour`. Offsetting + /// and missing hours (or extra hours) may mean that the `.hour` value may be less than, equal to, or greater than the `ordinal` parameter. + public func nthHour(_ ordinal: Int) throws -> Fixed { return try nth(ordinal) } + + /// Get a sequence of all the hours in this fixed value. + /// + /// - If this is a `Fixed`, the sequence will produce approximately 24 `Fixed` values. + /// - If this is a `Fixed`, the sequence will produce between about 100 and 750 `Fixed` values, + /// depending on the length of the month. + /// - If this value is a `Fixed`, the sequence will produce all the hours in the year + public var hours: FixedSequence { + return FixedSequence(parent: self) + } + } extension Fixed where Granularity == Day { - - /// Retrieve an hour on this day with a specific number - /// - Parameter number: The number of the hour (`0`, `13`, etc) - /// - Returns: A `Fixed` whose `.hour` is equal to the provided `number`, or `nil` if no such hour can be found - public func hour(_ number: Int) -> Fixed? { return numbered(number) } - + + /// Retrieve an hour on this day with a specific number + /// - Parameter number: The number of the hour (`0`, `13`, etc) + /// - Returns: A `Fixed` whose `.hour` is equal to the provided `number`, or `nil` if no such hour can be found + public func hour(_ number: Int) -> Fixed? { return numbered(number) } + } extension Fixed where Granularity: LTOEDay { - - /// Returns `true` if this fixed value is known to occur during the weekend. - /// - /// The definition of a "weekend" is supplied by the `Region`'s `Calendar`. - public var isWeekend: Bool { return calendar.isDateInWeekend(approximateMidPoint.date) } - - /// Returns `true` if this fixed value is known to *not* occur during the weekend. - /// - /// The definition of a "weekend" is supplied by the `Region`'s `Calendar`. - public var isWeekday: Bool { return !isWeekend } - - #if !os(Linux) + + /// Returns `true` if this fixed value is known to occur during the weekend. + /// + /// The definition of a "weekend" is supplied by the `Region`'s `Calendar`. + public var isWeekend: Bool { return calendar.isDateInWeekend(approximateMidPoint.date) } + + /// Returns `true` if this fixed value is known to *not* occur during the weekend. + /// + /// The definition of a "weekend" is supplied by the `Region`'s `Calendar`. + public var isWeekday: Bool { return !isWeekend } + + #if !os(Linux) /// Return this fixed value's day of the week /// /// - Warning: This property is not available on Linux + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) public var weekday: Locale.Weekday { return Locale.Weekday(dayOfWeek: self.dayOfWeek) } - #endif - - /// Returns the numerical representation of the this value's day of the week. - /// - /// For the Gregorian calendar, 1 = Sunday, 2 = Monday, ... 7 = Saturday - public var dayOfWeek: Int { return calendar.component(.weekday, from: approximateMidPoint.date) } - - /// Returns the day of the month on which this fixed value occurs. - /// - /// For example, given a value that represents "Halloween" (October 31st) on the Gregorian calendar, - /// this property returns "31". - public var dayOfMonth: Int { return day } - - /// Returns the day of the year on which this fixed value occurs. - /// - /// For example, given a value that represents the first of February on the Gregorian calendar, - /// this property returns "32". - public var dayOfYear: Int { return calendar.ordinality(of: .day, in: .year, for: approximateMidPoint.date)! } - - /// Returns the ordinal of this fixed value's weekday within its month. - /// - /// For example, if this fixed value falls on the second "Friday" of a month on the Gregorian calendar, - /// then `dayOfWeek` returns `6` ("Friday"), and this property returns `2` (the "second" Friday). - public var dayOfWeekOrdinal: Int { return calendar.component(.weekdayOrdinal, from: approximateMidPoint.date) } - - /// Returns the week of the month on which this fixed value occurs. - /// - /// - Note: Since most calendars do not use years that are evenly divisible by 7, there are days around month boundaries that are - /// attributed to a different "week of the month" than the month in which the day occurs. For example, if January 31st is a Friday, then - /// the following day (February 1st) wil likely belong to the same week of the month as the previous day (typically 4 or 5), even though they - /// occur in distinct calendar months. The behavior of this property is determined by the `.calendar.firstWeekday` and `.calendar.minimumDaysInFirstWeek` properties. - public var weekOfMonth: Int { return calendar.component(.weekOfMonth, from: approximateMidPoint.date) } - - /// Returns the week of the year on which this fixed value occurs. - /// - /// This property is most useful for identifying the occurrence of a week boundary between two `Fixed` values. - /// - /// - Note: Since most calendars do not use years that are evenly divisible by 7, there are days around year boundaries that are - /// attributed to a different "week of the year" than the year in which the day occurs. For example, if December 31st is a Friday, then - /// the following day (January 1st) wil likely belong to the same week of the year as the previous day (typically 52 or 53), even though they - /// occur in distinct calendar years. The behavior of this property is determined by the `.calendar.firstWeekday` and `.calendar.minimumDaysInFirstWeek` properties. - public var weekOfYear: Int { return calendar.component(.weekOfYear, from: approximateMidPoint.date) } + #endif + + /// Returns the numerical representation of the this value's day of the week. + /// + /// For the Gregorian calendar, 1 = Sunday, 2 = Monday, ... 7 = Saturday + public var dayOfWeek: Int { return calendar.component(.weekday, from: approximateMidPoint.date) } + + /// Returns the day of the month on which this fixed value occurs. + /// + /// For example, given a value that represents "Halloween" (October 31st) on the Gregorian calendar, + /// this property returns "31". + public var dayOfMonth: Int { return day } + + /// Returns the day of the year on which this fixed value occurs. + /// + /// For example, given a value that represents the first of February on the Gregorian calendar, + /// this property returns "32". + public var dayOfYear: Int { + return calendar.ordinality(of: .day, in: .year, for: approximateMidPoint.date)! + } + + /// Returns the ordinal of this fixed value's weekday within its month. + /// + /// For example, if this fixed value falls on the second "Friday" of a month on the Gregorian calendar, + /// then `dayOfWeek` returns `6` ("Friday"), and this property returns `2` (the "second" Friday). + public var dayOfWeekOrdinal: Int { + return calendar.component(.weekdayOrdinal, from: approximateMidPoint.date) + } + + /// Returns the week of the month on which this fixed value occurs. + /// + /// - Note: Since most calendars do not use years that are evenly divisible by 7, there are days around month boundaries that are + /// attributed to a different "week of the month" than the month in which the day occurs. For example, if January 31st is a Friday, then + /// the following day (February 1st) wil likely belong to the same week of the month as the previous day (typically 4 or 5), even though they + /// occur in distinct calendar months. The behavior of this property is determined by the `.calendar.firstWeekday` and `.calendar.minimumDaysInFirstWeek` properties. + public var weekOfMonth: Int { + return calendar.component(.weekOfMonth, from: approximateMidPoint.date) + } + + /// Returns the week of the year on which this fixed value occurs. + /// + /// This property is most useful for identifying the occurrence of a week boundary between two `Fixed` values. + /// + /// - Note: Since most calendars do not use years that are evenly divisible by 7, there are days around year boundaries that are + /// attributed to a different "week of the year" than the year in which the day occurs. For example, if December 31st is a Friday, then + /// the following day (January 1st) wil likely belong to the same week of the year as the previous day (typically 52 or 53), even though they + /// occur in distinct calendar years. The behavior of this property is determined by the `.calendar.firstWeekday` and `.calendar.minimumDaysInFirstWeek` properties. + public var weekOfYear: Int { + return calendar.component(.weekOfYear, from: approximateMidPoint.date) + } } diff --git a/Sources/Time/4-Fixed Values/Fixed+Equatable.swift b/Sources/Time/4-Fixed Values/Fixed+Equatable.swift index 78a4600..88e08d3 100644 --- a/Sources/Time/4-Fixed Values/Fixed+Equatable.swift +++ b/Sources/Time/4-Fixed Values/Fixed+Equatable.swift @@ -1,26 +1,26 @@ import Foundation extension Fixed: Equatable { - - /// Determine if two `Fixed` values are equal. - /// - /// Two `Fixed` values are equal if they have the same `Region` and represent the same calendrical components. - /// - Parameter lhs: a `Fixed` value - /// - Parameter rhs: a `Fixed` value - public static func ==(lhs: Self, rhs: Self) -> Bool { - return lhs.region == rhs.region && lhs.dateComponents == rhs.dateComponents - } - + + /// Determine if two `Fixed` values are equal. + /// + /// Two `Fixed` values are equal if they have the same `Region` and represent the same calendrical components. + /// - Parameter lhs: a `Fixed` value + /// - Parameter rhs: a `Fixed` value + public static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.region == rhs.region && lhs.dateComponents == rhs.dateComponents + } + } extension Fixed: Hashable { - - /// Compute the hash of an`Fixed` value - /// - /// - Parameter hasher: a `Hasher` - public func hash(into hasher: inout Hasher) { - hasher.combine(region) - hasher.combine(dateComponents) - } - + + /// Compute the hash of an`Fixed` value + /// + /// - Parameter hasher: a `Hasher` + public func hash(into hasher: inout Hasher) { + hasher.combine(region) + hasher.combine(dateComponents) + } + } diff --git a/Sources/Time/4-Fixed Values/Fixed+Era.swift b/Sources/Time/4-Fixed Values/Fixed+Era.swift index cd4d931..bccf762 100644 --- a/Sources/Time/4-Fixed Values/Fixed+Era.swift +++ b/Sources/Time/4-Fixed Values/Fixed+Era.swift @@ -1,41 +1,40 @@ import Foundation extension Fixed where Granularity == Era { - - /// Retrieve the first year of this era - public var firstYear: Fixed { return first() } - - /// Retrieve the last year of this era - /// - /// - Warning: Many calendars do not support accurate computations around eras. Depending on the - /// `.calendar`, this value will likely be wrong. - public var lastYear: Fixed { return last() } - - /// Retrieve a specific 1-based year from this era. - /// - /// Example: - /// ``` - /// let firstYear = try thisFixedEra.nthYear(1) - /// let secondYear = try thisFixedEra.nthYear(2) - /// ``` - /// - /// - Parameter ordinal: The offset of the desired year, as measured from the start of this era - /// - Returns: a fixed year - /// - Throws: This method throws a `TimeError` if `ordinal` is outside the range of values allowed by the `.calendar`. - public func nthYear(_ ordinal: Int) throws -> Fixed { return try nth(ordinal) } - - /// Retrieve a year in this era with a specific number - /// - Parameter number: The number of the year (`1492`, `2024`, etc) - /// - Returns: A `Fixed` whose `.year` is equal to the provided `number`, or `nil` if no such year can be found - public func year(_ number: Int) -> Fixed? { return numbered(number) } - - - /// Get a sequence of all the years in this era. - /// - /// - Warning: Many calendars do not support accurate computations around eras. Depending on the - /// `.calendar`, this sequence may produce unexpected results. - public var years: FixedSequence { - return FixedSequence(parent: self) - } - + + /// Retrieve the first year of this era + public var firstYear: Fixed { return first() } + + /// Retrieve the last year of this era + /// + /// - Warning: Many calendars do not support accurate computations around eras. Depending on the + /// `.calendar`, this value will likely be wrong. + public var lastYear: Fixed { return last() } + + /// Retrieve a specific 1-based year from this era. + /// + /// Example: + /// ``` + /// let firstYear = try thisFixedEra.nthYear(1) + /// let secondYear = try thisFixedEra.nthYear(2) + /// ``` + /// + /// - Parameter ordinal: The offset of the desired year, as measured from the start of this era + /// - Returns: a fixed year + /// - Throws: This method throws a `TimeError` if `ordinal` is outside the range of values allowed by the `.calendar`. + public func nthYear(_ ordinal: Int) throws -> Fixed { return try nth(ordinal) } + + /// Retrieve a year in this era with a specific number + /// - Parameter number: The number of the year (`1492`, `2024`, etc) + /// - Returns: A `Fixed` whose `.year` is equal to the provided `number`, or `nil` if no such year can be found + public func year(_ number: Int) -> Fixed? { return numbered(number) } + + /// Get a sequence of all the years in this era. + /// + /// - Warning: Many calendars do not support accurate computations around eras. Depending on the + /// `.calendar`, this sequence may produce unexpected results. + public var years: FixedSequence { + return FixedSequence(parent: self) + } + } diff --git a/Sources/Time/4-Fixed Values/Fixed+Hour.swift b/Sources/Time/4-Fixed Values/Fixed+Hour.swift index 6ac93d7..f7e5a43 100644 --- a/Sources/Time/4-Fixed Values/Fixed+Hour.swift +++ b/Sources/Time/4-Fixed Values/Fixed+Hour.swift @@ -1,47 +1,47 @@ import Foundation extension Fixed where Granularity: GTOEHour, Granularity: LTOEYear { - - /// Retrieve the first minute of this fixed value - public var firstMinute: Fixed { return first() } - - /// Retrieve the last minute of this fixed value - public var lastMinute: Fixed { return last() } - - /// Retrieve a specific 1-based minute from this fixed value - /// - /// Example: - /// ``` - /// let firstMinute = try thisFixedYear.nthMinute(1) - /// let secondMinute = try thisFixedYear.nthMinute(2) - /// ``` - /// - /// - Parameter ordinal: The offset of the desired minute, as measured from the start of this value's range - /// - Returns: a fixed minute - /// - Throws: This method throws a ``TimeError`` if `ordinal` is outside the range of values allowed by the `.calendar`. - /// - /// - Note: The allowable values for `ordinal` depend on the fixed value's `.calendar` and granularity. - /// For example, getting the `.nthMinute(72)` of a `Fixed` will throw an error, because no supported calendar has more - /// 60 minutes in an hour. However, getting the `.nthMinute(72)` of a `Fixed` is fine, because days typically have more - /// than 1,440 minutes in them. - public func nthMinute(_ ordinal: Int) throws -> Fixed { return try nth(ordinal) } - - /// Get a sequence of all the minutes in this fixed value. - /// - /// - If this is a `Fixed`, the sequence produces all the minutes in that hour - /// - If this is a `Fixed`, the sequence produces all the minutes in that day - /// - If this is a `Fixed`, the sequence produces all the minutes in that month - /// - If this is a `Fixed`, the sequence produces all the minutes in that year - public var minutes: FixedSequence { - return FixedSequence(parent: self) - } + + /// Retrieve the first minute of this fixed value + public var firstMinute: Fixed { return first() } + + /// Retrieve the last minute of this fixed value + public var lastMinute: Fixed { return last() } + + /// Retrieve a specific 1-based minute from this fixed value + /// + /// Example: + /// ``` + /// let firstMinute = try thisFixedYear.nthMinute(1) + /// let secondMinute = try thisFixedYear.nthMinute(2) + /// ``` + /// + /// - Parameter ordinal: The offset of the desired minute, as measured from the start of this value's range + /// - Returns: a fixed minute + /// - Throws: This method throws a ``TimeError`` if `ordinal` is outside the range of values allowed by the `.calendar`. + /// + /// - Note: The allowable values for `ordinal` depend on the fixed value's `.calendar` and granularity. + /// For example, getting the `.nthMinute(72)` of a `Fixed` will throw an error, because no supported calendar has more + /// 60 minutes in an hour. However, getting the `.nthMinute(72)` of a `Fixed` is fine, because days typically have more + /// than 1,440 minutes in them. + public func nthMinute(_ ordinal: Int) throws -> Fixed { return try nth(ordinal) } + + /// Get a sequence of all the minutes in this fixed value. + /// + /// - If this is a `Fixed`, the sequence produces all the minutes in that hour + /// - If this is a `Fixed`, the sequence produces all the minutes in that day + /// - If this is a `Fixed`, the sequence produces all the minutes in that month + /// - If this is a `Fixed`, the sequence produces all the minutes in that year + public var minutes: FixedSequence { + return FixedSequence(parent: self) + } } extension Fixed where Granularity == Hour { - - /// Retrieve a minute in this hour with a specific number - /// - Parameter number: The number of the minute (`0`, `42`, etc) - /// - Returns: A `Fixed` whose `.minute` is equal to the provided `number`, or `nil` if no such minute can be found - public func minute(_ number: Int) -> Fixed? { return numbered(number) } - + + /// Retrieve a minute in this hour with a specific number + /// - Parameter number: The number of the minute (`0`, `42`, etc) + /// - Returns: A `Fixed` whose `.minute` is equal to the provided `number`, or `nil` if no such minute can be found + public func minute(_ number: Int) -> Fixed? { return numbered(number) } + } diff --git a/Sources/Time/4-Fixed Values/Fixed+Initializers.swift b/Sources/Time/4-Fixed Values/Fixed+Initializers.swift index 9cac342..065036b 100644 --- a/Sources/Time/4-Fixed Values/Fixed+Initializers.swift +++ b/Sources/Time/4-Fixed Values/Fixed+Initializers.swift @@ -1,171 +1,184 @@ import Foundation extension Fixed where Granularity == Era { - - /// Construct a `Fixed` from the specified numeric components. - /// - Parameters: - /// - region: The `Region` in which the components will be interpreted. - /// - era: The numeric `Era` value for the value. - /// - Throws: A ``TimeError`` if the specified components cannot be converted into a calendar value. - public init(region: Region, era: Int) throws { - let components = DateComponents(era: era) - try self.init(region: region, strictDateComponents: components) - } - + + /// Construct a `Fixed` from the specified numeric components. + /// - Parameters: + /// - region: The `Region` in which the components will be interpreted. + /// - era: The numeric `Era` value for the value. + /// - Throws: A ``TimeError`` if the specified components cannot be converted into a calendar value. + public init(region: Region, era: Int) throws { + let components = DateComponents(era: era) + try self.init(region: region, strictDateComponents: components) + } + } extension Fixed where Granularity == Year { - - /// Construct a `Fixed` from the specified numeric components. - /// - Parameters: - /// - region: The `Region` in which the components will be interpreted. - /// - era: The numeric `Era` value for the value. If omitted, it will assumed to be the "current" era. - /// - year: The numeric `Year` value. - /// - Throws: A ``TimeError`` if the specified components cannot be converted into a calendar value. - public init(region: Region, era: Int? = nil, year: Int) throws { - let components = DateComponents(era: era, year: year) - try self.init(region: region, strictDateComponents: components) - } - + + /// Construct a `Fixed` from the specified numeric components. + /// - Parameters: + /// - region: The `Region` in which the components will be interpreted. + /// - era: The numeric `Era` value for the value. If omitted, it will assumed to be the "current" era. + /// - year: The numeric `Year` value. + /// - Throws: A ``TimeError`` if the specified components cannot be converted into a calendar value. + public init(region: Region, era: Int? = nil, year: Int) throws { + let components = DateComponents(era: era, year: year) + try self.init(region: region, strictDateComponents: components) + } + } extension Fixed where Granularity == Month { - - /// Construct a `Fixed` from the specified numeric components. - /// - Parameters: - /// - region: The `Region` in which the components will be interpreted. - /// - era: The numeric `Era` value for the value. If omitted, it will assumed to be the "current" era. - /// - year: The numeric `Year` value. - /// - month: The numeric `Month` value. - /// - Throws: A ``TimeError`` error if the specified components cannot be converted into a calendar value. - public init(region: Region, era: Int? = nil, year: Int, month: Int) throws { - let components = DateComponents(era: era, year: year, month: month) - try self.init(region: region, strictDateComponents: components) - } - + + /// Construct a `Fixed` from the specified numeric components. + /// - Parameters: + /// - region: The `Region` in which the components will be interpreted. + /// - era: The numeric `Era` value for the value. If omitted, it will assumed to be the "current" era. + /// - year: The numeric `Year` value. + /// - month: The numeric `Month` value. + /// - Throws: A ``TimeError`` error if the specified components cannot be converted into a calendar value. + public init(region: Region, era: Int? = nil, year: Int, month: Int) throws { + let components = DateComponents(era: era, year: year, month: month) + try self.init(region: region, strictDateComponents: components) + } + } extension Fixed where Granularity == Day { - - /// Construct a `Fixed` from the specified numeric components. - /// - Parameters: - /// - region: The `Region` in which the components will be interpreted. - /// - era: The numeric `Era` value for the value. If omitted, it will assumed to be the "current" era. - /// - year: The numeric `Year` value. - /// - month: The numeric `Month` value. - /// - day: the numeric `Day` value. - /// - Throws: A ``TimeError`` error if the specified components cannot be converted into a calendar value. - public init(region: Region, era: Int? = nil, year: Int, month: Int, day: Int) throws { - let components = DateComponents(era: era, year: year, month: month, day: day) - try self.init(region: region, strictDateComponents: components) - } - + + /// Construct a `Fixed` from the specified numeric components. + /// - Parameters: + /// - region: The `Region` in which the components will be interpreted. + /// - era: The numeric `Era` value for the value. If omitted, it will assumed to be the "current" era. + /// - year: The numeric `Year` value. + /// - month: The numeric `Month` value. + /// - day: the numeric `Day` value. + /// - Throws: A ``TimeError`` error if the specified components cannot be converted into a calendar value. + public init(region: Region, era: Int? = nil, year: Int, month: Int, day: Int) throws { + let components = DateComponents(era: era, year: year, month: month, day: day) + try self.init(region: region, strictDateComponents: components) + } + } extension Fixed where Granularity == Hour { - - /// Construct a `Fixed` from the specified numeric components. - /// - Parameters: - /// - region: The `Region` in which the components will be interpreted. - /// - era: The numeric `Era` value for the value. If omitted, it will assumed to be the "current" era. - /// - year: The numeric `Year` value. - /// - month: The numeric `Month` value. - /// - day: the numeric `Day` value. - /// - hour: the numeric `Hour` value. - /// - Throws: A ``TimeError`` error if the specified components cannot be converted into a calendar value. - public init(region: Region, era: Int? = nil, year: Int, month: Int, day: Int, hour: Int) throws { - let components = DateComponents(era: era, year: year, month: month, day: day, hour: hour) - try self.init(region: region, strictDateComponents: components) - } - + + /// Construct a `Fixed` from the specified numeric components. + /// - Parameters: + /// - region: The `Region` in which the components will be interpreted. + /// - era: The numeric `Era` value for the value. If omitted, it will assumed to be the "current" era. + /// - year: The numeric `Year` value. + /// - month: The numeric `Month` value. + /// - day: the numeric `Day` value. + /// - hour: the numeric `Hour` value. + /// - Throws: A ``TimeError`` error if the specified components cannot be converted into a calendar value. + public init(region: Region, era: Int? = nil, year: Int, month: Int, day: Int, hour: Int) throws { + let components = DateComponents(era: era, year: year, month: month, day: day, hour: hour) + try self.init(region: region, strictDateComponents: components) + } + } extension Fixed where Granularity == Minute { - - /// Construct a `Fixed` from the specified numeric components. - /// - Parameters: - /// - region: The `Region` in which the components will be interpreted. - /// - era: The numeric `Era` value for the value. If omitted, it will assumed to be the "current" era. - /// - year: The numeric `Year` value. - /// - month: The numeric `Month` value. - /// - day: the numeric `Day` value. - /// - hour: the numeric `Hour` value. - /// - minute: the numeric `Minute` value. - /// - Throws: A ``TimeError`` error if the specified components cannot be converted into a calendar value. - public init(region: Region, era: Int? = nil, year: Int, month: Int, day: Int, hour: Int, minute: Int) throws { - let components = DateComponents(era: era, year: year, month: month, day: day, hour: hour, minute: minute) - try self.init(region: region, strictDateComponents: components) - } - + + /// Construct a `Fixed` from the specified numeric components. + /// - Parameters: + /// - region: The `Region` in which the components will be interpreted. + /// - era: The numeric `Era` value for the value. If omitted, it will assumed to be the "current" era. + /// - year: The numeric `Year` value. + /// - month: The numeric `Month` value. + /// - day: the numeric `Day` value. + /// - hour: the numeric `Hour` value. + /// - minute: the numeric `Minute` value. + /// - Throws: A ``TimeError`` error if the specified components cannot be converted into a calendar value. + public init( + region: Region, era: Int? = nil, year: Int, month: Int, day: Int, hour: Int, minute: Int + ) throws { + let components = DateComponents( + era: era, year: year, month: month, day: day, hour: hour, minute: minute) + try self.init(region: region, strictDateComponents: components) + } + } extension Fixed where Granularity == Second { - - /// Construct a `Fixed` from the specified numeric components. - /// - Parameters: - /// - region: The `Region` in which the components will be interpreted. - /// - era: The numeric `Era` value for the value. If omitted, it will assumed to be the "current" era. - /// - year: The numeric `Year` value. - /// - month: The numeric `Month` value. - /// - day: the numeric `Day` value. - /// - hour: the numeric `Hour` value. - /// - minute: the numeric `Minute` value. - /// - second: the numeric `Second` value. - /// - Throws: A ``TimeError`` error if the specified components cannot be converted into a calendar value. - public init(region: Region, era: Int? = nil, year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int) throws { - let components = DateComponents(era: era, year: year, month: month, day: day, hour: hour, minute: minute, second: second) - try self.init(region: region, strictDateComponents: components) - } - + + /// Construct a `Fixed` from the specified numeric components. + /// - Parameters: + /// - region: The `Region` in which the components will be interpreted. + /// - era: The numeric `Era` value for the value. If omitted, it will assumed to be the "current" era. + /// - year: The numeric `Year` value. + /// - month: The numeric `Month` value. + /// - day: the numeric `Day` value. + /// - hour: the numeric `Hour` value. + /// - minute: the numeric `Minute` value. + /// - second: the numeric `Second` value. + /// - Throws: A ``TimeError`` error if the specified components cannot be converted into a calendar value. + public init( + region: Region, era: Int? = nil, year: Int, month: Int, day: Int, hour: Int, minute: Int, + second: Int + ) throws { + let components = DateComponents( + era: era, year: year, month: month, day: day, hour: hour, minute: minute, second: second) + try self.init(region: region, strictDateComponents: components) + } + } extension Fixed where Granularity == Nanosecond { - - /// Construct a `Fixed` from the specified numeric components. - /// - Parameters: - /// - region: The `Region` in which the components will be interpreted. - /// - era: The numeric `Era` value for the value. If omitted, it will assumed to be the "current" era. - /// - year: The numeric `Year` value. - /// - month: The numeric `Month` value. - /// - day: the numeric `Day` value. - /// - hour: the numeric `Hour` value. - /// - minute: the numeric `Minute` value. - /// - second: the numeric `Second` value. - /// - nanosecond: the numeric `Nanosecond` value. - /// - Throws: A ``TimeError`` error if the specified components cannot be converted into a calendar value. - public init(region: Region, era: Int? = nil, year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) throws { - let components = DateComponents(era: era, year: year, month: month, day: day, hour: hour, minute: minute, second: second, nanosecond: nanosecond) - try self.init(region: region, strictDateComponents: components) - } - + + /// Construct a `Fixed` from the specified numeric components. + /// - Parameters: + /// - region: The `Region` in which the components will be interpreted. + /// - era: The numeric `Era` value for the value. If omitted, it will assumed to be the "current" era. + /// - year: The numeric `Year` value. + /// - month: The numeric `Month` value. + /// - day: the numeric `Day` value. + /// - hour: the numeric `Hour` value. + /// - minute: the numeric `Minute` value. + /// - second: the numeric `Second` value. + /// - nanosecond: the numeric `Nanosecond` value. + /// - Throws: A ``TimeError`` error if the specified components cannot be converted into a calendar value. + public init( + region: Region, era: Int? = nil, year: Int, month: Int, day: Int, hour: Int, minute: Int, + second: Int, nanosecond: Int + ) throws { + let components = DateComponents( + era: era, year: year, month: month, day: day, hour: hour, minute: minute, second: second, + nanosecond: nanosecond) + try self.init(region: region, strictDateComponents: components) + } + } extension Fixed { - - /// Create a "deep" copy of the fixed value. This is a reasonably expensive operation, and should be used with care. - /// - /// This method is useful if you're on a platform that doesn't provide thread safety for the underlying date - /// primatives, most notably Linux at the time of writing (mid-2023). If you're using `Fixed` value objects in a - /// multithreaded environment and are seeing odd behaviour, you may need to work with copies. - /// - /// Notable observed "odd behaviours" include: - /// - /// - Attempting to create what should be a valid `Fixed` value range (like `someDay.. Self { - return Self(region: region._forcedCopy(), - instant: Instant(interval: instant.intervalSinceEpoch, since: instant.epoch)) - } - + + /// Create a "deep" copy of the fixed value. This is a reasonably expensive operation, and should be used with care. + /// + /// This method is useful if you're on a platform that doesn't provide thread safety for the underlying date + /// primatives, most notably Linux at the time of writing (mid-2023). If you're using `Fixed` value objects in a + /// multithreaded environment and are seeing odd behaviour, you may need to work with copies. + /// + /// Notable observed "odd behaviours" include: + /// + /// - Attempting to create what should be a valid `Fixed` value range (like `someDay.. Self { + return Self( + region: region._forcedCopy(), + instant: Instant(interval: instant.intervalSinceEpoch, since: instant.epoch)) + } + } diff --git a/Sources/Time/4-Fixed Values/Fixed+Minute.swift b/Sources/Time/4-Fixed Values/Fixed+Minute.swift index d9d1c9e..0d1a3fa 100644 --- a/Sources/Time/4-Fixed Values/Fixed+Minute.swift +++ b/Sources/Time/4-Fixed Values/Fixed+Minute.swift @@ -1,50 +1,50 @@ import Foundation extension Fixed where Granularity: GTOEMinute, Granularity: LTOEYear { - - /// Retrieve the first second of this fixed value - public var firstSecond: Fixed { return first() } - - /// Retrieve the last second of this fixed value - public var lastSecond: Fixed { return last() } - - /// Retrieve a specific 1-based second from this fixed value. - /// - /// Example: - /// ``` - /// let firstSecond = try thisFixedDay.nthSecond(1) - /// let secondSecond = try thisFixedDay.nthSecond(2) - /// ``` - /// - /// - Parameter ordinal: The offset of the desired second, as measured from the start of this value's range - /// - Returns: a fixed second - /// - Throws: This method throws a ``TimeError`` if `ordinal` is outside the range of values allowed by the `.calendar`. - /// - /// - Note: The allowable values for `ordinal` depends on the fixed value's `.calendar` and granularity. - /// For example, getting the `.nthSecond(100)` of a `Fixed` will throw an error, because no supported calendar has more than 60 seconds in a minute. - /// However, getting the `.nthSecond(100)` of a `Fixed` is fine, because a day typically has about 86,400 seconds in it. - /// - /// - Warning: This method does not guarantee a correspondance between the `ordinal` and the returned value's `.second`. Offsetting - /// and missing seconds (or extra seconds) may mean that the `.second` value may be less than, equal to, or greater than the `ordinal` parameter. - public func nthSecond(_ ordinal: Int) throws -> Fixed { return try nth(ordinal) } - - /// Get a sequence of all the seconds in this fixed value. - /// - /// - If this is a `Fixed`, the sequence produces all the seconds in the minute - /// - If this is a `Fixed`, the sequence produces all the seconds in the hour - /// - If this is a `Fixed`, the sequence produces all the seconds in the day - /// - etc. - public var seconds: FixedSequence { - return FixedSequence(parent: self) - } - + + /// Retrieve the first second of this fixed value + public var firstSecond: Fixed { return first() } + + /// Retrieve the last second of this fixed value + public var lastSecond: Fixed { return last() } + + /// Retrieve a specific 1-based second from this fixed value. + /// + /// Example: + /// ``` + /// let firstSecond = try thisFixedDay.nthSecond(1) + /// let secondSecond = try thisFixedDay.nthSecond(2) + /// ``` + /// + /// - Parameter ordinal: The offset of the desired second, as measured from the start of this value's range + /// - Returns: a fixed second + /// - Throws: This method throws a ``TimeError`` if `ordinal` is outside the range of values allowed by the `.calendar`. + /// + /// - Note: The allowable values for `ordinal` depends on the fixed value's `.calendar` and granularity. + /// For example, getting the `.nthSecond(100)` of a `Fixed` will throw an error, because no supported calendar has more than 60 seconds in a minute. + /// However, getting the `.nthSecond(100)` of a `Fixed` is fine, because a day typically has about 86,400 seconds in it. + /// + /// - Warning: This method does not guarantee a correspondance between the `ordinal` and the returned value's `.second`. Offsetting + /// and missing seconds (or extra seconds) may mean that the `.second` value may be less than, equal to, or greater than the `ordinal` parameter. + public func nthSecond(_ ordinal: Int) throws -> Fixed { return try nth(ordinal) } + + /// Get a sequence of all the seconds in this fixed value. + /// + /// - If this is a `Fixed`, the sequence produces all the seconds in the minute + /// - If this is a `Fixed`, the sequence produces all the seconds in the hour + /// - If this is a `Fixed`, the sequence produces all the seconds in the day + /// - etc. + public var seconds: FixedSequence { + return FixedSequence(parent: self) + } + } extension Fixed where Granularity == Minute { - - /// Retrieve a second in this minute with a specific number - /// - Parameter number: The number of the second (`0`, `13`, etc) - /// - Returns: A `Fixed` whose `.sour` is equal to the provided `number`, or `nil` if no such second can be found - public func second(_ number: Int) -> Fixed? { return numbered(number) } - + + /// Retrieve a second in this minute with a specific number + /// - Parameter number: The number of the second (`0`, `13`, etc) + /// - Returns: A `Fixed` whose `.sour` is equal to the provided `number`, or `nil` if no such second can be found + public func second(_ number: Int) -> Fixed? { return numbered(number) } + } diff --git a/Sources/Time/4-Fixed Values/Fixed+Month.swift b/Sources/Time/4-Fixed Values/Fixed+Month.swift index 424db0e..e077402 100644 --- a/Sources/Time/4-Fixed Values/Fixed+Month.swift +++ b/Sources/Time/4-Fixed Values/Fixed+Month.swift @@ -1,47 +1,47 @@ import Foundation extension Fixed where Granularity: GTOEMonth { - - /// Retrieve the first day of this fixed value - public var firstDay: Fixed { return first() } - - /// Retrieve the last day of this fixed value - public var lastDay: Fixed { return last() } - - /// Retrieve a specific 1-based day from this fixed value - /// - /// Example: - /// ``` - /// let firstDay = try thisFixedYear.nthDay(1) - /// let secondDay = try thisFixedYear.nthDay(2) - /// ``` - /// - /// - Parameter ordinal: The offset of the desired day, as measured from the start of this value's range - /// - Returns: a fixed day - /// - Throws: This method throws a ``TimeError`` if `ordinal` is outside the range of values allowed by the `.calendar`. - /// - /// - Note: The allowable values for `ordinal` depend on the fixed value's `.calendar` and granularity. - /// For example, getting the `.nthDay(72)` of a `Fixed` will throw an error, because no supported calendar has a month - /// with more than about 31 days. However, getting the `.nthDay(72)` of a `Fixed` is fine, because years typically have at least - /// than 340 days in them. - public func nthDay(_ ordinal: Int) throws -> Fixed { return try nth(ordinal) } - - /// Get a sequence of all the days in this fixed value. - /// - /// - If this is a `Fixed`, the sequence produces all the days in the month - /// - If this is a `Fixed`, the sequence produces all the days in the year - /// - etc. - public var days: FixedSequence { - return FixedSequence(parent: self) - } - + + /// Retrieve the first day of this fixed value + public var firstDay: Fixed { return first() } + + /// Retrieve the last day of this fixed value + public var lastDay: Fixed { return last() } + + /// Retrieve a specific 1-based day from this fixed value + /// + /// Example: + /// ``` + /// let firstDay = try thisFixedYear.nthDay(1) + /// let secondDay = try thisFixedYear.nthDay(2) + /// ``` + /// + /// - Parameter ordinal: The offset of the desired day, as measured from the start of this value's range + /// - Returns: a fixed day + /// - Throws: This method throws a ``TimeError`` if `ordinal` is outside the range of values allowed by the `.calendar`. + /// + /// - Note: The allowable values for `ordinal` depend on the fixed value's `.calendar` and granularity. + /// For example, getting the `.nthDay(72)` of a `Fixed` will throw an error, because no supported calendar has a month + /// with more than about 31 days. However, getting the `.nthDay(72)` of a `Fixed` is fine, because years typically have at least + /// than 340 days in them. + public func nthDay(_ ordinal: Int) throws -> Fixed { return try nth(ordinal) } + + /// Get a sequence of all the days in this fixed value. + /// + /// - If this is a `Fixed`, the sequence produces all the days in the month + /// - If this is a `Fixed`, the sequence produces all the days in the year + /// - etc. + public var days: FixedSequence { + return FixedSequence(parent: self) + } + } extension Fixed where Granularity == Month { - - /// Retrieve a day in this month with a specific number - /// - Parameter number: The number of the day (`1`, `13`, etc) - /// - Returns: A `Fixed` whose `.day` is equal to the provided `number`, or `nil` if no such day can be found - public func day(_ number: Int) -> Fixed? { return numbered(number) } - + + /// Retrieve a day in this month with a specific number + /// - Parameter number: The number of the day (`1`, `13`, etc) + /// - Returns: A `Fixed` whose `.day` is equal to the provided `number`, or `nil` if no such day can be found + public func day(_ number: Int) -> Fixed? { return numbered(number) } + } diff --git a/Sources/Time/4-Fixed Values/Fixed+Truncation.swift b/Sources/Time/4-Fixed Values/Fixed+Truncation.swift index e2265d5..2f575f9 100644 --- a/Sources/Time/4-Fixed Values/Fixed+Truncation.swift +++ b/Sources/Time/4-Fixed Values/Fixed+Truncation.swift @@ -1,64 +1,64 @@ import Foundation extension Fixed where Granularity: LTOEYear { - - /// Retrieve the fixed era described by this calendar value. - /// - /// In effect, this property returns a truncated version of the fixed value; - /// all of the calendar units smaller than the era will be removed. - public var fixedEra: Fixed { truncated() } + + /// Retrieve the fixed era described by this calendar value. + /// + /// In effect, this property returns a truncated version of the fixed value; + /// all of the calendar units smaller than the era will be removed. + public var fixedEra: Fixed { truncated() } } extension Fixed where Granularity: LTOEMonth { - - /// Retrieve the fixed year described by this calendar value. - /// - /// In effect, this property returns a truncated version of the fixed value; - /// all of the calendar units smaller than the year will be removed. - public var fixedYear: Fixed { truncated() } + + /// Retrieve the fixed year described by this calendar value. + /// + /// In effect, this property returns a truncated version of the fixed value; + /// all of the calendar units smaller than the year will be removed. + public var fixedYear: Fixed { truncated() } } extension Fixed where Granularity: LTOEDay { - - /// Retrieve the fixed month described by this calendar value. - /// - /// In effect, this property returns a truncated version of the fixed value; - /// all of the calendar units smaller than the month will be removed. - public var fixedMonth: Fixed { truncated() } + + /// Retrieve the fixed month described by this calendar value. + /// + /// In effect, this property returns a truncated version of the fixed value; + /// all of the calendar units smaller than the month will be removed. + public var fixedMonth: Fixed { truncated() } } extension Fixed where Granularity: LTOEHour { - - /// Retrieve the fixed day described by this calendar value. - /// - /// In effect, this property returns a truncated version of the fixed value; - /// all of the calendar units smaller than the day will be removed. - public var fixedDay: Fixed { truncated() } + + /// Retrieve the fixed day described by this calendar value. + /// + /// In effect, this property returns a truncated version of the fixed value; + /// all of the calendar units smaller than the day will be removed. + public var fixedDay: Fixed { truncated() } } extension Fixed where Granularity: LTOEMinute { - - /// Retrieve the fixed hour described by this calendar value. - /// - /// In effect, this property returns a truncated version of the fixed value; - /// all of the calendar units smaller than the hour will be removed. - public var fixedHour: Fixed { truncated() } + + /// Retrieve the fixed hour described by this calendar value. + /// + /// In effect, this property returns a truncated version of the fixed value; + /// all of the calendar units smaller than the hour will be removed. + public var fixedHour: Fixed { truncated() } } extension Fixed where Granularity: LTOESecond { - - /// Retrieve the fixed minute described by this calendar value. - /// - /// In effect, this property returns a truncated version of the fixed value; - /// all of the calendar units smaller than the minute will be removed. - public var fixedMinute: Fixed { truncated() } + + /// Retrieve the fixed minute described by this calendar value. + /// + /// In effect, this property returns a truncated version of the fixed value; + /// all of the calendar units smaller than the minute will be removed. + public var fixedMinute: Fixed { truncated() } } extension Fixed where Granularity: LTOENanosecond { - - /// Retrieve the fixed second described by this calendar value. - /// - /// In effect, this property returns a truncated version of the fixed value; - /// all of the calendar units smaller than the second will be removed. - public var fixedSecond: Fixed { truncated() } + + /// Retrieve the fixed second described by this calendar value. + /// + /// In effect, this property returns a truncated version of the fixed value; + /// all of the calendar units smaller than the second will be removed. + public var fixedSecond: Fixed { truncated() } } diff --git a/Sources/Time/4-Fixed Values/Fixed+Year.swift b/Sources/Time/4-Fixed Values/Fixed+Year.swift index f10053d..d778aca 100644 --- a/Sources/Time/4-Fixed Values/Fixed+Year.swift +++ b/Sources/Time/4-Fixed Values/Fixed+Year.swift @@ -1,34 +1,34 @@ import Foundation extension Fixed where Granularity == Year { - - /// Retrieve the first month of this year - public var firstMonth: Fixed { return first() } - - /// Retrieve the last month of this year - public var lastMonth: Fixed { return last() } - - /// Retrieve a specific 1-based month from this year - /// - /// Example: - /// ``` - /// let firstMonth = try thisFixedYear.nthMonth(1) - /// let secondMonth = try thisFixedYear.nthMonth>(2) - /// ``` - /// - /// - Parameter ordinal: The offset of the desired month, as measured from the start of this value's range - /// - Returns: a fixed month - /// - Throws: This method throws a ``TimeError`` if `ordinal` is outside the range of values allowed by the `.calendar`. - public func nthMonth(_ ordinal: Int) throws -> Fixed { return try nth(ordinal) } - - /// Retrieve a month in this year with a specific number - /// - Parameter month: The number of the month (`1`, `8`, etc) - /// - Returns: A `Fixed` whose `.month` is equal to the provided `number`, or `nil` if no such month can be found - public func month(_ number: Int) -> Fixed? { return numbered(number) } - - /// Get a sequence of all the months in this year - public var months: FixedSequence { - return FixedSequence(parent: self) - } - + + /// Retrieve the first month of this year + public var firstMonth: Fixed { return first() } + + /// Retrieve the last month of this year + public var lastMonth: Fixed { return last() } + + /// Retrieve a specific 1-based month from this year + /// + /// Example: + /// ``` + /// let firstMonth = try thisFixedYear.nthMonth(1) + /// let secondMonth = try thisFixedYear.nthMonth>(2) + /// ``` + /// + /// - Parameter ordinal: The offset of the desired month, as measured from the start of this value's range + /// - Returns: a fixed month + /// - Throws: This method throws a ``TimeError`` if `ordinal` is outside the range of values allowed by the `.calendar`. + public func nthMonth(_ ordinal: Int) throws -> Fixed { return try nth(ordinal) } + + /// Retrieve a month in this year with a specific number + /// - Parameter month: The number of the month (`1`, `8`, etc) + /// - Returns: A `Fixed` whose `.month` is equal to the provided `number`, or `nil` if no such month can be found + public func month(_ number: Int) -> Fixed? { return numbered(number) } + + /// Get a sequence of all the months in this year + public var months: FixedSequence { + return FixedSequence(parent: self) + } + } diff --git a/Sources/Time/4-Fixed Values/Fixed.swift b/Sources/Time/4-Fixed Values/Fixed.swift index 8fe2333..f160f58 100644 --- a/Sources/Time/4-Fixed Values/Fixed.swift +++ b/Sources/Time/4-Fixed Values/Fixed.swift @@ -20,130 +20,132 @@ import Foundation /// /// Fixed values are Equatable, Hashable, Comparable, Sendable, and Codable. public struct Fixed: Sendable { - - /// The set of `Calendar.Components` represented by this particular `Fixed` value - internal static var representedComponents: Set { - return Calendar.Component.from(lower: Granularity.self, to: Era.self) - } - - /// The `Region` value used in computing this `Fixed` value's components. - public let region: Region - - internal let instant: Time.Instant - internal let dateComponents: Foundation.DateComponents - - /// The set of calendar components represented by this `Fixed` value. - public var representedComponents: Set { - return Self.representedComponents - } - - /// The `Calendar` used in computing this `Fixed` value's components, as defined by its `Region`. - public var calendar: Calendar { return region.calendar } - - /// The `TimeZone` used in computing this `Fixed` value's components, as defined by its `Region`. - public var timeZone: TimeZone { return region.timeZone } - - /// The `Locale` used in computing this `Fixed` value's components, as defined by its `Region`. - public var locale: Locale { return region.locale } - - /// The designated initializer for all Fixed values - /// - /// All initializers must funnel through this one. By the time this is called, the components should already be extracted - internal init(region: Region, instant: Instant, components: Foundation.DateComponents) { - self.region = region.snapshot(forced: false) - self.instant = instant - self.dateComponents = components - } - - /// Construct a `Fixed` value from an instantaneous point in time. - /// - Parameter region: The `Region` in which to interpret the point in time - /// - Parameter instant: The `Instant` that is contained by the constructed `Fixed` value - public init(region: Region, instant: Instant) { - let dateComponents = region.calendar.dateComponents(in: region.timeZone, from: instant.date) - .restrict(to: Self.representedComponents) - self.init(region: region, instant: instant, components: dateComponents) - } - - /// Construct a `Fixed` value from an instantaneous point in time. - /// - Parameter region: The `Region` in which to interpret the point in time - /// - Parameter instant: The `Date` that is contained by the constructed `Fixed` value - public init(region: Region, date: Foundation.Date) { - let dateComponents = region.calendar.dateComponents(in: region.timeZone, from: date) - .restrict(to: Self.representedComponents) - self.init(region: region, instant: Instant(date: date), components: dateComponents) - } - - /// Construct a `Fixed` value from a set of `DateComponents`. - /// - /// This method is "strict" because it is fairly easy for it to produce an error. - /// For example, if you are attempting to construct an `Fixed` but only provide - /// a `year` value in the `DateComponents`, then this will throw a `TimeError`. - /// - /// If you are attempting to construct a calendrically impossible date, such as "February 30th", - /// then this will throw a `TimeError`. - /// - /// The matching done on the `DateComponents` is a *strict* match; the returned `Fixed` value will - /// either exactly match the provided components, or this will throw a `TimeError`. - /// - /// - Parameter region: The `Region` in which to interpret the date components - /// - Parameter strictDateComponents: The `DateComponents` describing the desired calendrical date - public init(region: Region, strictDateComponents: DateComponents) throws { - let (date, actualComponents) = try region.calendar.exactDate(from: strictDateComponents, - in: region.timeZone, - matching: Self.representedComponents) - self.init(region: region, instant: Instant(date: date), components: actualComponents) - } - + + /// The set of `Calendar.Components` represented by this particular `Fixed` value + internal static var representedComponents: Set { + return Calendar.Component.from(lower: Granularity.self, to: Era.self) + } + + /// The `Region` value used in computing this `Fixed` value's components. + public let region: Region + + internal let instant: Time.Instant + internal let dateComponents: Foundation.DateComponents + + /// The set of calendar components represented by this `Fixed` value. + public var representedComponents: Set { + return Self.representedComponents + } + + /// The `Calendar` used in computing this `Fixed` value's components, as defined by its `Region`. + public var calendar: Calendar { return region.calendar } + + /// The `TimeZone` used in computing this `Fixed` value's components, as defined by its `Region`. + public var timeZone: TimeZone { return region.timeZone } + + /// The `Locale` used in computing this `Fixed` value's components, as defined by its `Region`. + public var locale: Locale { return region.locale } + + /// The designated initializer for all Fixed values + /// + /// All initializers must funnel through this one. By the time this is called, the components should already be extracted + internal init(region: Region, instant: Instant, components: Foundation.DateComponents) { + self.region = region.snapshot(forced: false) + self.instant = instant + self.dateComponents = components + } + + /// Construct a `Fixed` value from an instantaneous point in time. + /// - Parameter region: The `Region` in which to interpret the point in time + /// - Parameter instant: The `Instant` that is contained by the constructed `Fixed` value + public init(region: Region, instant: Instant) { + let dateComponents = region.calendar.dateComponents(in: region.timeZone, from: instant.date) + .restrict(to: Self.representedComponents) + self.init(region: region, instant: instant, components: dateComponents) + } + + /// Construct a `Fixed` value from an instantaneous point in time. + /// - Parameter region: The `Region` in which to interpret the point in time + /// - Parameter instant: The `Date` that is contained by the constructed `Fixed` value + public init(region: Region, date: Foundation.Date) { + let dateComponents = region.calendar.dateComponents(in: region.timeZone, from: date) + .restrict(to: Self.representedComponents) + self.init(region: region, instant: Instant(date: date), components: dateComponents) + } + + /// Construct a `Fixed` value from a set of `DateComponents`. + /// + /// This method is "strict" because it is fairly easy for it to produce an error. + /// For example, if you are attempting to construct an `Fixed` but only provide + /// a `year` value in the `DateComponents`, then this will throw a `TimeError`. + /// + /// If you are attempting to construct a calendrically impossible date, such as "February 30th", + /// then this will throw a `TimeError`. + /// + /// The matching done on the `DateComponents` is a *strict* match; the returned `Fixed` value will + /// either exactly match the provided components, or this will throw a `TimeError`. + /// + /// - Parameter region: The `Region` in which to interpret the date components + /// - Parameter strictDateComponents: The `DateComponents` describing the desired calendrical date + public init(region: Region, strictDateComponents: DateComponents) throws { + let (date, actualComponents) = try region.calendar.exactDate( + from: strictDateComponents, + in: region.timeZone, + matching: Self.representedComponents) + self.init(region: region, instant: Instant(date: date), components: actualComponents) + } + } extension Fixed: Comparable { - - /// Determine if one `Fixed` value is greater than another `Fixed` value. - /// - /// A `Fixed` value is greater than another if they have the same `Region`, and the first's - /// calendrical components come *after* the other's components. - /// - Parameter lhs: a `Fixed` value - /// - Parameter rhs: a `Fixed` value - public static func > (lhs: Self, rhs: Self) -> Bool { - guard lhs.region == rhs.region else { return false } - - // since we're comparing two Fixed values of the same granularity, - // we can confidently retrieve their respective `firstInstants` and compare those - return lhs.firstInstant > rhs.firstInstant - } - - /// Determine if one `Fixed` value is less than another `Fixed` value. - /// - /// A `Fixed` value is less than another if they have the same `Region`, and the first's - /// calendrical components come *before* the other's components. - /// - Parameter lhs: a `Fixed` value - /// - Parameter rhs: a `Fixed` value - public static func < (lhs: Self, rhs: Self) -> Bool { - guard lhs.region == rhs.region else { return false } - - return lhs.firstInstant < rhs.firstInstant - } - + + /// Determine if one `Fixed` value is greater than another `Fixed` value. + /// + /// A `Fixed` value is greater than another if they have the same `Region`, and the first's + /// calendrical components come *after* the other's components. + /// - Parameter lhs: a `Fixed` value + /// - Parameter rhs: a `Fixed` value + public static func > (lhs: Self, rhs: Self) -> Bool { + guard lhs.region == rhs.region else { return false } + + // since we're comparing two Fixed values of the same granularity, + // we can confidently retrieve their respective `firstInstants` and compare those + return lhs.firstInstant > rhs.firstInstant + } + + /// Determine if one `Fixed` value is less than another `Fixed` value. + /// + /// A `Fixed` value is less than another if they have the same `Region`, and the first's + /// calendrical components come *before* the other's components. + /// - Parameter lhs: a `Fixed` value + /// - Parameter rhs: a `Fixed` value + public static func < (lhs: Self, rhs: Self) -> Bool { + guard lhs.region == rhs.region else { return false } + + return lhs.firstInstant < rhs.firstInstant + } + } extension Fixed: CustomStringConvertible, CustomDebugStringConvertible { - - /// Provide a description of the `Fixed` value. - /// - /// The description is a localized "natural" formatting of the calendar value. - public var description: String { - let style = FixedFormat(naturalFormats: calendar) - return format(style) - } - - public var debugDescription: String { - return "Fixed<\(Granularity.self)>{ " + [ - "timestamp: \(instant.debugDescription)", - "components: \(dateComponents.loggingDescription)", - "locale: \(locale.loggingDescription)", - "calendar: \(calendar.loggingDescription)", - "timeZone: \(timeZone.identifier)" - ].joined(separator: ", ") + " }" - } - + + /// Provide a description of the `Fixed` value. + /// + /// The description is a localized "natural" formatting of the calendar value. + public var description: String { + let style = FixedFormat(naturalFormats: calendar) + return format(style) + } + + public var debugDescription: String { + return "Fixed<\(Granularity.self)>{ " + + [ + "timestamp: \(instant.debugDescription)", + "components: \(dateComponents.loggingDescription)", + "locale: \(locale.loggingDescription)", + "calendar: \(calendar.loggingDescription)", + "timeZone: \(timeZone.identifier)", + ].joined(separator: ", ") + " }" + } + } diff --git a/Sources/Time/5-Differences/Fixed+TimeDifference.swift b/Sources/Time/5-Differences/Fixed+TimeDifference.swift index 38da75a..14ccd02 100644 --- a/Sources/Time/5-Differences/Fixed+TimeDifference.swift +++ b/Sources/Time/5-Differences/Fixed+TimeDifference.swift @@ -1,131 +1,131 @@ import Foundation extension Fixed { - - /// Compute the difference between two Fixed values - /// - /// This operator is equivalent to `lhs.difference(to: rhs)`. - /// - /// - Parameters: - /// - lhs: A fixed value - /// - rhs: A fixed value - /// - Returns: The calendrical difference between the two fixed values. - public static func - (lhs: Self, rhs: Self) -> TimeDifference { - return lhs.difference(to: rhs) - } - - /// Compute the difference from this value to another fixed value. - /// - /// The granularity of the difference is determined by the granularity of the fixed values. Computing the difference between - /// two `Fixed` values will return a `TimeDifference` that describes the interval in terms of years, months, and days. - /// The time difference between two `Fixed` values will be described in terms of years, months, days, hours, minutes, and seconds. - /// - /// For specific granularity comparisions, such as the difference in whole days between two `Fixed` values regardless of - /// their respective months or years, see the various `differenceInWhole...` methods. - /// - /// - Warning: This method allows you to find the difference between two values that belong to different regions. This is typically - /// undesireable and/or nonsensical. - /// - /// - Parameter other: A fixed value - /// - Returns: A ``TimeDifference`` that describes the calendrical difference between the two fixed values. - public func difference(to other: Self) -> TimeDifference { - return computeDifference(to: other) - } - + + /// Compute the difference between two Fixed values + /// + /// This operator is equivalent to `lhs.difference(to: rhs)`. + /// + /// - Parameters: + /// - lhs: A fixed value + /// - rhs: A fixed value + /// - Returns: The calendrical difference between the two fixed values. + public static func - (lhs: Self, rhs: Self) -> TimeDifference { + return lhs.difference(to: rhs) + } + + /// Compute the difference from this value to another fixed value. + /// + /// The granularity of the difference is determined by the granularity of the fixed values. Computing the difference between + /// two `Fixed` values will return a `TimeDifference` that describes the interval in terms of years, months, and days. + /// The time difference between two `Fixed` values will be described in terms of years, months, days, hours, minutes, and seconds. + /// + /// For specific granularity comparisions, such as the difference in whole days between two `Fixed` values regardless of + /// their respective months or years, see the various `differenceInWhole...` methods. + /// + /// - Warning: This method allows you to find the difference between two values that belong to different regions. This is typically + /// undesireable and/or nonsensical. + /// + /// - Parameter other: A fixed value + /// - Returns: A ``TimeDifference`` that describes the calendrical difference between the two fixed values. + public func difference(to other: Self) -> TimeDifference { + return computeDifference(to: other) + } + } extension Fixed where Granularity: LTOEYear { - - /// Compute the difference in whole years from this value to another fixed value. - /// - /// In order for the returned `TimeDifference` to represent a non-zero value, there must be *at least* a full elapsed year - /// between the two fixed values. For example, even though `30 December 2023` is *almost* a full year away from `29 December 2024`, - /// it is still less than a whole year, and therefore the difference in whole years between those two values is zero. - /// - /// - Parameter other: A fixed value - /// - Returns: A ``TimeDifference`` that describes the difference in whole years between the two fixed values. - public func differenceInWholeYears(to other: Self) -> TimeDifference { - return computeDifference(to: other) - } - + + /// Compute the difference in whole years from this value to another fixed value. + /// + /// In order for the returned `TimeDifference` to represent a non-zero value, there must be *at least* a full elapsed year + /// between the two fixed values. For example, even though `30 December 2023` is *almost* a full year away from `29 December 2024`, + /// it is still less than a whole year, and therefore the difference in whole years between those two values is zero. + /// + /// - Parameter other: A fixed value + /// - Returns: A ``TimeDifference`` that describes the difference in whole years between the two fixed values. + public func differenceInWholeYears(to other: Self) -> TimeDifference { + return computeDifference(to: other) + } + } extension Fixed where Granularity: LTOEMonth { - - /// Compute the difference in whole months from this value to another fixed value. - /// - /// In order for the returned `TimeDifference` to represent a non-zero value, there must be *at least* a full elapsed month - /// between the two fixed values. For example, even though `1 October` is *almost* a full month away from `31 October`, - /// it is still less than a whole month, and therefore the difference in whole months between those two values is zero. - /// - /// - Parameter other: A fixed value - /// - Returns: A ``TimeDifference`` that describes the difference in whole months between the two fixed values. - public func differenceInWholeMonths(to other: Self) -> TimeDifference { - return computeDifference(to: other) - } - + + /// Compute the difference in whole months from this value to another fixed value. + /// + /// In order for the returned `TimeDifference` to represent a non-zero value, there must be *at least* a full elapsed month + /// between the two fixed values. For example, even though `1 October` is *almost* a full month away from `31 October`, + /// it is still less than a whole month, and therefore the difference in whole months between those two values is zero. + /// + /// - Parameter other: A fixed value + /// - Returns: A ``TimeDifference`` that describes the difference in whole months between the two fixed values. + public func differenceInWholeMonths(to other: Self) -> TimeDifference { + return computeDifference(to: other) + } + } extension Fixed where Granularity: LTOEDay { - - /// Compute the difference in whole days from this value to another fixed value. - /// - /// In order for the returned `TimeDifference` to represent a non-zero value, there must be *at least* a full elapsed day - /// between the two fixed values. For example, even though `1 February 9:00 AM` is *almost* a full day away from `2 February 8:00 AM`, - /// it is still less than a whole day, and therefore the difference in whole days between those two values is zero. - /// - /// - Parameter other: A fixed value - /// - Returns: A ``TimeDifference`` that describes the difference in whole days between the two fixed values. - public func differenceInWholeDays(to other: Self) -> TimeDifference { - return computeDifference(to: other) - } - + + /// Compute the difference in whole days from this value to another fixed value. + /// + /// In order for the returned `TimeDifference` to represent a non-zero value, there must be *at least* a full elapsed day + /// between the two fixed values. For example, even though `1 February 9:00 AM` is *almost* a full day away from `2 February 8:00 AM`, + /// it is still less than a whole day, and therefore the difference in whole days between those two values is zero. + /// + /// - Parameter other: A fixed value + /// - Returns: A ``TimeDifference`` that describes the difference in whole days between the two fixed values. + public func differenceInWholeDays(to other: Self) -> TimeDifference { + return computeDifference(to: other) + } + } extension Fixed where Granularity: LTOEHour { - - /// Compute the difference in whole hours from this value to another fixed value. - /// - /// In order for the returned `TimeDifference` to represent a non-zero value, there must be *at least* a full elapsed hour - /// between the two fixed values. For example, even though `1 February 9:13 AM` is *almost* a full hour away from `1 February 10:06 AM`, - /// it is still less than a whole hour, and therefore the difference in whole hours between those two values is zero. - /// - /// - Parameter other: A fixed value - /// - Returns: A ``TimeDifference`` that describes the difference in whole hours between the two fixed values. - public func differenceInWholeHours(to other: Self) -> TimeDifference { - return computeDifference(to: other) - } - + + /// Compute the difference in whole hours from this value to another fixed value. + /// + /// In order for the returned `TimeDifference` to represent a non-zero value, there must be *at least* a full elapsed hour + /// between the two fixed values. For example, even though `1 February 9:13 AM` is *almost* a full hour away from `1 February 10:06 AM`, + /// it is still less than a whole hour, and therefore the difference in whole hours between those two values is zero. + /// + /// - Parameter other: A fixed value + /// - Returns: A ``TimeDifference`` that describes the difference in whole hours between the two fixed values. + public func differenceInWholeHours(to other: Self) -> TimeDifference { + return computeDifference(to: other) + } + } extension Fixed where Granularity: LTOEMinute { - - /// Compute the difference in whole minutes from this value to another fixed value. - /// - /// In order for the returned `TimeDifference` to represent a non-zero value, there must be *at least* a full elapsed minute - /// between the two fixed values. For example, even though `1 February 9:13:40 AM` is *almost* a full minute away from `1 February 9:14:39 AM`, - /// it is still less than a whole minute, and therefore the difference in whole minutes between those two values is zero. - /// - /// - Parameter other: A fixed value - /// - Returns: A ``TimeDifference`` that describes the difference in whole minutes between the two fixed values. - public func differenceInWholeMinutes(to other: Self) -> TimeDifference { - return computeDifference(to: other) - } - + + /// Compute the difference in whole minutes from this value to another fixed value. + /// + /// In order for the returned `TimeDifference` to represent a non-zero value, there must be *at least* a full elapsed minute + /// between the two fixed values. For example, even though `1 February 9:13:40 AM` is *almost* a full minute away from `1 February 9:14:39 AM`, + /// it is still less than a whole minute, and therefore the difference in whole minutes between those two values is zero. + /// + /// - Parameter other: A fixed value + /// - Returns: A ``TimeDifference`` that describes the difference in whole minutes between the two fixed values. + public func differenceInWholeMinutes(to other: Self) -> TimeDifference { + return computeDifference(to: other) + } + } extension Fixed where Granularity: LTOESecond { - - /// Compute the difference in whole seconds from this value to another fixed value. - /// - /// In order for the returned `TimeDifference` to represent a non-zero value, there must be *at least* a full elapsed second - /// between the two fixed values. For example, even though `1 February 9:13:40.123 AM` is *almost* a full minute away from `1 February 9:13:41.012 AM`, - /// it is still less than a whole second, and therefore the difference in whole seconds between those two values is zero. - /// - /// - Parameter other: A fixed value - /// - Returns: A ``TimeDifference`` that describes the difference in whole seconds between the two fixed values. - public func differenceInWholeSeconds(to other: Self) -> TimeDifference { - return computeDifference(to: other) - } - + + /// Compute the difference in whole seconds from this value to another fixed value. + /// + /// In order for the returned `TimeDifference` to represent a non-zero value, there must be *at least* a full elapsed second + /// between the two fixed values. For example, even though `1 February 9:13:40.123 AM` is *almost* a full minute away from `1 February 9:13:41.012 AM`, + /// it is still less than a whole second, and therefore the difference in whole seconds between those two values is zero. + /// + /// - Parameter other: A fixed value + /// - Returns: A ``TimeDifference`` that describes the difference in whole seconds between the two fixed values. + public func differenceInWholeSeconds(to other: Self) -> TimeDifference { + return computeDifference(to: other) + } + } diff --git a/Sources/Time/5-Differences/TimeDifference+Invalid.swift b/Sources/Time/5-Differences/TimeDifference+Invalid.swift index b94c306..2e57eb8 100644 --- a/Sources/Time/5-Differences/TimeDifference+Invalid.swift +++ b/Sources/Time/5-Differences/TimeDifference+Invalid.swift @@ -3,120 +3,120 @@ import Foundation /// INVALID ADDITION OPERATORS extension Fixed where Granularity == Year { - - @available(*, unavailable, message: "Adding months to a year is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding days to a year is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding hours to a year is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding minutes to a year is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding seconds to a year is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding nanoseconds to a year is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - - @available(*, unavailable, message: "Subtracting months from a year is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting days from a year is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting hours from a year is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting minutes from a year is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting seconds from a year is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting nanoseconds from a year is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } + + @available(*, unavailable, message: "Adding months to a year is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding days to a year is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding hours to a year is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding minutes to a year is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding seconds to a year is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding nanoseconds to a year is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + + @available(*, unavailable, message: "Subtracting months from a year is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting days from a year is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting hours from a year is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting minutes from a year is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting seconds from a year is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting nanoseconds from a year is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } } extension Fixed where Granularity == Month { - - @available(*, unavailable, message: "Adding days to a month is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding hours to a month is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding minutes to a month is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding seconds to a month is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding nanoseconds to a month is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - - @available(*, unavailable, message: "Subtracting days from a month is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting hours from a month is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting minutes from a month is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting seconds from a month is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting nanoseconds from a month is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - + + @available(*, unavailable, message: "Adding days to a month is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding hours to a month is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding minutes to a month is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding seconds to a month is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding nanoseconds to a month is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + + @available(*, unavailable, message: "Subtracting days from a month is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting hours from a month is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting minutes from a month is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting seconds from a month is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting nanoseconds from a month is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + } extension Fixed where Granularity == Day { - - @available(*, unavailable, message: "Adding hours to a day is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding minutes to a day is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding seconds to a day is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding nanoseconds to a day is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - - @available(*, unavailable, message: "Subtracting hours from a day is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting minutes from a day is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting seconds from a day is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting nanoseconds from a day is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - + + @available(*, unavailable, message: "Adding hours to a day is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding minutes to a day is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding seconds to a day is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding nanoseconds to a day is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + + @available(*, unavailable, message: "Subtracting hours from a day is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting minutes from a day is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting seconds from a day is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting nanoseconds from a day is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + } extension Fixed where Granularity == Hour { - - @available(*, unavailable, message: "Adding minutes to an hour is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding seconds to an hour is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding nanoseconds to an hour is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - - @available(*, unavailable, message: "Subtracting minutes from an hour is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting seconds from an hour is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting nanoseconds from an hour is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - + + @available(*, unavailable, message: "Adding minutes to an hour is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding seconds to an hour is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding nanoseconds to an hour is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + + @available(*, unavailable, message: "Subtracting minutes from an hour is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting seconds from an hour is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting nanoseconds from an hour is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + } extension Fixed where Granularity == Minute { - - @available(*, unavailable, message: "Adding seconds to a minute is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Adding nanoseconds to a minute is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - - @available(*, unavailable, message: "Subtracting seconds from a minute is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - @available(*, unavailable, message: "Subtracting nanoseconds from a minute is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - + + @available(*, unavailable, message: "Adding seconds to a minute is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Adding nanoseconds to a minute is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + + @available(*, unavailable, message: "Subtracting seconds from a minute is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + @available(*, unavailable, message: "Subtracting nanoseconds from a minute is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + } extension Fixed where Granularity == Second { - - @available(*, unavailable, message: "Adding nanoseconds to a second is invalid") - public static func +(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - - @available(*, unavailable, message: "Subtracting nanoseconds from a second is invalid") - public static func -(lhs: Self, rhs: TimeDifference) -> Never { invalid() } - + + @available(*, unavailable, message: "Adding nanoseconds to a second is invalid") + public static func + (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + + @available(*, unavailable, message: "Subtracting nanoseconds from a second is invalid") + public static func - (lhs: Self, rhs: TimeDifference) -> Never { invalid() } + } diff --git a/Sources/Time/5-Differences/TimeDifference.swift b/Sources/Time/5-Differences/TimeDifference.swift index 7ab99bb..b1f7b5a 100644 --- a/Sources/Time/5-Differences/TimeDifference.swift +++ b/Sources/Time/5-Differences/TimeDifference.swift @@ -8,144 +8,178 @@ import Foundation /// - Parameter MaximumGranularity: The largest represesntable ``Unit`` expressed in this time difference. /// public struct TimeDifference: Sendable { - internal let dateComponents: DateComponents - - internal init(_ dateComponents: DateComponents) { - let allowed = Calendar.Component.from(lower: MinimumGranularity.self, to: MaximumGranularity.self) - self.dateComponents = dateComponents.restrict(to: allowed) - } - - internal init(value: Int, unit: Calendar.Component) { - self.init(DateComponents(value: value, component: unit)) - } - - /// Negate the time difference - /// - Returns: A new `TimeDifference` value where all the represented unit values have been negated. - public var negated: TimeDifference { - return TimeDifference(dateComponents.scale(by: -1)) - } - - internal func scale(by scale: Int) -> Self { - if scale == 1 { return self } - return Self(dateComponents.scale(by: scale)) - } + internal let dateComponents: DateComponents + + internal init(_ dateComponents: DateComponents) { + let allowed = Calendar.Component.from( + lower: MinimumGranularity.self, to: MaximumGranularity.self) + self.dateComponents = dateComponents.restrict(to: allowed) + } + + internal init(value: Int, unit: Calendar.Component) { + self.init(DateComponents(value: value, component: unit)) + } + + /// Negate the time difference + /// - Returns: A new `TimeDifference` value where all the represented unit values have been negated. + public var negated: TimeDifference { + return TimeDifference(dateComponents.scale(by: -1)) + } + + internal func scale(by scale: Int) -> Self { + if scale == 1 { return self } + return Self(dateComponents.scale(by: scale)) + } } extension TimeDifference where MinimumGranularity: LTOEYear, MaximumGranularity == Era { - - /// Create a time difference representing a specific number of eras - /// - Parameter value: the number of eras - /// - Returns: A `TimeDifference` that describes an interval of `value` eras. - /// - Warning: Most commonly-used calendars do not use eras and struggle to correctly perform calculations involving them. - public static func eras(_ value: Int) -> TimeDifference { return self.init(value: value, unit: .era) } - - /// Retrieve the number of eras in a calendrical difference. - public var eras: Int { - return dateComponents.era - .unwrap("A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have an era value") - } + + /// Create a time difference representing a specific number of eras + /// - Parameter value: the number of eras + /// - Returns: A `TimeDifference` that describes an interval of `value` eras. + /// - Warning: Most commonly-used calendars do not use eras and struggle to correctly perform calculations involving them. + public static func eras(_ value: Int) -> TimeDifference { + return self.init(value: value, unit: .era) + } + + /// Retrieve the number of eras in a calendrical difference. + public var eras: Int { + return dateComponents.era + .unwrap( + "A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have an era value" + ) + } } extension TimeDifference where MinimumGranularity: LTOEYear, MaximumGranularity: GTOEYear { - - /// Create a time difference representing a specific number of years - /// - Parameter value: the number of years - /// - Returns: A `TimeDifference` that represents an interval of `value` years. - /// - Note: Most calendars perform [intercalation](https://en.wikipedia.org/wiki/Intercalation_%28timekeeping%29), and so years may - /// not have a consistent length. Therefore, a `TimeDifference` of `.years(1)` can represent *many* possible absolute intervals, depending on the - /// fixed values to which the interval is applied. - public static func years(_ value: Int) -> TimeDifference { return self.init(value: value, unit: .year) } - - /// Retrieve the number of years in a calendrical difference. - public var years: Int { - return dateComponents.year - .unwrap("A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have a year value") - } + + /// Create a time difference representing a specific number of years + /// - Parameter value: the number of years + /// - Returns: A `TimeDifference` that represents an interval of `value` years. + /// - Note: Most calendars perform [intercalation](https://en.wikipedia.org/wiki/Intercalation_%28timekeeping%29), and so years may + /// not have a consistent length. Therefore, a `TimeDifference` of `.years(1)` can represent *many* possible absolute intervals, depending on the + /// fixed values to which the interval is applied. + public static func years(_ value: Int) -> TimeDifference { + return self.init(value: value, unit: .year) + } + + /// Retrieve the number of years in a calendrical difference. + public var years: Int { + return dateComponents.year + .unwrap( + "A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have a year value" + ) + } } extension TimeDifference where MinimumGranularity: LTOEMonth, MaximumGranularity: GTOEMonth { - - /// Create a time difference representing a specific number of months - /// - Parameter value: the number of months - /// - Returns: A `TimeDifference` that represents an interval of `value` months. - /// - Note: Most calendars describe months that do not have a consistent length. Therefore, a `TimeDifference` of `.months(1)` - /// can represent *many* possible absolute intervals, depending on the fixed values to which the interval is applied. - public static func months(_ value: Int) -> TimeDifference { return self.init(value: value, unit: .month) } - - /// Retrieve the number of months in a calendrical difference. - public var months: Int { - return dateComponents.month - .unwrap("A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have a month value") - } + + /// Create a time difference representing a specific number of months + /// - Parameter value: the number of months + /// - Returns: A `TimeDifference` that represents an interval of `value` months. + /// - Note: Most calendars describe months that do not have a consistent length. Therefore, a `TimeDifference` of `.months(1)` + /// can represent *many* possible absolute intervals, depending on the fixed values to which the interval is applied. + public static func months(_ value: Int) -> TimeDifference { + return self.init(value: value, unit: .month) + } + + /// Retrieve the number of months in a calendrical difference. + public var months: Int { + return dateComponents.month + .unwrap( + "A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have a month value" + ) + } } extension TimeDifference where MinimumGranularity: LTOEDay, MaximumGranularity: GTOEDay { - - /// Create a time difference representing a specific number of days - /// - Parameter value: the number of days - /// - Returns: A `TimeDifference` that represents an interval of `value` days. - /// - Note: Most calendars describe days that do not have a consistent length, especially when consider Daylight Saving Time. Therefore, - /// a `TimeDifference` of `.days(1)` can represent *many* possible absolute intervals, depending on the fixed values to which the interval is applied. - public static func days(_ value: Int) -> TimeDifference { return self.init(value: value, unit: .day) } - - /// Retrieve the number of days in a calendrical difference. - public var days: Int { - return dateComponents.day.unwrap("A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have a day value") - } + + /// Create a time difference representing a specific number of days + /// - Parameter value: the number of days + /// - Returns: A `TimeDifference` that represents an interval of `value` days. + /// - Note: Most calendars describe days that do not have a consistent length, especially when consider Daylight Saving Time. Therefore, + /// a `TimeDifference` of `.days(1)` can represent *many* possible absolute intervals, depending on the fixed values to which the interval is applied. + public static func days(_ value: Int) -> TimeDifference { + return self.init(value: value, unit: .day) + } + + /// Retrieve the number of days in a calendrical difference. + public var days: Int { + return dateComponents.day.unwrap( + "A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have a day value" + ) + } } extension TimeDifference where MinimumGranularity: LTOEHour, MaximumGranularity: GTOEHour { - - /// Create a time difference representing a specific number of hours - /// - Parameter value: the number of hours - /// - Returns: A `TimeDifference` that represents an interval of `value` hours. - public static func hours(_ value: Int) -> TimeDifference { return self.init(value: value, unit: .hour) } - - /// Retrieve the number of hours in a calendrical difference. - public var hours: Int { - return dateComponents.hour - .unwrap("A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have an hour value") - } + + /// Create a time difference representing a specific number of hours + /// - Parameter value: the number of hours + /// - Returns: A `TimeDifference` that represents an interval of `value` hours. + public static func hours(_ value: Int) -> TimeDifference { + return self.init(value: value, unit: .hour) + } + + /// Retrieve the number of hours in a calendrical difference. + public var hours: Int { + return dateComponents.hour + .unwrap( + "A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have an hour value" + ) + } } extension TimeDifference where MinimumGranularity: LTOEMinute, MaximumGranularity: GTOEMinute { - - /// Create a time difference representing a specific number of minutes - /// - Parameter value: the number of minutes - /// - Returns: A `TimeDifference` that represents an interval of `value` minutes. - public static func minutes(_ value: Int) -> TimeDifference { return self.init(value: value, unit: .minute) } - - /// Retrieve the number of minutes in a calendrical difference. - public var minutes: Int { - return dateComponents.minute - .unwrap("A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have a minute value") - } + + /// Create a time difference representing a specific number of minutes + /// - Parameter value: the number of minutes + /// - Returns: A `TimeDifference` that represents an interval of `value` minutes. + public static func minutes(_ value: Int) -> TimeDifference { + return self.init(value: value, unit: .minute) + } + + /// Retrieve the number of minutes in a calendrical difference. + public var minutes: Int { + return dateComponents.minute + .unwrap( + "A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have a minute value" + ) + } } extension TimeDifference where MinimumGranularity: LTOESecond, MaximumGranularity: GTOESecond { - - /// Create a time difference representing a specific number of seconds - /// - Parameter value: the number of seconds - /// - Returns: A `TimeDifference` that represents an interval of `value` seconds. - public static func seconds(_ value: Int) -> TimeDifference { return self.init(value: value, unit: .second) } - - /// Retrieve the number of seconds in a calendrical difference. - public var seconds: Int { - return dateComponents.second - .unwrap("A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have a second value") - } + + /// Create a time difference representing a specific number of seconds + /// - Parameter value: the number of seconds + /// - Returns: A `TimeDifference` that represents an interval of `value` seconds. + public static func seconds(_ value: Int) -> TimeDifference { + return self.init(value: value, unit: .second) + } + + /// Retrieve the number of seconds in a calendrical difference. + public var seconds: Int { + return dateComponents.second + .unwrap( + "A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have a second value" + ) + } } -extension TimeDifference where MinimumGranularity: LTOENanosecond, MaximumGranularity: GTOENanosecond { - - /// Create a time difference representing a specific number of nanoseconds - /// - Parameter value: the number of nanoseconds - /// - Returns: A `TimeDifference` that represents an interval of `value` nanoseconds. - public static func nanoseconds(_ value: Int) -> TimeDifference { return self.init(value: value, unit: .nanosecond) } - - /// Retrieve the number of nanoseconds in a calendrical difference. - public var nanoseconds: Int { - return dateComponents.nanosecond - .unwrap("A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have a nanosecond value") - } +extension TimeDifference +where MinimumGranularity: LTOENanosecond, MaximumGranularity: GTOENanosecond { + + /// Create a time difference representing a specific number of nanoseconds + /// - Parameter value: the number of nanoseconds + /// - Returns: A `TimeDifference` that represents an interval of `value` nanoseconds. + public static func nanoseconds(_ value: Int) -> TimeDifference { + return self.init(value: value, unit: .nanosecond) + } + + /// Retrieve the number of nanoseconds in a calendrical difference. + public var nanoseconds: Int { + return dateComponents.nanosecond + .unwrap( + "A TimeDifference<\(MinimumGranularity.self), \(MaximumGranularity.self)> must have a nanosecond value" + ) + } } diff --git a/Sources/Time/6-Adjustments/Fixed+SafeAdjustment.swift b/Sources/Time/6-Adjustments/Fixed+SafeAdjustment.swift index 487131b..1e8a6c3 100644 --- a/Sources/Time/6-Adjustments/Fixed+SafeAdjustment.swift +++ b/Sources/Time/6-Adjustments/Fixed+SafeAdjustment.swift @@ -1,246 +1,252 @@ import Foundation extension Fixed { - - /// Apply a time difference to a fixed value - /// - /// This operator is equivalent to `lhs.applying(difference: rhs)` - /// - /// - Parameters: - /// - lhs: A fixed value - /// - rhs: A time difference - /// - Returns: A new fixed value that has been adjusted forward or backwards in time - public static func +(lhs: Self, rhs: TimeDifference) -> Self { - return lhs.applying(difference: rhs) - } - - /// Apply a time difference to a fixed value - /// - /// This operator is equivalent to `lhs.applying(difference: rhs.negated)` - /// - /// - Parameters: - /// - lhs: A fixed value - /// - rhs: A time difference - /// - Returns: A new fixed value that has been adjusted forward or backwards in time - public static func -(lhs: Self, rhs: TimeDifference) -> Self { - return lhs.applying(difference: rhs.negated) - } - - /// Adjust a fixed value by applying a temporal delta value. - /// - /// - Parameter difference: The `TimeDifference` that describes the difference between the fixed value - /// and the produced value. - /// - Returns: A new fixed value that has been adjusted forwards or backwards in time - public func applying(difference: TimeDifference) -> Self { - let d = self.range.lowerBound.date - let diff = difference.dateComponents - let newDate = self.calendar.date(byAdding: diff, to: d).unwrap("Unable to add \(diff) to \(self)") - return Self(region: self.region, date: newDate) - } - - /// Adjust the fixed value forwards or backwards. - /// - /// The amount of adjustment depends on the value's granularity. For example, if you have a `Fixed`, then `.offset(by: 2)` - /// moves the value 2 days forward in time. If you have a `Fixed`, then `.offset(by: 2)` moves the value 2 *months* forward - /// in time. - /// - Parameter count: The number of units by which to adjust this fixed value. - /// - Returns: A new fixed value that has been adjusted forward or backwards in time. - public func offset(by count: Int) -> Self { - guard count != 0 else { return self } - - let difference = TimeDifference(value: count, unit: Granularity.component) - return applying(difference: difference) - } - - /// Adjust the fixed value forward by one `Granularity` unit. - /// - /// This is equivalent to `.offset(by: 1)` - public var next: Self { offset(by: 1) } - - /// Adjust the fixed value backward by one `Granularity` unit. - /// - /// This is equivalent to `.offset(by: -1)` - public var previous: Self { offset(by: -1) } + + /// Apply a time difference to a fixed value + /// + /// This operator is equivalent to `lhs.applying(difference: rhs)` + /// + /// - Parameters: + /// - lhs: A fixed value + /// - rhs: A time difference + /// - Returns: A new fixed value that has been adjusted forward or backwards in time + public static func + (lhs: Self, rhs: TimeDifference) -> Self { + return lhs.applying(difference: rhs) + } + + /// Apply a time difference to a fixed value + /// + /// This operator is equivalent to `lhs.applying(difference: rhs.negated)` + /// + /// - Parameters: + /// - lhs: A fixed value + /// - rhs: A time difference + /// - Returns: A new fixed value that has been adjusted forward or backwards in time + public static func - (lhs: Self, rhs: TimeDifference) -> Self { + return lhs.applying(difference: rhs.negated) + } + + /// Adjust a fixed value by applying a temporal delta value. + /// + /// - Parameter difference: The `TimeDifference` that describes the difference between the fixed value + /// and the produced value. + /// - Returns: A new fixed value that has been adjusted forwards or backwards in time + public func applying(difference: TimeDifference) -> Self { + let d = self.range.lowerBound.date + let diff = difference.dateComponents + let newDate = self.calendar.date(byAdding: diff, to: d).unwrap( + "Unable to add \(diff) to \(self)") + return Self(region: self.region, date: newDate) + } + + /// Adjust the fixed value forwards or backwards. + /// + /// The amount of adjustment depends on the value's granularity. For example, if you have a `Fixed`, then `.offset(by: 2)` + /// moves the value 2 days forward in time. If you have a `Fixed`, then `.offset(by: 2)` moves the value 2 *months* forward + /// in time. + /// - Parameter count: The number of units by which to adjust this fixed value. + /// - Returns: A new fixed value that has been adjusted forward or backwards in time. + public func offset(by count: Int) -> Self { + guard count != 0 else { return self } + + let difference = TimeDifference(value: count, unit: Granularity.component) + return applying(difference: difference) + } + + /// Adjust the fixed value forward by one `Granularity` unit. + /// + /// This is equivalent to `.offset(by: 1)` + public var next: Self { offset(by: 1) } + + /// Adjust the fixed value backward by one `Granularity` unit. + /// + /// This is equivalent to `.offset(by: -1)` + public var previous: Self { offset(by: -1) } } extension Fixed where Granularity: LTOEYear { - - /// Create a new `Fixed` value by moving forward one year. - public var nextYear: Self { return adding(years: 1) } - - /// Create a new `Fixed` value by moving backward one year. - public var previousYear: Self { return subtracting(years: 1) } - - /// Create a new `Fixed` value by moving forward some number of years. - /// - Parameter years: The number of years by which to move forward. - public func adding(years: Int) -> Self { return applying(difference: .years(years)) } - - /// Create a new `Fixed` value by moving backward some number of years. - /// - Parameter years: The number of years by which to move backward. - public func subtracting(years: Int) -> Self { return applying(difference: .years(-years)) } - + + /// Create a new `Fixed` value by moving forward one year. + public var nextYear: Self { return adding(years: 1) } + + /// Create a new `Fixed` value by moving backward one year. + public var previousYear: Self { return subtracting(years: 1) } + + /// Create a new `Fixed` value by moving forward some number of years. + /// - Parameter years: The number of years by which to move forward. + public func adding(years: Int) -> Self { return applying(difference: .years(years)) } + + /// Create a new `Fixed` value by moving backward some number of years. + /// - Parameter years: The number of years by which to move backward. + public func subtracting(years: Int) -> Self { return applying(difference: .years(-years)) } + } extension Fixed where Granularity: LTOEMonth { - - /// Create a new `Fixed` value by moving forward one month. - public var nextMonth: Self { return adding(months: 1) } - - /// Create a new `Fixed` value by moving backward one month. - public var previousMonth: Self { return subtracting(months: 1) } - - /// Create a new `Fixed` value by moving forward some number of months. - /// - Parameter months: The number of months by which to move forward. - public func adding(months: Int) -> Self { return applying(difference: .months(months)) } - - /// Create a new `Fixed` value by moving backward some number of months. - /// - Parameter months: The number of months by which to move backward. - public func subtracting(months: Int) -> Self { return applying(difference: .months(-months)) } - + + /// Create a new `Fixed` value by moving forward one month. + public var nextMonth: Self { return adding(months: 1) } + + /// Create a new `Fixed` value by moving backward one month. + public var previousMonth: Self { return subtracting(months: 1) } + + /// Create a new `Fixed` value by moving forward some number of months. + /// - Parameter months: The number of months by which to move forward. + public func adding(months: Int) -> Self { return applying(difference: .months(months)) } + + /// Create a new `Fixed` value by moving backward some number of months. + /// - Parameter months: The number of months by which to move backward. + public func subtracting(months: Int) -> Self { return applying(difference: .months(-months)) } + } extension Fixed where Granularity: LTOEDay { - - /// Adjust the date to the beginning of the calendar's week. - public var startOfWeek: Self { - var s = self - let targetWeekday = region.calendar.firstWeekday - - // Github issue #71 tracks improving this - while s.dayOfWeek != targetWeekday { - s = s.previousDay - } - return s + + /// Adjust the date to the beginning of the calendar's week. + public var startOfWeek: Self { + var s = self + let targetWeekday = region.calendar.firstWeekday + + // Github issue #71 tracks improving this + while s.dayOfWeek != targetWeekday { + s = s.previousDay } - - /// Create a new `Fixed` value by moving forward one day. - public var nextDay: Self { return adding(days: 1) } - - /// Create a new `Fixed` value by moving backward one day. - public var previousDay: Self { return subtracting(days: 1) } - - /// Create a new `Fixed` value by moving forward some number of days. - /// - Parameter days: The number of days by which to move forward. - public func adding(days: Int) -> Self { return applying(difference: .days(days)) } - - /// Create a new `Fixed` value by moving backward some number of days. - /// - Parameter days: The number of days by which to move backward. - public func subtracting(days: Int) -> Self { return applying(difference: .days(-days)) } - - /// Create a new `Fixed` value that corresponds to the specified day of the week - /// - Parameter dayOfWeek: The numeric value for the day of the week. - /// For the Gregorian calendar, 1 = Sunday, 2 = Monday, ... 7 = Saturday - /// - Returns: A fixed value whose `.dayOfWeek` is equal to the `dayOfWeek` parameter. - public func next(dayOfWeek: Int) -> Self { - let daysInWeek = calendar.maximumRange(of: .weekday) ?? 1 ..< 8 - var day = dayOfWeek - while day < daysInWeek.lowerBound { day += daysInWeek.count } - day %= daysInWeek.count - - let thisDay = self.dayOfWeek - - var offset = 0 - if day < thisDay { - offset = (day + daysInWeek.count) - thisDay - } else if day == thisDay { - offset = daysInWeek.count - } else { - offset = day - thisDay - } - - // try the "O(1)" jump first - let proposed = self.adding(days: offset) - if proposed.dayOfWeek == day { return proposed } - - // something went wrong. manually scan forward - var current = self.nextDay - while current.dayOfWeek != day { - current = current.nextDay - } - return current + return s + } + + /// Create a new `Fixed` value by moving forward one day. + public var nextDay: Self { return adding(days: 1) } + + /// Create a new `Fixed` value by moving backward one day. + public var previousDay: Self { return subtracting(days: 1) } + + /// Create a new `Fixed` value by moving forward some number of days. + /// - Parameter days: The number of days by which to move forward. + public func adding(days: Int) -> Self { return applying(difference: .days(days)) } + + /// Create a new `Fixed` value by moving backward some number of days. + /// - Parameter days: The number of days by which to move backward. + public func subtracting(days: Int) -> Self { return applying(difference: .days(-days)) } + + /// Create a new `Fixed` value that corresponds to the specified day of the week + /// - Parameter dayOfWeek: The numeric value for the day of the week. + /// For the Gregorian calendar, 1 = Sunday, 2 = Monday, ... 7 = Saturday + /// - Returns: A fixed value whose `.dayOfWeek` is equal to the `dayOfWeek` parameter. + public func next(dayOfWeek: Int) -> Self { + let daysInWeek = calendar.maximumRange(of: .weekday) ?? 1..<8 + var day = dayOfWeek + while day < daysInWeek.lowerBound { day += daysInWeek.count } + day %= daysInWeek.count + + let thisDay = self.dayOfWeek + + var offset = 0 + if day < thisDay { + offset = (day + daysInWeek.count) - thisDay + } else if day == thisDay { + offset = daysInWeek.count + } else { + offset = day - thisDay } - - #if !os(Linux) + + // try the "O(1)" jump first + let proposed = self.adding(days: offset) + if proposed.dayOfWeek == day { return proposed } + + // something went wrong. manually scan forward + var current = self.nextDay + while current.dayOfWeek != day { + current = current.nextDay + } + return current + } + + #if !os(Linux) /// Create a new `Fixed` value that corresponds to the specified weekday /// - Parameter weekday: The day of the week. /// - Returns: A fixed value whose `.weekday` is equal to the `weekday` parameter. /// - Warning: This property is not available on Linux + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) public func next(weekday: Locale.Weekday) -> Self { - return self.next(dayOfWeek: weekday.dayOfWeek) + return self.next(dayOfWeek: weekday.dayOfWeek) } - #endif - + #endif + } extension Fixed where Granularity: LTOEHour { - - /// Create a new `Fixed` value by moving forward one hour. - public var nextHour: Self { return adding(hours: 1) } - - /// Create a new `Fixed` value by moving backward one hour. - public var previousHour: Self { return subtracting(hours: 1) } - - /// Create a new `Fixed` value by moving forward some number of hours. - /// - Parameter hours: The number of hours by which to move forward. - public func adding(hours: Int) -> Self { return applying(difference: .hours(hours)) } - - /// Create a new `Fixed` value by moving backward some number of hours. - /// - Parameter hours: The number of hours by which to move backward. - public func subtracting(hours: Int) -> Self { return applying(difference: .hours(-hours)) } - + + /// Create a new `Fixed` value by moving forward one hour. + public var nextHour: Self { return adding(hours: 1) } + + /// Create a new `Fixed` value by moving backward one hour. + public var previousHour: Self { return subtracting(hours: 1) } + + /// Create a new `Fixed` value by moving forward some number of hours. + /// - Parameter hours: The number of hours by which to move forward. + public func adding(hours: Int) -> Self { return applying(difference: .hours(hours)) } + + /// Create a new `Fixed` value by moving backward some number of hours. + /// - Parameter hours: The number of hours by which to move backward. + public func subtracting(hours: Int) -> Self { return applying(difference: .hours(-hours)) } + } extension Fixed where Granularity: LTOEMinute { - - /// Create a new `Fixed` value by moving forward one minute. - public var nextMinute: Self { return adding(minutes: 1) } - - /// Create a new `Fixed` value by moving backward one minute. - public var previousMinute: Self { return subtracting(minutes: 1) } - - /// Create a new `Fixed` value by moving forward some number of minutes. - /// - Parameter minutes: The number of minutes by which to move forward. - public func adding(minutes: Int) -> Self { return applying(difference: .minutes(minutes)) } - - /// Create a new `Fixed` value by moving backward some number of minutes. - /// - Parameter minutes: The number of minutes by which to move backward. - public func subtracting(minutes: Int) -> Self { return applying(difference: .minutes(-minutes)) } - + + /// Create a new `Fixed` value by moving forward one minute. + public var nextMinute: Self { return adding(minutes: 1) } + + /// Create a new `Fixed` value by moving backward one minute. + public var previousMinute: Self { return subtracting(minutes: 1) } + + /// Create a new `Fixed` value by moving forward some number of minutes. + /// - Parameter minutes: The number of minutes by which to move forward. + public func adding(minutes: Int) -> Self { return applying(difference: .minutes(minutes)) } + + /// Create a new `Fixed` value by moving backward some number of minutes. + /// - Parameter minutes: The number of minutes by which to move backward. + public func subtracting(minutes: Int) -> Self { return applying(difference: .minutes(-minutes)) } + } extension Fixed where Granularity: LTOESecond { - - /// Create a new `Fixed` value by moving forward one second. - public var nextSecond: Self { return adding(seconds: 1) } - - /// Create a new `Fixed` value by moving backward one second. - public var previousSecond: Self { return subtracting(seconds: 1) } - - /// Create a new `Fixed` value by moving forward some number of seconds. - /// - Parameter seconds: The number of seconds by which to move forward. - public func adding(seconds: Int) -> Self { return applying(difference: .seconds(seconds)) } - - /// Create a new `Fixed` value by moving backward some number of seconds. - /// - Parameter seconds: The number of seconds by which to move backward. - public func subtracting(seconds: Int) -> Self { return applying(difference: .seconds(-seconds)) } - + + /// Create a new `Fixed` value by moving forward one second. + public var nextSecond: Self { return adding(seconds: 1) } + + /// Create a new `Fixed` value by moving backward one second. + public var previousSecond: Self { return subtracting(seconds: 1) } + + /// Create a new `Fixed` value by moving forward some number of seconds. + /// - Parameter seconds: The number of seconds by which to move forward. + public func adding(seconds: Int) -> Self { return applying(difference: .seconds(seconds)) } + + /// Create a new `Fixed` value by moving backward some number of seconds. + /// - Parameter seconds: The number of seconds by which to move backward. + public func subtracting(seconds: Int) -> Self { return applying(difference: .seconds(-seconds)) } + } extension Fixed where Granularity: LTOENanosecond { - - /// Create a new `Fixed` value by moving forward one nanosecond. - public var nextNanosecond: Self { return adding(nanoseconds: 1) } - - /// Create a new `Fixed` value by moving backward one nanosecond. - public var previousNanosecond: Self { return subtracting(nanoseconds: 1) } - - /// Create a new `Fixed` value by moving forward some number of nanoseconds. - /// - Parameter nanoseconds: The number of nanoseconds by which to move forward. - public func adding(nanoseconds: Int) -> Self { return applying(difference: .nanoseconds(nanoseconds)) } - - /// Create a new `Fixed` value by moving backward some number of nanoseconds. - /// - Parameter nanoseconds: The number of nanoseconds by which to move backward. - public func subtracting(nanoseconds: Int) -> Self { return applying(difference: .nanoseconds(-nanoseconds)) } - + + /// Create a new `Fixed` value by moving forward one nanosecond. + public var nextNanosecond: Self { return adding(nanoseconds: 1) } + + /// Create a new `Fixed` value by moving backward one nanosecond. + public var previousNanosecond: Self { return subtracting(nanoseconds: 1) } + + /// Create a new `Fixed` value by moving forward some number of nanoseconds. + /// - Parameter nanoseconds: The number of nanoseconds by which to move forward. + public func adding(nanoseconds: Int) -> Self { + return applying(difference: .nanoseconds(nanoseconds)) + } + + /// Create a new `Fixed` value by moving backward some number of nanoseconds. + /// - Parameter nanoseconds: The number of nanoseconds by which to move backward. + public func subtracting(nanoseconds: Int) -> Self { + return applying(difference: .nanoseconds(-nanoseconds)) + } + } diff --git a/Sources/Time/6-Adjustments/Fixed+UnsafeAdjustment.swift b/Sources/Time/6-Adjustments/Fixed+UnsafeAdjustment.swift index ee1f481..344bf46 100644 --- a/Sources/Time/6-Adjustments/Fixed+UnsafeAdjustment.swift +++ b/Sources/Time/6-Adjustments/Fixed+UnsafeAdjustment.swift @@ -1,562 +1,663 @@ import Foundation extension Fixed where Granularity: LTOEYear { - - /// Create a new fixed value by setting the `.year` - /// - /// This method preserves all other components (`.month`, `.day`, etc). - /// - /// - Parameter year: The numeric year - /// - Returns: A new fixed value with the specified year. - /// - Throws: Throws a ``TimeError`` if setting the year would result in a non-existent date. For example, - /// `try aFixedDay.setting(year: -1)` would throw an error, because no supported calendar produces negative-numbered years. - /// As another example, `try february29.setting(year: 2023)` would throw an error, because 2023 on the gregorian calendar - /// was not a leap year, and February 29th did not exist that year. - public func setting(year: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(year: year)) - } - + + /// Create a new fixed value by setting the `.year` + /// + /// This method preserves all other components (`.month`, `.day`, etc). + /// + /// - Parameter year: The numeric year + /// - Returns: A new fixed value with the specified year. + /// - Throws: Throws a ``TimeError`` if setting the year would result in a non-existent date. For example, + /// `try aFixedDay.setting(year: -1)` would throw an error, because no supported calendar produces negative-numbered years. + /// As another example, `try february29.setting(year: 2023)` would throw an error, because 2023 on the gregorian calendar + /// was not a leap year, and February 29th did not exist that year. + public func setting(year: Int) throws -> Self { + return try Self(region: region, strictDateComponents: dateComponents.setting(year: year)) + } + } extension Fixed where Granularity: LTOEMonth { - - /// Create a new fixed value by setting its year and month - /// - Parameters: - /// - year: The new year value - /// - month: The new month value - /// - Returns: A new fixed value with the specified year and month - /// - Throws: Throws a ``TimeError`` if setting the year and month would result in a non-existent date. - public func setting(year: Int, month: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(year: year, month: month)) - } - - /// Create a new fixed value by setting its month - /// - Parameters: - /// - month: The new month value - /// - Returns: A new fixed value with the specified month - /// - Throws: Throws a ``TimeError`` if setting the month would result in a non-existent date. - public func setting(month: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(month: month)) - } - + + /// Create a new fixed value by setting its year and month + /// - Parameters: + /// - year: The new year value + /// - month: The new month value + /// - Returns: A new fixed value with the specified year and month + /// - Throws: Throws a ``TimeError`` if setting the year and month would result in a non-existent date. + public func setting(year: Int, month: Int) throws -> Self { + return try Self( + region: region, strictDateComponents: dateComponents.setting(year: year, month: month)) + } + + /// Create a new fixed value by setting its month + /// - Parameters: + /// - month: The new month value + /// - Returns: A new fixed value with the specified month + /// - Throws: Throws a ``TimeError`` if setting the month would result in a non-existent date. + public func setting(month: Int) throws -> Self { + return try Self(region: region, strictDateComponents: dateComponents.setting(month: month)) + } + } extension Fixed where Granularity: LTOEDay { - - /// Create a new fixed value by setting its year, month, and day - /// - Parameters: - /// - year: The new year value - /// - month: The new month value - /// - day: The new day value - /// - Returns: A new fixed value with the specified year, month, and day - /// - Throws: Throws a ``TimeError`` if setting the year, month, and day would result in a non-existent date. - public func setting(year: Int, month: Int, day: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(year: year, month: month, day: day)) - } - - /// Create a new fixed value by setting its month and day - /// - Parameters: - /// - month: The new month value - /// - day: The new day value - /// - Returns: A new fixed value with the specified month and day - /// - Throws: Throws a ``TimeError`` if setting the month and day would result in a non-existent date. - public func setting(month: Int, day: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(month: month, day: day)) - } - - /// Create a new fixed value by setting its day - /// - Parameters: - /// - day: The new day value - /// - Returns: A new fixed value with the specified day - /// - Throws: Throws a ``TimeError`` if setting the day would result in a non-existent date. - public func setting(day: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(day: day)) - } - + + /// Create a new fixed value by setting its year, month, and day + /// - Parameters: + /// - year: The new year value + /// - month: The new month value + /// - day: The new day value + /// - Returns: A new fixed value with the specified year, month, and day + /// - Throws: Throws a ``TimeError`` if setting the year, month, and day would result in a non-existent date. + public func setting(year: Int, month: Int, day: Int) throws -> Self { + return try Self( + region: region, + strictDateComponents: dateComponents.setting(year: year, month: month, day: day)) + } + + /// Create a new fixed value by setting its month and day + /// - Parameters: + /// - month: The new month value + /// - day: The new day value + /// - Returns: A new fixed value with the specified month and day + /// - Throws: Throws a ``TimeError`` if setting the month and day would result in a non-existent date. + public func setting(month: Int, day: Int) throws -> Self { + return try Self( + region: region, strictDateComponents: dateComponents.setting(month: month, day: day)) + } + + /// Create a new fixed value by setting its day + /// - Parameters: + /// - day: The new day value + /// - Returns: A new fixed value with the specified day + /// - Throws: Throws a ``TimeError`` if setting the day would result in a non-existent date. + public func setting(day: Int) throws -> Self { + return try Self(region: region, strictDateComponents: dateComponents.setting(day: day)) + } + } extension Fixed where Granularity: LTOEHour { - - /// Create a new fixed value by setting its year, month, day, and hour - /// - Parameters: - /// - year: The new year value - /// - month: The new month value - /// - day: The new day value - /// - hour: The new hour value - /// - Returns: A new fixed value with the specified year, month, day, and hour - /// - Throws: Throws a ``TimeError`` if setting the year, month, day, and hour would result in a non-existent date. - public func setting(year: Int, month: Int, day: Int, hour: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(year: year, month: month, day: day, hour: hour)) - } - - /// Create a new fixed value by setting its month, day, and hour - /// - Parameters: - /// - month: The new month value - /// - day: The new day value - /// - hour: The new hour value - /// - Returns: A new fixed value with the specified month, day, and hour - /// - Throws: Throws a ``TimeError`` if setting the month, day, and hour would result in a non-existent date. - public func setting(month: Int, day: Int, hour: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(month: month, day: day, hour: hour)) - } - - /// Create a new fixed value by setting its day and hour - /// - Parameters: - /// - day: The new day value - /// - hour: The new hour value - /// - Returns: A new fixed value with the specified day and hour - /// - Throws: Throws a ``TimeError`` if setting the day and hour would result in a non-existent date. - public func setting(day: Int, hour: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(day: day, hour: hour)) - } - - /// Create a new fixed value by setting its hour - /// - Parameters: - /// - hour: The new hour value - /// - Returns: A new fixed value with the specified hour - /// - Throws: Throws a ``TimeError`` if setting the hour would result in a non-existent date. - public func setting(hour: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(hour: hour)) - } - + + /// Create a new fixed value by setting its year, month, day, and hour + /// - Parameters: + /// - year: The new year value + /// - month: The new month value + /// - day: The new day value + /// - hour: The new hour value + /// - Returns: A new fixed value with the specified year, month, day, and hour + /// - Throws: Throws a ``TimeError`` if setting the year, month, day, and hour would result in a non-existent date. + public func setting(year: Int, month: Int, day: Int, hour: Int) throws -> Self { + return try Self( + region: region, + strictDateComponents: dateComponents.setting(year: year, month: month, day: day, hour: hour)) + } + + /// Create a new fixed value by setting its month, day, and hour + /// - Parameters: + /// - month: The new month value + /// - day: The new day value + /// - hour: The new hour value + /// - Returns: A new fixed value with the specified month, day, and hour + /// - Throws: Throws a ``TimeError`` if setting the month, day, and hour would result in a non-existent date. + public func setting(month: Int, day: Int, hour: Int) throws -> Self { + return try Self( + region: region, + strictDateComponents: dateComponents.setting(month: month, day: day, hour: hour)) + } + + /// Create a new fixed value by setting its day and hour + /// - Parameters: + /// - day: The new day value + /// - hour: The new hour value + /// - Returns: A new fixed value with the specified day and hour + /// - Throws: Throws a ``TimeError`` if setting the day and hour would result in a non-existent date. + public func setting(day: Int, hour: Int) throws -> Self { + return try Self( + region: region, strictDateComponents: dateComponents.setting(day: day, hour: hour)) + } + + /// Create a new fixed value by setting its hour + /// - Parameters: + /// - hour: The new hour value + /// - Returns: A new fixed value with the specified hour + /// - Throws: Throws a ``TimeError`` if setting the hour would result in a non-existent date. + public func setting(hour: Int) throws -> Self { + return try Self(region: region, strictDateComponents: dateComponents.setting(hour: hour)) + } + } extension Fixed where Granularity: LTOEMinute { - - /// Create a new fixed value by setting its year, month, day, hour, and minute - /// - Parameters: - /// - year: The new year value - /// - month: The new month value - /// - day: The new day value - /// - hour: The new hour value - /// - minute: The new minute value - /// - Returns: A new fixed value with the specified year, month, day, hour, and minute - /// - Throws: Throws a ``TimeError`` if setting the year, month, day, hour, and minute would result in a non-existent date. - public func setting(year: Int, month: Int, day: Int, hour: Int, minute: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(year: year, month: month, day: day, hour: hour, minute: minute)) - } - - /// Create a new fixed value by setting its month, day, hour, and minute - /// - Parameters: - /// - month: The new month value - /// - day: The new day value - /// - hour: The new hour value - /// - minute: The new minute value - /// - Returns: A new fixed value with the specified month, day, hour, and minute - /// - Throws: Throws a ``TimeError`` if setting the month, day, hour, and minute would result in a non-existent date. - public func setting(month: Int, day: Int, hour: Int, minute: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(month: month, day: day, hour: hour, minute: minute)) - } - - /// Create a new fixed value by setting its day, hour, and minute - /// - Parameters: - /// - day: The new day value - /// - hour: The new hour value - /// - minute: The new minute value - /// - Returns: A new fixed value with the specified day, hour, and minute - /// - Throws: Throws a ``TimeError`` if setting the day, hour, and minute would result in a non-existent date. - public func setting(day: Int, hour: Int, minute: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(day: day, hour: hour, minute: minute)) - } - - /// Create a new fixed value by setting its hour and minute - /// - Parameters: - /// - hour: The new hour value - /// - minute: The new minute value - /// - Returns: A new fixed value with the specified hour and minute - /// - Throws: Throws a ``TimeError`` if setting the hour and minute would result in a non-existent date. - public func setting(hour: Int, minute: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(hour: hour, minute: minute)) - } - - /// Create a new fixed value by setting its minute - /// - Parameters: - /// - minute: The new minute value - /// - Returns: A new fixed value with the specified minute - /// - Throws: Throws a ``TimeError`` if setting the minute would result in a non-existent date. - public func setting(minute: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(minute: minute)) - } - + + /// Create a new fixed value by setting its year, month, day, hour, and minute + /// - Parameters: + /// - year: The new year value + /// - month: The new month value + /// - day: The new day value + /// - hour: The new hour value + /// - minute: The new minute value + /// - Returns: A new fixed value with the specified year, month, day, hour, and minute + /// - Throws: Throws a ``TimeError`` if setting the year, month, day, hour, and minute would result in a non-existent date. + public func setting(year: Int, month: Int, day: Int, hour: Int, minute: Int) throws -> Self { + return try Self( + region: region, + strictDateComponents: dateComponents.setting( + year: year, month: month, day: day, hour: hour, minute: minute)) + } + + /// Create a new fixed value by setting its month, day, hour, and minute + /// - Parameters: + /// - month: The new month value + /// - day: The new day value + /// - hour: The new hour value + /// - minute: The new minute value + /// - Returns: A new fixed value with the specified month, day, hour, and minute + /// - Throws: Throws a ``TimeError`` if setting the month, day, hour, and minute would result in a non-existent date. + public func setting(month: Int, day: Int, hour: Int, minute: Int) throws -> Self { + return try Self( + region: region, + strictDateComponents: dateComponents.setting( + month: month, day: day, hour: hour, minute: minute)) + } + + /// Create a new fixed value by setting its day, hour, and minute + /// - Parameters: + /// - day: The new day value + /// - hour: The new hour value + /// - minute: The new minute value + /// - Returns: A new fixed value with the specified day, hour, and minute + /// - Throws: Throws a ``TimeError`` if setting the day, hour, and minute would result in a non-existent date. + public func setting(day: Int, hour: Int, minute: Int) throws -> Self { + return try Self( + region: region, + strictDateComponents: dateComponents.setting(day: day, hour: hour, minute: minute)) + } + + /// Create a new fixed value by setting its hour and minute + /// - Parameters: + /// - hour: The new hour value + /// - minute: The new minute value + /// - Returns: A new fixed value with the specified hour and minute + /// - Throws: Throws a ``TimeError`` if setting the hour and minute would result in a non-existent date. + public func setting(hour: Int, minute: Int) throws -> Self { + return try Self( + region: region, strictDateComponents: dateComponents.setting(hour: hour, minute: minute)) + } + + /// Create a new fixed value by setting its minute + /// - Parameters: + /// - minute: The new minute value + /// - Returns: A new fixed value with the specified minute + /// - Throws: Throws a ``TimeError`` if setting the minute would result in a non-existent date. + public func setting(minute: Int) throws -> Self { + return try Self(region: region, strictDateComponents: dateComponents.setting(minute: minute)) + } + } extension Fixed where Granularity: LTOESecond { - - /// Create a new fixed value by setting its year, month, day, hour, minute, and second - /// - Parameters: - /// - year: The new year value - /// - month: The new month value - /// - day: The new day value - /// - hour: The new hour value - /// - minute: The new minute value - /// - second: The new second value - /// - Returns: A new fixed value with the specified year, month, day, hour, minute, and second - /// - Throws: Throws a ``TimeError`` if setting the year, month, day, hour, minute, and second would result in a non-existent date. - public func setting(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(year: year, month: month, day: day, hour: hour, minute: minute, second: second)) - } - - /// Create a new fixed value by setting its month, day, hour, minute, and second - /// - Parameters: - /// - month: The new month value - /// - day: The new day value - /// - hour: The new hour value - /// - minute: The new minute value - /// - second: The new second value - /// - Returns: A new fixed value with the specified month, day, hour, minute, and second - /// - Throws: Throws a ``TimeError`` if setting the month, day, hour, minute, and second would result in a non-existent date. - public func setting(month: Int, day: Int, hour: Int, minute: Int, second: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(month: month, day: day, hour: hour, minute: minute, second: second)) - } - - /// Create a new fixed value by setting its day, hour, minute, and second - /// - Parameters: - /// - day: The new day value - /// - hour: The new hour value - /// - minute: The new minute value - /// - second: The new second value - /// - Returns: A new fixed value with the specified day, hour, minute, and second - /// - Throws: Throws a ``TimeError`` if setting the day, hour, minute, and second would result in a non-existent date. - public func setting(day: Int, hour: Int, minute: Int, second: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(day: day, hour: hour, minute: minute, second: second)) - } - - /// Create a new fixed value by setting its hour, minute, and second - /// - Parameters: - /// - hour: The new hour value - /// - minute: The new minute value - /// - second: The new second value - /// - Returns: A new fixed value with the specified hour, minute, and second - /// - Throws: Throws a ``TimeError`` if setting the hour, minute, and second would result in a non-existent date. - public func setting(hour: Int, minute: Int, second: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(hour: hour, minute: minute, second: second)) - } - - /// Create a new fixed value by setting its minute and second - /// - Parameters: - /// - minute: The new minute value - /// - second: The new second value - /// - Returns: A new fixed value with the specified minute and second - /// - Throws: Throws a ``TimeError`` if setting the minute and second would result in a non-existent date. - public func setting(minute: Int, second: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(minute: minute, second: second)) - } - - /// Create a new fixed value by setting its second - /// - Parameters: - /// - second: The new second value - /// - Returns: A new fixed value with the specified second - /// - Throws: Throws a ``TimeError`` if setting the second would result in a non-existent date. - public func setting(second: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(second: second)) - } - + + /// Create a new fixed value by setting its year, month, day, hour, minute, and second + /// - Parameters: + /// - year: The new year value + /// - month: The new month value + /// - day: The new day value + /// - hour: The new hour value + /// - minute: The new minute value + /// - second: The new second value + /// - Returns: A new fixed value with the specified year, month, day, hour, minute, and second + /// - Throws: Throws a ``TimeError`` if setting the year, month, day, hour, minute, and second would result in a non-existent date. + public func setting(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int) throws + -> Self + { + return try Self( + region: region, + strictDateComponents: dateComponents.setting( + year: year, month: month, day: day, hour: hour, minute: minute, second: second)) + } + + /// Create a new fixed value by setting its month, day, hour, minute, and second + /// - Parameters: + /// - month: The new month value + /// - day: The new day value + /// - hour: The new hour value + /// - minute: The new minute value + /// - second: The new second value + /// - Returns: A new fixed value with the specified month, day, hour, minute, and second + /// - Throws: Throws a ``TimeError`` if setting the month, day, hour, minute, and second would result in a non-existent date. + public func setting(month: Int, day: Int, hour: Int, minute: Int, second: Int) throws -> Self { + return try Self( + region: region, + strictDateComponents: dateComponents.setting( + month: month, day: day, hour: hour, minute: minute, second: second)) + } + + /// Create a new fixed value by setting its day, hour, minute, and second + /// - Parameters: + /// - day: The new day value + /// - hour: The new hour value + /// - minute: The new minute value + /// - second: The new second value + /// - Returns: A new fixed value with the specified day, hour, minute, and second + /// - Throws: Throws a ``TimeError`` if setting the day, hour, minute, and second would result in a non-existent date. + public func setting(day: Int, hour: Int, minute: Int, second: Int) throws -> Self { + return try Self( + region: region, + strictDateComponents: dateComponents.setting( + day: day, hour: hour, minute: minute, second: second)) + } + + /// Create a new fixed value by setting its hour, minute, and second + /// - Parameters: + /// - hour: The new hour value + /// - minute: The new minute value + /// - second: The new second value + /// - Returns: A new fixed value with the specified hour, minute, and second + /// - Throws: Throws a ``TimeError`` if setting the hour, minute, and second would result in a non-existent date. + public func setting(hour: Int, minute: Int, second: Int) throws -> Self { + return try Self( + region: region, + strictDateComponents: dateComponents.setting(hour: hour, minute: minute, second: second)) + } + + /// Create a new fixed value by setting its minute and second + /// - Parameters: + /// - minute: The new minute value + /// - second: The new second value + /// - Returns: A new fixed value with the specified minute and second + /// - Throws: Throws a ``TimeError`` if setting the minute and second would result in a non-existent date. + public func setting(minute: Int, second: Int) throws -> Self { + return try Self( + region: region, strictDateComponents: dateComponents.setting(minute: minute, second: second)) + } + + /// Create a new fixed value by setting its second + /// - Parameters: + /// - second: The new second value + /// - Returns: A new fixed value with the specified second + /// - Throws: Throws a ``TimeError`` if setting the second would result in a non-existent date. + public func setting(second: Int) throws -> Self { + return try Self(region: region, strictDateComponents: dateComponents.setting(second: second)) + } + } extension Fixed where Granularity: LTOENanosecond { - - /// Create a new fixed value by setting its year, month, day, hour, minute, second, and nanosecond - /// - Parameters: - /// - year: The new year value - /// - month: The new month value - /// - day: The new day value - /// - hour: The new hour value - /// - minute: The new minute value - /// - second: The new second value - /// - nanosecond: The new nanosecond value - /// - Returns: A new fixed value with the specified year, month, day, hour, minute, second, and nanosecond - /// - Throws: Throws a ``TimeError`` if setting the year, month, day, hour, minute, second, and nanosecond would result in a non-existent date. - public func setting(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(year: year, month: month, day: day, hour: hour, minute: minute, second: second, nanosecond: nanosecond)) - } - - /// Create a new fixed value by setting its month, day, hour, minute, second, and nanosecond - /// - Parameters: - /// - month: The new month value - /// - day: The new day value - /// - hour: The new hour value - /// - minute: The new minute value - /// - second: The new second value - /// - nanosecond: The new nanosecond value - /// - Returns: A new fixed value with the specified month, day, hour, minute, second, and nanosecond - /// - Throws: Throws a ``TimeError`` if setting the month, day, hour, minute, second, and nanosecond would result in a non-existent date. - public func setting(month: Int, day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(month: month, day: day, hour: hour, minute: minute, second: second, nanosecond: nanosecond)) - } - - /// Create a new fixed value by setting its day, hour, minute, second, and nanosecond - /// - Parameters: - /// - day: The new day value - /// - hour: The new hour value - /// - minute: The new minute value - /// - second: The new second value - /// - nanosecond: The new nanosecond value - /// - Returns: A new fixed value with the specified day, hour, minute, second, and nanosecond - /// - Throws: Throws a ``TimeError`` if setting the day, hour, minute, second, and nanosecond would result in a non-existent date. - public func setting(day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(day: day, hour: hour, minute: minute, second: second, nanosecond: nanosecond)) - } - - /// Create a new fixed value by setting its hour, minute, second, and nanosecond - /// - Parameters: - /// - hour: The new hour value - /// - minute: The new minute value - /// - second: The new second value - /// - nanosecond: The new nanosecond value - /// - Returns: A new fixed value with the specified hour, minute, second, and nanosecond - /// - Throws: Throws a ``TimeError`` if setting the hour, minute, second, and nanosecond would result in a non-existent date. - public func setting(hour: Int, minute: Int, second: Int, nanosecond: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(hour: hour, minute: minute, second: second, nanosecond: nanosecond)) - } - - /// Create a new fixed value by setting its minute, second, and nanosecond - /// - Parameters: - /// - minute: The new minute value - /// - second: The new second value - /// - nanosecond: The new nanosecond value - /// - Returns: A new fixed value with the specified minute, second, and nanosecond - /// - Throws: Throws a ``TimeError`` if setting the minute, second, and nanosecond would result in a non-existent date. - public func setting(minute: Int, second: Int, nanosecond: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(minute: minute, second: second, nanosecond: nanosecond)) - } - - /// Create a new fixed value by setting its second and nanosecond - /// - Parameters: - /// - second: The new second value - /// - nanosecond: The new nanosecond value - /// - Returns: A new fixed value with the specified second and nanosecond - /// - Throws: Throws a ``TimeError`` if setting the second and nanosecond would result in a non-existent date. - public func setting(second: Int, nanosecond: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(second: second, nanosecond: nanosecond)) - } - - /// Create a new fixed value by setting its nanosecond - /// - Parameters: - /// - nanosecond: The new nanosecond value - /// - Returns: A new fixed value with the specified nanosecond - /// - Throws: Throws a ``TimeError`` if setting the nanosecond would result in a non-existent date. - public func setting(nanosecond: Int) throws -> Self { - return try Self(region: region, strictDateComponents: dateComponents.setting(nanosecond: nanosecond)) - } + + /// Create a new fixed value by setting its year, month, day, hour, minute, second, and nanosecond + /// - Parameters: + /// - year: The new year value + /// - month: The new month value + /// - day: The new day value + /// - hour: The new hour value + /// - minute: The new minute value + /// - second: The new second value + /// - nanosecond: The new nanosecond value + /// - Returns: A new fixed value with the specified year, month, day, hour, minute, second, and nanosecond + /// - Throws: Throws a ``TimeError`` if setting the year, month, day, hour, minute, second, and nanosecond would result in a non-existent date. + public func setting( + year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int + ) throws -> Self { + return try Self( + region: region, + strictDateComponents: dateComponents.setting( + year: year, month: month, day: day, hour: hour, minute: minute, second: second, + nanosecond: nanosecond)) + } + + /// Create a new fixed value by setting its month, day, hour, minute, second, and nanosecond + /// - Parameters: + /// - month: The new month value + /// - day: The new day value + /// - hour: The new hour value + /// - minute: The new minute value + /// - second: The new second value + /// - nanosecond: The new nanosecond value + /// - Returns: A new fixed value with the specified month, day, hour, minute, second, and nanosecond + /// - Throws: Throws a ``TimeError`` if setting the month, day, hour, minute, second, and nanosecond would result in a non-existent date. + public func setting(month: Int, day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) + throws -> Self + { + return try Self( + region: region, + strictDateComponents: dateComponents.setting( + month: month, day: day, hour: hour, minute: minute, second: second, nanosecond: nanosecond)) + } + + /// Create a new fixed value by setting its day, hour, minute, second, and nanosecond + /// - Parameters: + /// - day: The new day value + /// - hour: The new hour value + /// - minute: The new minute value + /// - second: The new second value + /// - nanosecond: The new nanosecond value + /// - Returns: A new fixed value with the specified day, hour, minute, second, and nanosecond + /// - Throws: Throws a ``TimeError`` if setting the day, hour, minute, second, and nanosecond would result in a non-existent date. + public func setting(day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) throws -> Self + { + return try Self( + region: region, + strictDateComponents: dateComponents.setting( + day: day, hour: hour, minute: minute, second: second, nanosecond: nanosecond)) + } + + /// Create a new fixed value by setting its hour, minute, second, and nanosecond + /// - Parameters: + /// - hour: The new hour value + /// - minute: The new minute value + /// - second: The new second value + /// - nanosecond: The new nanosecond value + /// - Returns: A new fixed value with the specified hour, minute, second, and nanosecond + /// - Throws: Throws a ``TimeError`` if setting the hour, minute, second, and nanosecond would result in a non-existent date. + public func setting(hour: Int, minute: Int, second: Int, nanosecond: Int) throws -> Self { + return try Self( + region: region, + strictDateComponents: dateComponents.setting( + hour: hour, minute: minute, second: second, nanosecond: nanosecond)) + } + + /// Create a new fixed value by setting its minute, second, and nanosecond + /// - Parameters: + /// - minute: The new minute value + /// - second: The new second value + /// - nanosecond: The new nanosecond value + /// - Returns: A new fixed value with the specified minute, second, and nanosecond + /// - Throws: Throws a ``TimeError`` if setting the minute, second, and nanosecond would result in a non-existent date. + public func setting(minute: Int, second: Int, nanosecond: Int) throws -> Self { + return try Self( + region: region, + strictDateComponents: dateComponents.setting( + minute: minute, second: second, nanosecond: nanosecond)) + } + + /// Create a new fixed value by setting its second and nanosecond + /// - Parameters: + /// - second: The new second value + /// - nanosecond: The new nanosecond value + /// - Returns: A new fixed value with the specified second and nanosecond + /// - Throws: Throws a ``TimeError`` if setting the second and nanosecond would result in a non-existent date. + public func setting(second: Int, nanosecond: Int) throws -> Self { + return try Self( + region: region, + strictDateComponents: dateComponents.setting(second: second, nanosecond: nanosecond)) + } + + /// Create a new fixed value by setting its nanosecond + /// - Parameters: + /// - nanosecond: The new nanosecond value + /// - Returns: A new fixed value with the specified nanosecond + /// - Throws: Throws a ``TimeError`` if setting the nanosecond would result in a non-existent date. + public func setting(nanosecond: Int) throws -> Self { + return try Self( + region: region, strictDateComponents: dateComponents.setting(nanosecond: nanosecond)) + } } extension Fixed where Granularity == Year { - - /// Create a `Fixed` from a `Fixed` by specifying the month - /// - Parameter month: The new month value - /// - Returns: A new `Fixed` with the specified month value - /// - Throws: Throws a ``TimeError`` if the specified month value would result in a non-existent date. - public func setting(month: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(month: month)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the month and day - /// - Parameter month: The new month value - /// - Parameter day: The new day value - /// - Returns: A new `Fixed` with the specified month and day values - /// - Throws: Throws a ``TimeError`` if the specified month and day values would result in a non-existent date. - public func setting(month: Int, day: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(month: month, day: day)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the month, day, and hour - /// - Parameter month: The new month value - /// - Parameter day: The new day value - /// - Parameter hour: The new hour value - /// - Returns: A new `Fixed` with the specified month, day, and hour values - /// - Throws: Throws a ``TimeError`` if the specified month, day, and hour values would result in a non-existent date. - public func setting(month: Int, day: Int, hour: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(month: month, day: day, hour: hour)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the month, day, hour, and minute - /// - Parameter month: The new month value - /// - Parameter day: The new day value - /// - Parameter hour: The new hour value - /// - Parameter minute: The new minute value - /// - Returns: A new `Fixed` with the specified month, day, hour, and minute values - /// - Throws: Throws a ``TimeError`` if the specified month, day, hour, and minute values would result in a non-existent date. - public func setting(month: Int, day: Int, hour: Int, minute: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(month: month, day: day, hour: hour, minute: minute)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the month, day, hour, minute, and second - /// - Parameter month: The new month value - /// - Parameter day: The new day value - /// - Parameter hour: The new hour value - /// - Parameter minute: The new minute value - /// - Parameter second: The new second value - /// - Returns: A new `Fixed` with the specified month, day, hour, minute, and second values - /// - Throws: Throws a ``TimeError`` if the specified month, day, hour, minute, and second values would result in a non-existent date. - public func setting(month: Int, day: Int, hour: Int, minute: Int, second: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(month: month, day: day, hour: hour, minute: minute, second: second)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the month, day, hour, minute, second, and nanosecond - /// - Parameter month: The new month value - /// - Parameter day: The new day value - /// - Parameter hour: The new hour value - /// - Parameter minute: The new minute value - /// - Parameter second: The new second value - /// - Parameter nanosecond: The new nanosecond value - /// - Returns: A new `Fixed` with the specified month, day, hour, minute, second, and nanosecond values - /// - Throws: Throws a ``TimeError`` if the specified month, day, hour, minute, second, and nanosecond values would result in a non-existent date. - public func setting(month: Int, day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(month: month, day: day, hour: hour, minute: minute, second: second, nanosecond: nanosecond)) - } + + /// Create a `Fixed` from a `Fixed` by specifying the month + /// - Parameter month: The new month value + /// - Returns: A new `Fixed` with the specified month value + /// - Throws: Throws a ``TimeError`` if the specified month value would result in a non-existent date. + public func setting(month: Int) throws -> Fixed { + return try Fixed( + region: region, strictDateComponents: dateComponents.setting(month: month)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the month and day + /// - Parameter month: The new month value + /// - Parameter day: The new day value + /// - Returns: A new `Fixed` with the specified month and day values + /// - Throws: Throws a ``TimeError`` if the specified month and day values would result in a non-existent date. + public func setting(month: Int, day: Int) throws -> Fixed { + return try Fixed( + region: region, strictDateComponents: dateComponents.setting(month: month, day: day)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the month, day, and hour + /// - Parameter month: The new month value + /// - Parameter day: The new day value + /// - Parameter hour: The new hour value + /// - Returns: A new `Fixed` with the specified month, day, and hour values + /// - Throws: Throws a ``TimeError`` if the specified month, day, and hour values would result in a non-existent date. + public func setting(month: Int, day: Int, hour: Int) throws -> Fixed { + return try Fixed( + region: region, + strictDateComponents: dateComponents.setting(month: month, day: day, hour: hour)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the month, day, hour, and minute + /// - Parameter month: The new month value + /// - Parameter day: The new day value + /// - Parameter hour: The new hour value + /// - Parameter minute: The new minute value + /// - Returns: A new `Fixed` with the specified month, day, hour, and minute values + /// - Throws: Throws a ``TimeError`` if the specified month, day, hour, and minute values would result in a non-existent date. + public func setting(month: Int, day: Int, hour: Int, minute: Int) throws -> Fixed { + return try Fixed( + region: region, + strictDateComponents: dateComponents.setting( + month: month, day: day, hour: hour, minute: minute)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the month, day, hour, minute, and second + /// - Parameter month: The new month value + /// - Parameter day: The new day value + /// - Parameter hour: The new hour value + /// - Parameter minute: The new minute value + /// - Parameter second: The new second value + /// - Returns: A new `Fixed` with the specified month, day, hour, minute, and second values + /// - Throws: Throws a ``TimeError`` if the specified month, day, hour, minute, and second values would result in a non-existent date. + public func setting(month: Int, day: Int, hour: Int, minute: Int, second: Int) throws -> Fixed< + Second + > { + return try Fixed( + region: region, + strictDateComponents: dateComponents.setting( + month: month, day: day, hour: hour, minute: minute, second: second)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the month, day, hour, minute, second, and nanosecond + /// - Parameter month: The new month value + /// - Parameter day: The new day value + /// - Parameter hour: The new hour value + /// - Parameter minute: The new minute value + /// - Parameter second: The new second value + /// - Parameter nanosecond: The new nanosecond value + /// - Returns: A new `Fixed` with the specified month, day, hour, minute, second, and nanosecond values + /// - Throws: Throws a ``TimeError`` if the specified month, day, hour, minute, second, and nanosecond values would result in a non-existent date. + public func setting(month: Int, day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) + throws -> Fixed + { + return try Fixed( + region: region, + strictDateComponents: dateComponents.setting( + month: month, day: day, hour: hour, minute: minute, second: second, nanosecond: nanosecond)) + } } extension Fixed where Granularity == Month { - - /// Create a `Fixed` from a `Fixed` by specifying the day - /// - Parameter day: The new day value - /// - Returns: A new `Fixed` with the specified day value - /// - Throws: Throws a ``TimeError`` if the specified day value would result in a non-existent date. - public func setting(day: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(day: day)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the day and hour - /// - Parameter day: The new day value - /// - Parameter hour: The new hour value - /// - Returns: A new `Fixed` with the specified day and hour values - /// - Throws: Throws a ``TimeError`` if the specified day and hour values would result in a non-existent date. - public func setting(day: Int, hour: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(day: day, hour: hour)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the day, hour, and minute - /// - Parameter day: The new day value - /// - Parameter hour: The new hour value - /// - Parameter minute: The new minute value - /// - Returns: A new `Fixed` with the specified day, hour, and minute values - /// - Throws: Throws a ``TimeError`` if the specified day, hour, and minute values would result in a non-existent date. - public func setting(day: Int, hour: Int, minute: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(day: day, hour: hour, minute: minute)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the day, hour, minute, and second - /// - Parameter day: The new day value - /// - Parameter hour: The new hour value - /// - Parameter minute: The new minute value - /// - Parameter second: The new second value - /// - Returns: A new `Fixed` with the specified day, hour, minute, and second values - /// - Throws: Throws a ``TimeError`` if the specified day, hour, minute, and second values would result in a non-existent date. - public func setting(day: Int, hour: Int, minute: Int, second: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(day: day, hour: hour, minute: minute, second: second)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the day, hour, minute, second, and nanosecond - /// - Parameter day: The new day value - /// - Parameter hour: The new hour value - /// - Parameter minute: The new minute value - /// - Parameter second: The new second value - /// - Parameter nanosecond: The new nanosecond value - /// - Returns: A new `Fixed` with the specified day, hour, minute, second, and nanosecond values - /// - Throws: Throws a ``TimeError`` if the specified day, hour, minute, second, and nanosecond values would result in a non-existent date. - public func setting(day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(day: day, hour: hour, minute: minute, second: second, nanosecond: nanosecond)) - } + + /// Create a `Fixed` from a `Fixed` by specifying the day + /// - Parameter day: The new day value + /// - Returns: A new `Fixed` with the specified day value + /// - Throws: Throws a ``TimeError`` if the specified day value would result in a non-existent date. + public func setting(day: Int) throws -> Fixed { + return try Fixed(region: region, strictDateComponents: dateComponents.setting(day: day)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the day and hour + /// - Parameter day: The new day value + /// - Parameter hour: The new hour value + /// - Returns: A new `Fixed` with the specified day and hour values + /// - Throws: Throws a ``TimeError`` if the specified day and hour values would result in a non-existent date. + public func setting(day: Int, hour: Int) throws -> Fixed { + return try Fixed( + region: region, strictDateComponents: dateComponents.setting(day: day, hour: hour)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the day, hour, and minute + /// - Parameter day: The new day value + /// - Parameter hour: The new hour value + /// - Parameter minute: The new minute value + /// - Returns: A new `Fixed` with the specified day, hour, and minute values + /// - Throws: Throws a ``TimeError`` if the specified day, hour, and minute values would result in a non-existent date. + public func setting(day: Int, hour: Int, minute: Int) throws -> Fixed { + return try Fixed( + region: region, + strictDateComponents: dateComponents.setting(day: day, hour: hour, minute: minute)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the day, hour, minute, and second + /// - Parameter day: The new day value + /// - Parameter hour: The new hour value + /// - Parameter minute: The new minute value + /// - Parameter second: The new second value + /// - Returns: A new `Fixed` with the specified day, hour, minute, and second values + /// - Throws: Throws a ``TimeError`` if the specified day, hour, minute, and second values would result in a non-existent date. + public func setting(day: Int, hour: Int, minute: Int, second: Int) throws -> Fixed { + return try Fixed( + region: region, + strictDateComponents: dateComponents.setting( + day: day, hour: hour, minute: minute, second: second)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the day, hour, minute, second, and nanosecond + /// - Parameter day: The new day value + /// - Parameter hour: The new hour value + /// - Parameter minute: The new minute value + /// - Parameter second: The new second value + /// - Parameter nanosecond: The new nanosecond value + /// - Returns: A new `Fixed` with the specified day, hour, minute, second, and nanosecond values + /// - Throws: Throws a ``TimeError`` if the specified day, hour, minute, second, and nanosecond values would result in a non-existent date. + public func setting(day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) throws + -> Fixed + { + return try Fixed( + region: region, + strictDateComponents: dateComponents.setting( + day: day, hour: hour, minute: minute, second: second, nanosecond: nanosecond)) + } } extension Fixed where Granularity == Day { - - /// Create a `Fixed` from a `Fixed` by specifying the hour - /// - Parameter hour: The new hour value - /// - Returns: A new `Fixed` with the specified hour value - /// - Throws: Throws a ``TimeError`` if the specified hour value would result in a non-existent date. - public func setting(hour: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(hour: hour)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the hour and minute - /// - Parameter hour: The new hour value - /// - Parameter minute: The new minute value - /// - Returns: A new `Fixed` with the specified hour and minute values - /// - Throws: Throws a ``TimeError`` if the specified hour and minute values would result in a non-existent date. - public func setting(hour: Int, minute: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(hour: hour, minute: minute)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the hour, minute, and second - /// - Parameter hour: The new hour value - /// - Parameter minute: The new minute value - /// - Parameter second: The new second value - /// - Returns: A new `Fixed` with the specified hour, minute, and second values - /// - Throws: Throws a ``TimeError`` if the specified hour, minute, and second values would result in a non-existent date. - public func setting(hour: Int, minute: Int, second: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(hour: hour, minute: minute, second: second)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the hour, minute, second, and nanosecond - /// - Parameter hour: The new hour value - /// - Parameter minute: The new minute value - /// - Parameter second: The new second value - /// - Parameter nanosecond: The new nanosecond value - /// - Returns: A new `Fixed` with the specified hour, minute, second, and nanosecond values - /// - Throws: Throws a ``TimeError`` if the specified hour, minute, second, and nanosecond values would result in a non-existent date. - public func setting(hour: Int, minute: Int, second: Int, nanosecond: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(hour: hour, minute: minute, second: second, nanosecond: nanosecond)) - } + + /// Create a `Fixed` from a `Fixed` by specifying the hour + /// - Parameter hour: The new hour value + /// - Returns: A new `Fixed` with the specified hour value + /// - Throws: Throws a ``TimeError`` if the specified hour value would result in a non-existent date. + public func setting(hour: Int) throws -> Fixed { + return try Fixed(region: region, strictDateComponents: dateComponents.setting(hour: hour)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the hour and minute + /// - Parameter hour: The new hour value + /// - Parameter minute: The new minute value + /// - Returns: A new `Fixed` with the specified hour and minute values + /// - Throws: Throws a ``TimeError`` if the specified hour and minute values would result in a non-existent date. + public func setting(hour: Int, minute: Int) throws -> Fixed { + return try Fixed( + region: region, strictDateComponents: dateComponents.setting(hour: hour, minute: minute)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the hour, minute, and second + /// - Parameter hour: The new hour value + /// - Parameter minute: The new minute value + /// - Parameter second: The new second value + /// - Returns: A new `Fixed` with the specified hour, minute, and second values + /// - Throws: Throws a ``TimeError`` if the specified hour, minute, and second values would result in a non-existent date. + public func setting(hour: Int, minute: Int, second: Int) throws -> Fixed { + return try Fixed( + region: region, + strictDateComponents: dateComponents.setting(hour: hour, minute: minute, second: second)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the hour, minute, second, and nanosecond + /// - Parameter hour: The new hour value + /// - Parameter minute: The new minute value + /// - Parameter second: The new second value + /// - Parameter nanosecond: The new nanosecond value + /// - Returns: A new `Fixed` with the specified hour, minute, second, and nanosecond values + /// - Throws: Throws a ``TimeError`` if the specified hour, minute, second, and nanosecond values would result in a non-existent date. + public func setting(hour: Int, minute: Int, second: Int, nanosecond: Int) throws -> Fixed< + Nanosecond + > { + return try Fixed( + region: region, + strictDateComponents: dateComponents.setting( + hour: hour, minute: minute, second: second, nanosecond: nanosecond)) + } } extension Fixed where Granularity == Hour { - - /// Create a `Fixed` from a `Fixed` by specifying the minute - /// - Parameter minute: The new minute value - /// - Returns: A new `Fixed` with the specified minute value - /// - Throws: Throws a ``TimeError`` if the specified minute value would result in a non-existent date. - public func setting(minute: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(minute: minute)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the minute and second - /// - Parameter minute: The new minute value - /// - Parameter second: The new second value - /// - Returns: A new `Fixed` with the specified minute and second values - /// - Throws: Throws a ``TimeError`` if the specified minute and second values would result in a non-existent date. - public func setting(minute: Int, second: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(minute: minute, second: second)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the minute, second, and nanosecond - /// - Parameter minute: The new minute value - /// - Parameter second: The new second value - /// - Parameter nanosecond: The new nanosecond value - /// - Returns: A new `Fixed` with the specified minute, second, and nanosecond values - /// - Throws: Throws a ``TimeError`` if the specified minute, second, and nanosecond values would result in a non-existent date. - public func setting(minute: Int, second: Int, nanosecond: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(minute: minute, second: second, nanosecond: nanosecond)) - } + + /// Create a `Fixed` from a `Fixed` by specifying the minute + /// - Parameter minute: The new minute value + /// - Returns: A new `Fixed` with the specified minute value + /// - Throws: Throws a ``TimeError`` if the specified minute value would result in a non-existent date. + public func setting(minute: Int) throws -> Fixed { + return try Fixed( + region: region, strictDateComponents: dateComponents.setting(minute: minute)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the minute and second + /// - Parameter minute: The new minute value + /// - Parameter second: The new second value + /// - Returns: A new `Fixed` with the specified minute and second values + /// - Throws: Throws a ``TimeError`` if the specified minute and second values would result in a non-existent date. + public func setting(minute: Int, second: Int) throws -> Fixed { + return try Fixed( + region: region, strictDateComponents: dateComponents.setting(minute: minute, second: second)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the minute, second, and nanosecond + /// - Parameter minute: The new minute value + /// - Parameter second: The new second value + /// - Parameter nanosecond: The new nanosecond value + /// - Returns: A new `Fixed` with the specified minute, second, and nanosecond values + /// - Throws: Throws a ``TimeError`` if the specified minute, second, and nanosecond values would result in a non-existent date. + public func setting(minute: Int, second: Int, nanosecond: Int) throws -> Fixed { + return try Fixed( + region: region, + strictDateComponents: dateComponents.setting( + minute: minute, second: second, nanosecond: nanosecond)) + } } extension Fixed where Granularity == Minute { - - /// Create a `Fixed` from a `Fixed` by specifying the second - /// - Parameter second: The new second value - /// - Returns: A new `Fixed` with the specified second value - /// - Throws: Throws a ``TimeError`` if the specified second value would result in a non-existent date. - public func setting(second: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(second: second)) - } - - /// Create a `Fixed` from a `Fixed` by specifying the second and nanosecond - /// - Parameter second: The new second value - /// - Parameter nanosecond: The new nanosecond value - /// - Returns: A new `Fixed` with the specified second and nanosecond values - /// - Throws: Throws a ``TimeError`` if the specified second and nanosecond values would result in a non-existent date. - public func setting(second: Int, nanosecond: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(second: second, nanosecond: nanosecond)) - } + + /// Create a `Fixed` from a `Fixed` by specifying the second + /// - Parameter second: The new second value + /// - Returns: A new `Fixed` with the specified second value + /// - Throws: Throws a ``TimeError`` if the specified second value would result in a non-existent date. + public func setting(second: Int) throws -> Fixed { + return try Fixed( + region: region, strictDateComponents: dateComponents.setting(second: second)) + } + + /// Create a `Fixed` from a `Fixed` by specifying the second and nanosecond + /// - Parameter second: The new second value + /// - Parameter nanosecond: The new nanosecond value + /// - Returns: A new `Fixed` with the specified second and nanosecond values + /// - Throws: Throws a ``TimeError`` if the specified second and nanosecond values would result in a non-existent date. + public func setting(second: Int, nanosecond: Int) throws -> Fixed { + return try Fixed( + region: region, + strictDateComponents: dateComponents.setting(second: second, nanosecond: nanosecond)) + } } extension Fixed where Granularity == Second { - - /// Create a `Fixed` from a `Fixed` by specifying the nanosecond - /// - Parameter nanosecond: The new nanosecond value - /// - Returns: A new `Fixed` with the specified nanosecond value - /// - Throws: Throws a ``TimeError`` if the specified nanosecond value would result in a non-existent date. - public func setting(nanosecond: Int) throws -> Fixed { - return try Fixed(region: region, strictDateComponents: dateComponents.setting(nanosecond: nanosecond)) - } + + /// Create a `Fixed` from a `Fixed` by specifying the nanosecond + /// - Parameter nanosecond: The new nanosecond value + /// - Returns: A new `Fixed` with the specified nanosecond value + /// - Throws: Throws a ``TimeError`` if the specified nanosecond value would result in a non-existent date. + public func setting(nanosecond: Int) throws -> Fixed { + return try Fixed( + region: region, strictDateComponents: dateComponents.setting(nanosecond: nanosecond)) + } } diff --git a/Sources/Time/7-Sequences/Fixed+BoundaryAlignedSequence.swift b/Sources/Time/7-Sequences/Fixed+BoundaryAlignedSequence.swift index 5f9c5c9..c19923a 100644 --- a/Sources/Time/7-Sequences/Fixed+BoundaryAlignedSequence.swift +++ b/Sources/Time/7-Sequences/Fixed+BoundaryAlignedSequence.swift @@ -1,6 +1,5 @@ import Foundation - /// Boundary-aligned sequences reset to a boundary while striding. /// /// For example, a sequence that strides 15 minutes will naturally fall on the hour boundary while striding: @@ -12,57 +11,64 @@ import Foundation /// Humans typically expect that iterating by 13 minutes would likely reset to the boundary when it is crossed. This sequence does that. /// :00 → :13 → :26 → :39 → :52 → :00 → :13 → :26 → … internal struct BoundaryAlignedSequence: Sequence { - - private let constructor: () -> BoundaryAlignedIterator - - init(start: Fixed, stride: TimeDifference, boundaryStride: TimeDifference) { - - constructor = { - return BoundaryAlignedIterator(start: start, - stride: stride, - boundaryStride: boundaryStride) - } - - } - - func makeIterator() -> BoundaryAlignedIterator { - return constructor() + + private let constructor: () -> BoundaryAlignedIterator + + init( + start: Fixed, stride: TimeDifference, + boundaryStride: TimeDifference + ) { + + constructor = { + return BoundaryAlignedIterator( + start: start, + stride: stride, + boundaryStride: boundaryStride) } - + + } + + func makeIterator() -> BoundaryAlignedIterator { + return constructor() + } + } internal struct BoundaryAlignedIterator: IteratorProtocol { - - var scale = 0 - - var currentBoundary: Range> - - let stride: TimeDifference - let boundaryStride: TimeDifference - - init(start: Fixed, stride: TimeDifference, boundaryStride: TimeDifference) { - - self.currentBoundary = start ..< start + boundaryStride - self.stride = stride - self.boundaryStride = boundaryStride - - } - - mutating func next() -> Fixed? { - let offset = stride.scale(by: scale) - let next = currentBoundary.lowerBound + offset - - if next < currentBoundary.upperBound { - scale += 1 - return next - } else { - scale = 1 // the next time we loop, we want to add 1 - - let next = currentBoundary.upperBound - currentBoundary = next ..< next + boundaryStride - - return next - } + + var scale = 0 + + var currentBoundary: Range> + + let stride: TimeDifference + let boundaryStride: TimeDifference + + init( + start: Fixed, stride: TimeDifference, + boundaryStride: TimeDifference + ) { + + self.currentBoundary = start.. Fixed? { + let offset = stride.scale(by: scale) + let next = currentBoundary.lowerBound + offset + + if next < currentBoundary.upperBound { + scale += 1 + return next + } else { + scale = 1 // the next time we loop, we want to add 1 + + let next = currentBoundary.upperBound + currentBoundary = next..: Sequence { - - private let constructor: () -> FixedIterator - - /// Construct an infinite sequence of fixed values starting from a specific value. - /// - Parameters: - /// - start: The starting fixed value. - /// - stride: The difference between subsequent calendar values. - public init(start: Fixed, stride: TimeDifference) { - constructor = { FixedIterator(start: start, stride: stride, keepGoing: { _ in return true })} - } - - /// Construct a sequence of fixed values starting from a specific value. - /// - Parameters: - /// - start: The starting fixed value. - /// - stride: The difference between subsequent fixed values. - /// - keepGoing: A closure that is invoked to indicate whether the sequence should continue. This closure is invoked *before* the next value is generated. - public init(start: Fixed, stride: TimeDifference, while keepGoing: @escaping (Fixed) -> Bool) { - constructor = { FixedIterator(start: start, stride: stride, keepGoing: keepGoing)} - } - - /// Construct a sequence of fixed values that iterates through a definite range of values. - /// - /// - Note: This sequence iterates through values *up to but not including* the upper bound of the range. - /// - Parameters: - /// - range: The `Range` of fixed values to iterate through. - /// - stride: The difference between subsequent fixed values. - public init(range: Range>, stride: TimeDifference) { - let lower = range.lowerBound - let upper = range.upperBound.region.isEquivalent(to: lower.region) ? range.upperBound : Fixed(region: lower.region, instant: range.upperBound.firstInstant) - constructor = { FixedIterator(region: lower.region, range: lower.firstInstant ..< upper.firstInstant, stride: stride) } - } - - /// Construct a sequence of fixed values that iterates through a closed range of values. - /// - /// - Note: This sequence iterates through values *up to and including* the upper bound of the range. - /// - Parameters: - /// - range: The `ClosedRange` of fixed values to iterate through. - /// - stride: The difference between subsequent fixed values. - public init(range: ClosedRange>, stride: TimeDifference) { - let lower = range.lowerBound - let upper = range.upperBound.region.isEquivalent(to: lower.region) ? range.upperBound : Fixed(region: lower.region, instant: range.upperBound.firstInstant) - constructor = { FixedIterator(region: lower.region, range: lower.firstInstant ... upper.firstInstant, stride: stride) } - } - - internal init(parent: Fixed, stride: TimeDifference = TimeDifference(value: 1, unit: U.component)) { - constructor = { FixedIterator(region: parent.region, range: parent.range, stride: stride) } + + private let constructor: () -> FixedIterator + + /// Construct an infinite sequence of fixed values starting from a specific value. + /// - Parameters: + /// - start: The starting fixed value. + /// - stride: The difference between subsequent calendar values. + public init(start: Fixed, stride: TimeDifference) { + constructor = { FixedIterator(start: start, stride: stride, keepGoing: { _ in return true }) } + } + + /// Construct a sequence of fixed values starting from a specific value. + /// - Parameters: + /// - start: The starting fixed value. + /// - stride: The difference between subsequent fixed values. + /// - keepGoing: A closure that is invoked to indicate whether the sequence should continue. This closure is invoked *before* the next value is generated. + public init( + start: Fixed, stride: TimeDifference, while keepGoing: @escaping (Fixed) -> Bool + ) { + constructor = { FixedIterator(start: start, stride: stride, keepGoing: keepGoing) } + } + + /// Construct a sequence of fixed values that iterates through a definite range of values. + /// + /// - Note: This sequence iterates through values *up to but not including* the upper bound of the range. + /// - Parameters: + /// - range: The `Range` of fixed values to iterate through. + /// - stride: The difference between subsequent fixed values. + public init(range: Range>, stride: TimeDifference) { + let lower = range.lowerBound + let upper = + range.upperBound.region.isEquivalent(to: lower.region) + ? range.upperBound : Fixed(region: lower.region, instant: range.upperBound.firstInstant) + constructor = { + FixedIterator( + region: lower.region, range: lower.firstInstant.. FixedIterator { - return constructor() + } + + /// Construct a sequence of fixed values that iterates through a closed range of values. + /// + /// - Note: This sequence iterates through values *up to and including* the upper bound of the range. + /// - Parameters: + /// - range: The `ClosedRange` of fixed values to iterate through. + /// - stride: The difference between subsequent fixed values. + public init(range: ClosedRange>, stride: TimeDifference) { + let lower = range.lowerBound + let upper = + range.upperBound.region.isEquivalent(to: lower.region) + ? range.upperBound : Fixed(region: lower.region, instant: range.upperBound.firstInstant) + constructor = { + FixedIterator( + region: lower.region, range: lower.firstInstant...upper.firstInstant, stride: stride) } - + } + + internal init( + parent: Fixed, stride: TimeDifference = TimeDifference(value: 1, unit: U.component) + ) { + constructor = { FixedIterator(region: parent.region, range: parent.range, stride: stride) } + } + + public __consuming func makeIterator() -> FixedIterator { + return constructor() + } + } /// An iterator of fixed values. public struct FixedIterator: IteratorProtocol { - private let region: Region - - private let keepGoing: (Fixed) -> Bool - private let start: Fixed - - private var scale = 0 - private let stride: DateComponents - - /// Construct an iterator of fixed values starting from a specific value. - /// - Parameters: - /// - start: The starting fixed value. - /// - stride: The difference between subsequent fixed values. - /// - keepGoing: A closure that is invoked to indicate whether the sequence should continue. This closure is invoked *before* the next value is generated. - public init(start: Fixed, stride: TimeDifference, keepGoing: @escaping (Fixed) -> Bool) { - self.region = start.region - self.start = start - self.stride = stride.dateComponents - self.keepGoing = keepGoing - } - - /// Construct an iterator of fixed values that are within a specific range - /// - Parameters: - /// - region: The ``Region`` of the fixed values - /// - range: The ``Instant`` range through which to iterate - /// - stride: The difference between subsequent fixed values - public init(region: Region, range: Range, stride: TimeDifference) { - self.region = region - self.keepGoing = { - let thisRange = $0.range - return range.lowerBound <= thisRange.lowerBound && thisRange.upperBound <= range.upperBound - } - self.start = Fixed(region: region, instant: range.lowerBound) - self.stride = stride.dateComponents - } - - /// Construct an iterator of fixed values that are within a specific closed range - /// - /// - Note: This sequence iterates through values *up to and including* the upper bound of the range. - /// - Parameters: - /// - region: The ``Region`` of the fixed values - /// - range: The `ClosedRange` of ``Instant`` values to iterate through. - /// - stride: The difference between subsequent fixed values. - public init(region: Region, range: ClosedRange, stride: TimeDifference) { - self.region = region - self.keepGoing = { range.overlaps($0.range) } - self.start = Fixed(region: region, instant: range.lowerBound) - self.stride = stride.dateComponents - } - - /// Produce the next fixed value - /// - Returns: The next fixed value, or `nil` if there are no more values to produce. - public mutating func next() -> Fixed? { - let next = stride.scale(by: scale) - scale += 1 - - let delta = TimeDifference(next) - let n = start + delta - guard keepGoing(n) else { return nil } - - return n + private let region: Region + + private let keepGoing: (Fixed) -> Bool + private let start: Fixed + + private var scale = 0 + private let stride: DateComponents + + /// Construct an iterator of fixed values starting from a specific value. + /// - Parameters: + /// - start: The starting fixed value. + /// - stride: The difference between subsequent fixed values. + /// - keepGoing: A closure that is invoked to indicate whether the sequence should continue. This closure is invoked *before* the next value is generated. + public init( + start: Fixed, stride: TimeDifference, keepGoing: @escaping (Fixed) -> Bool + ) { + self.region = start.region + self.start = start + self.stride = stride.dateComponents + self.keepGoing = keepGoing + } + + /// Construct an iterator of fixed values that are within a specific range + /// - Parameters: + /// - region: The ``Region`` of the fixed values + /// - range: The ``Instant`` range through which to iterate + /// - stride: The difference between subsequent fixed values + public init(region: Region, range: Range, stride: TimeDifference) { + self.region = region + self.keepGoing = { + let thisRange = $0.range + return range.lowerBound <= thisRange.lowerBound && thisRange.upperBound <= range.upperBound } + self.start = Fixed(region: region, instant: range.lowerBound) + self.stride = stride.dateComponents + } + + /// Construct an iterator of fixed values that are within a specific closed range + /// + /// - Note: This sequence iterates through values *up to and including* the upper bound of the range. + /// - Parameters: + /// - region: The ``Region`` of the fixed values + /// - range: The `ClosedRange` of ``Instant`` values to iterate through. + /// - stride: The difference between subsequent fixed values. + public init(region: Region, range: ClosedRange, stride: TimeDifference) { + self.region = region + self.keepGoing = { range.overlaps($0.range) } + self.start = Fixed(region: region, instant: range.lowerBound) + self.stride = stride.dateComponents + } + + /// Produce the next fixed value + /// - Returns: The next fixed value, or `nil` if there are no more values to produce. + public mutating func next() -> Fixed? { + let next = stride.scale(by: scale) + scale += 1 + + let delta = TimeDifference(next) + let n = start + delta + guard keepGoing(n) else { return nil } + + return n + } } diff --git a/Sources/Time/8-Formatting/Fixed+FormatRaw.swift b/Sources/Time/8-Formatting/Fixed+FormatRaw.swift index 75c3e1a..e345c18 100644 --- a/Sources/Time/8-Formatting/Fixed+FormatRaw.swift +++ b/Sources/Time/8-Formatting/Fixed+FormatRaw.swift @@ -1,49 +1,50 @@ import Foundation extension Fixed { - - /// Format a `Fixed` value using hard-coded format string. - /// - /// The localized format options provided for Fixed values are not always sufficient. - /// The most common scenario for needing a hard-coded format string is when communicating with servers, - /// which tend to expect a timestamp in a very specific format (ex: ISO8601). This requirement - /// is at odds with the locale-sensitive formatting provided by this package. - /// - /// This method allows you to provided an explicit format string while formatting. - /// - /// The format string is checked to make sure the specified calendar components are - /// all represented by this value. If they are not *and* the `strict` parameter is `true` (the default), - /// then a `TimeError` is thrown indicating that there are missing components while parsing. - /// - /// If the format string specifies unrepresented calendar components and `strict` is `false`, then - /// the `firstInstant` of the value is used. - /// - /// - Parameters: - /// - rawFormatString: The raw, unlocalized format string to be used for formatting. - /// - strict: Whether or not this method should throw a ``TimeError`` if the format string specifies - /// unrepresented calendar components. - /// - Throws: A `TimeError` if either the `rawFormatString` is syntactically incorrect, or the format string is - /// requesting units for formatting that are not represented by this fixed value *and* the `strict` parameter is `true` - public func format(raw rawFormatString: String, strict: Bool = true) throws -> String { - let format = try ParsedFormat(formatString: rawFormatString) - - let formattedUnits = format.components.compactMap(\.unit) - let requiredUnits = formattedUnits.map(\.minimumRequiredComponent) - - let missingUnits = Set(requiredUnits).subtracting(self.representedComponents) - - let style: FixedFormat - if missingUnits.isEmpty == false { - if strict == true { - // the format string specified units that are not represented by this value - let desc = "The provided format string '\(rawFormatString)' includes components (\(missingUnits)) that are not represented in a \(Self.self) value (\(Self.representedComponents))" - throw TimeError.invalidFormatString(rawFormatString, units: missingUnits, description: desc) - } else { - style = .init(raw: rawFormatString) - } - } else { - style = .init(raw: rawFormatString) - } - return self.format(style) + + /// Format a `Fixed` value using hard-coded format string. + /// + /// The localized format options provided for Fixed values are not always sufficient. + /// The most common scenario for needing a hard-coded format string is when communicating with servers, + /// which tend to expect a timestamp in a very specific format (ex: ISO8601). This requirement + /// is at odds with the locale-sensitive formatting provided by this package. + /// + /// This method allows you to provided an explicit format string while formatting. + /// + /// The format string is checked to make sure the specified calendar components are + /// all represented by this value. If they are not *and* the `strict` parameter is `true` (the default), + /// then a `TimeError` is thrown indicating that there are missing components while parsing. + /// + /// If the format string specifies unrepresented calendar components and `strict` is `false`, then + /// the `firstInstant` of the value is used. + /// + /// - Parameters: + /// - rawFormatString: The raw, unlocalized format string to be used for formatting. + /// - strict: Whether or not this method should throw a ``TimeError`` if the format string specifies + /// unrepresented calendar components. + /// - Throws: A `TimeError` if either the `rawFormatString` is syntactically incorrect, or the format string is + /// requesting units for formatting that are not represented by this fixed value *and* the `strict` parameter is `true` + public func format(raw rawFormatString: String, strict: Bool = true) throws -> String { + let format = try ParsedFormat(formatString: rawFormatString) + + let formattedUnits = format.components.compactMap(\.unit) + let requiredUnits = formattedUnits.map(\.minimumRequiredComponent) + + let missingUnits = Set(requiredUnits).subtracting(self.representedComponents) + + let style: FixedFormat + if missingUnits.isEmpty == false { + if strict == true { + // the format string specified units that are not represented by this value + let desc = + "The provided format string '\(rawFormatString)' includes components (\(missingUnits)) that are not represented in a \(Self.self) value (\(Self.representedComponents))" + throw TimeError.invalidFormatString(rawFormatString, units: missingUnits, description: desc) + } else { + style = .init(raw: rawFormatString) + } + } else { + style = .init(raw: rawFormatString) } + return self.format(style) + } } diff --git a/Sources/Time/8-Formatting/Fixed+FormatStyle.swift b/Sources/Time/8-Formatting/Fixed+FormatStyle.swift index 8804009..a2bdb61 100644 --- a/Sources/Time/8-Formatting/Fixed+FormatStyle.swift +++ b/Sources/Time/8-Formatting/Fixed+FormatStyle.swift @@ -2,91 +2,91 @@ import Foundation /// A convenient way to specify general formats for fixed values. public struct FixedFormatStyle: Hashable { - - /// An extremely verbose format style. - /// - /// When applied to date values, a full format style includes the weekday name and fully-spelled-out month name. - /// - /// Example: `Monday, January 1, 2024` - /// - /// When applied to time values, it includes the time down to the second a fully-spelled-out time zone names. - /// - /// Example: `3:12:43 AM Central Standard Time` - public static let full = FixedFormatStyle(style: .full) - - /// A verbose format style. - /// - /// When applied to date values, it includes the fully-spelled-out month name. - /// - /// Example: `January 1, 2024` - /// - /// When applied to time values, it includes the time down to the second and abbreviated time zone information. - /// - /// Example: `3:12:43 AM CST` - public static let long = FixedFormatStyle(style: .long) - - /// A simple format style. - /// - /// When applied to dates, it includes an abbreviated month name. - /// - /// Example: `Jan 1, 2024` - /// - /// When applied to time values, it includes the time down to the second. - /// - /// Example: `3:12:43 AM` - public static let medium = FixedFormatStyle(style: .medium) - - /// A terse format style. - /// - /// When applied to date values, it uses abbreviated numeric values. - /// - /// Example: `1/1/24` - /// - /// When applied to time values, it omits seconds. - /// - /// Example: `3:12 AM` - public static let short = FixedFormatStyle(style: .short) - - internal let style: DateFormatter.Style - + + /// An extremely verbose format style. + /// + /// When applied to date values, a full format style includes the weekday name and fully-spelled-out month name. + /// + /// Example: `Monday, January 1, 2024` + /// + /// When applied to time values, it includes the time down to the second a fully-spelled-out time zone names. + /// + /// Example: `3:12:43 AM Central Standard Time` + public static let full = FixedFormatStyle(style: .full) + + /// A verbose format style. + /// + /// When applied to date values, it includes the fully-spelled-out month name. + /// + /// Example: `January 1, 2024` + /// + /// When applied to time values, it includes the time down to the second and abbreviated time zone information. + /// + /// Example: `3:12:43 AM CST` + public static let long = FixedFormatStyle(style: .long) + + /// A simple format style. + /// + /// When applied to dates, it includes an abbreviated month name. + /// + /// Example: `Jan 1, 2024` + /// + /// When applied to time values, it includes the time down to the second. + /// + /// Example: `3:12:43 AM` + public static let medium = FixedFormatStyle(style: .medium) + + /// A terse format style. + /// + /// When applied to date values, it uses abbreviated numeric values. + /// + /// Example: `1/1/24` + /// + /// When applied to time values, it omits seconds. + /// + /// Example: `3:12 AM` + public static let short = FixedFormatStyle(style: .short) + + internal let style: DateFormatter.Style + } extension Fixed where Granularity: LTOEDay { - - /// Format the fixed value's date information - /// - Parameter dateStyle: The `FixedFormatStyle` to use - /// - Returns: A localized string containing the formatted date information - public func format(date dateStyle: FixedFormatStyle) -> String { - let style = FixedFormat(dateStyle: dateStyle.style, timeStyle: .none) - return self.format(style) - } - + + /// Format the fixed value's date information + /// - Parameter dateStyle: The `FixedFormatStyle` to use + /// - Returns: A localized string containing the formatted date information + public func format(date dateStyle: FixedFormatStyle) -> String { + let style = FixedFormat(dateStyle: dateStyle.style, timeStyle: .none) + return self.format(style) + } + } extension Fixed where Granularity: LTOEMinute { - - /// Format the fixed value's date and time information - /// - /// - Parameters: - /// - dateStyle: The `FixedFormatStyle` to use for the date information - /// - timeStyle: The `FixedFormatStyle` to use for the time information - /// - Returns: A string containing the localized formatted date and time information - /// - Note: Formatting the time using the `.full`, `.long`, or `.medium` format styles will produce a string that displays a seconds value. - /// If these are used on a `Fixed` value, then it will assume the seconds component is `:00`. - public func format(date dateStyle: FixedFormatStyle, time timeStyle: FixedFormatStyle) -> String { - let style = FixedFormat(dateStyle: dateStyle.style, timeStyle: timeStyle.style) - return self.format(style) - } - - /// Format the fixed value's time information - /// - /// - Parameter timeStyle: The `FixedFormatStyle` to use - /// - Returns: A string containing the localized formatted time information - /// - Note: Formatting the time using the `.full`, `.long`, or `.medium` format styles will produce a string that displays a seconds value. - /// If these are used on a `Fixed` value, then it will assume the seconds component is `:00`. - public func format(time timeStyle: FixedFormatStyle) -> String { - let style = FixedFormat(dateStyle: .none, timeStyle: timeStyle.style) - return self.format(style) - } - + + /// Format the fixed value's date and time information + /// + /// - Parameters: + /// - dateStyle: The `FixedFormatStyle` to use for the date information + /// - timeStyle: The `FixedFormatStyle` to use for the time information + /// - Returns: A string containing the localized formatted date and time information + /// - Note: Formatting the time using the `.full`, `.long`, or `.medium` format styles will produce a string that displays a seconds value. + /// If these are used on a `Fixed` value, then it will assume the seconds component is `:00`. + public func format(date dateStyle: FixedFormatStyle, time timeStyle: FixedFormatStyle) -> String { + let style = FixedFormat(dateStyle: dateStyle.style, timeStyle: timeStyle.style) + return self.format(style) + } + + /// Format the fixed value's time information + /// + /// - Parameter timeStyle: The `FixedFormatStyle` to use + /// - Returns: A string containing the localized formatted time information + /// - Note: Formatting the time using the `.full`, `.long`, or `.medium` format styles will produce a string that displays a seconds value. + /// If these are used on a `Fixed` value, then it will assume the seconds component is `:00`. + public func format(time timeStyle: FixedFormatStyle) -> String { + let style = FixedFormat(dateStyle: .none, timeStyle: timeStyle.style) + return self.format(style) + } + } diff --git a/Sources/Time/8-Formatting/Fixed+FormatTemplate.swift b/Sources/Time/8-Formatting/Fixed+FormatTemplate.swift index a623a32..fb791ef 100644 --- a/Sources/Time/8-Formatting/Fixed+FormatTemplate.swift +++ b/Sources/Time/8-Formatting/Fixed+FormatTemplate.swift @@ -1,176 +1,190 @@ import Foundation extension Fixed { - - /// Format the era of a fixed value - /// - Parameter era: The template for formatting the era - /// - Returns: A string with the formatted era information - public func format(era: Template) -> String { - return format([era]) - } - + + /// Format the era of a fixed value + /// - Parameter era: The template for formatting the era + /// - Returns: A string with the formatted era information + public func format(era: Template) -> String { + return format([era]) + } + } extension Fixed where Granularity: LTOEYear { - - /// Format the year of a fixed value - /// - Parameters: - /// - era: The template for formatting the era - /// - year: The template for formatting the year - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(era: Template, - year: Template, - timeZone: Template? = nil) -> String { - return format([era, year, timeZone]) - } - + + /// Format the year of a fixed value + /// - Parameters: + /// - era: The template for formatting the era + /// - year: The template for formatting the year + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + era: Template, + year: Template, + timeZone: Template? = nil + ) -> String { + return format([era, year, timeZone]) + } + } extension Fixed where Granularity: LTOEMonth { - - /// Format the year and month of a fixed value - /// - Parameters: - /// - era: The template for formatting the era - /// - year: The template for formatting the year - /// - month: The template for formatting the month - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(era: Template, - year: Template, - month: Template, - timeZone: Template? = nil) -> String { - return format([era, year, month, timeZone]) - } - + + /// Format the year and month of a fixed value + /// - Parameters: + /// - era: The template for formatting the era + /// - year: The template for formatting the year + /// - month: The template for formatting the month + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + era: Template, + year: Template, + month: Template, + timeZone: Template? = nil + ) -> String { + return format([era, year, month, timeZone]) + } + } extension Fixed where Granularity: LTOEDay { - - /// Format the year, month, and day of a fixed value - /// - Parameters: - /// - era: The template for formatting the era - /// - year: The template for formatting the year - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(era: Template? = nil, - year: Template, - month: Template, - day: Template, - weekday: Template? = nil, - timeZone: Template? = nil) -> String { - return format([era, year, month, day, weekday, timeZone]) - } - + + /// Format the year, month, and day of a fixed value + /// - Parameters: + /// - era: The template for formatting the era + /// - year: The template for formatting the year + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + era: Template? = nil, + year: Template, + month: Template, + day: Template, + weekday: Template? = nil, + timeZone: Template? = nil + ) -> String { + return format([era, year, month, day, weekday, timeZone]) + } + } extension Fixed where Granularity: LTOEHour { - - /// Format the year, month, day, and hour of a fixed value - /// - Parameters: - /// - era: The template for formatting the era - /// - year: The template for formatting the year - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(era: Template? = nil, - year: Template, - month: Template, - day: Template, - weekday: Template? = nil, - hour: Template, - timeZone: Template? = nil) -> String { - return format([era, year, month, day, weekday, hour, timeZone]) - } - + + /// Format the year, month, day, and hour of a fixed value + /// - Parameters: + /// - era: The template for formatting the era + /// - year: The template for formatting the year + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + era: Template? = nil, + year: Template, + month: Template, + day: Template, + weekday: Template? = nil, + hour: Template, + timeZone: Template? = nil + ) -> String { + return format([era, year, month, day, weekday, hour, timeZone]) + } + } extension Fixed where Granularity: LTOEMinute { - - /// Format the year, month, day, hour, and minute of a fixed value - /// - Parameters: - /// - era: The template for formatting the era - /// - year: The template for formatting the year - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(era: Template? = nil, - year: Template, - month: Template, - day: Template, - weekday: Template? = nil, - hour: Template, - minute: Template, - timeZone: Template? = nil) -> String { - return format([era, year, month, day, weekday, hour, minute, timeZone]) - } - + + /// Format the year, month, day, hour, and minute of a fixed value + /// - Parameters: + /// - era: The template for formatting the era + /// - year: The template for formatting the year + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + era: Template? = nil, + year: Template, + month: Template, + day: Template, + weekday: Template? = nil, + hour: Template, + minute: Template, + timeZone: Template? = nil + ) -> String { + return format([era, year, month, day, weekday, hour, minute, timeZone]) + } + } extension Fixed where Granularity: LTOESecond { - - /// Format the year, month, day, hour, minute, and second of a fixed value - /// - Parameters: - /// - era: The template for formatting the era - /// - year: The template for formatting the year - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - second: The template for formatting the second - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(era: Template? = nil, - year: Template, - month: Template, - day: Template, - weekday: Template? = nil, - hour: Template, - minute: Template, - second: Template, - timeZone: Template? = nil) -> String { - return format([era, year, month, day, weekday, hour, minute, second, timeZone]) - } - + + /// Format the year, month, day, hour, minute, and second of a fixed value + /// - Parameters: + /// - era: The template for formatting the era + /// - year: The template for formatting the year + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - second: The template for formatting the second + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + era: Template? = nil, + year: Template, + month: Template, + day: Template, + weekday: Template? = nil, + hour: Template, + minute: Template, + second: Template, + timeZone: Template? = nil + ) -> String { + return format([era, year, month, day, weekday, hour, minute, second, timeZone]) + } + } extension Fixed where Granularity: LTOENanosecond { - - /// Format the year, month, day, hour, minute, second, and nanosecond of a fixed value - /// - Parameters: - /// - era: The template for formatting the era - /// - year: The template for formatting the year - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - second: The template for formatting the second - /// - nanosecond: The template for formatting the nanosecond - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(era: Template? = nil, - year: Template, - month: Template, - day: Template, - weekday: Template? = nil, - hour: Template, - minute: Template, - second: Template, - nanosecond: Template, - timeZone: Template? = nil) -> String { - return format([era, year, month, day, weekday, hour, minute, second, nanosecond, timeZone]) - } - + + /// Format the year, month, day, hour, minute, second, and nanosecond of a fixed value + /// - Parameters: + /// - era: The template for formatting the era + /// - year: The template for formatting the year + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - second: The template for formatting the second + /// - nanosecond: The template for formatting the nanosecond + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + era: Template? = nil, + year: Template, + month: Template, + day: Template, + weekday: Template? = nil, + hour: Template, + minute: Template, + second: Template, + nanosecond: Template, + timeZone: Template? = nil + ) -> String { + return format([era, year, month, day, weekday, hour, minute, second, nanosecond, timeZone]) + } + } diff --git a/Sources/Time/8-Formatting/FixedFormat.swift b/Sources/Time/8-Formatting/FixedFormat.swift index fc0512b..6759faf 100644 --- a/Sources/Time/8-Formatting/FixedFormat.swift +++ b/Sources/Time/8-Formatting/FixedFormat.swift @@ -2,24 +2,24 @@ import Foundation /// A type that encapsulates the information necessary to format a fixed value internal struct FixedFormat: Sendable { - - internal let configuration: FormatConfiguration - - internal init(dateStyle: DateFormatter.Style, timeStyle: DateFormatter.Style) { - self.configuration = .styles(dateStyle, timeStyle) - } - - internal init(raw: String) { - self.configuration = .raw(raw) - } - - internal init(templates: Array) { - self.configuration = .template(templates.compactMap { $0?.template }.joined()) - } - - init(naturalFormats calendar: Calendar) { - let formats = Fixed.naturalFormats(in: calendar) - self.init(templates: formats) - } - + + internal let configuration: FormatConfiguration + + internal init(dateStyle: DateFormatter.Style, timeStyle: DateFormatter.Style) { + self.configuration = .styles(dateStyle, timeStyle) + } + + internal init(raw: String) { + self.configuration = .raw(raw) + } + + internal init(templates: [Format?]) { + self.configuration = .template(templates.compactMap { $0?.template }.joined()) + } + + init(naturalFormats calendar: Calendar) { + let formats = Fixed.naturalFormats(in: calendar) + self.init(templates: formats) + } + } diff --git a/Sources/Time/8-Formatting/FormatTemplates.swift b/Sources/Time/8-Formatting/FormatTemplates.swift index 0232629..88b9d5f 100644 --- a/Sources/Time/8-Formatting/FormatTemplates.swift +++ b/Sources/Time/8-Formatting/FormatTemplates.swift @@ -2,307 +2,308 @@ import Foundation /// A type for defining template patterns used when formatting fixed values public struct Template: Equatable, Format, Sendable { - internal let template: String - internal init(_ template: String) { - self.template = template - } - + internal let template: String + internal init(_ template: String) { + self.template = template + } + } extension Template where Unit == Era { - /// The abbreviated name of an era, such as "AD" or "BC" - public static let abbreviated = Template("G") - - /// The narrow name of an era, such as "A" or "B" - public static let narrow = Template("GGGGG") - - /// The full name of an era, such as "Anno Domini" or "Before Christ" - public static let wide = Template("GGGG") - + /// The abbreviated name of an era, such as "AD" or "BC" + public static let abbreviated = Template("G") + + /// The narrow name of an era, such as "A" or "B" + public static let narrow = Template("GGGGG") + + /// The full name of an era, such as "Anno Domini" or "Before Christ" + public static let wide = Template("GGGG") + } extension Template where Unit == Year { - - /// The natural length of a year, such as "5" or "2024" or "23941" - public static let naturalDigits = Template("y") - - /// The last to digits of a year, such as "05", "24" or "41" - public static let twoDigits = digits(paddedToLength: 2) - - /// A year padded or truncated to a fixed length - /// - /// For example, `.digits(paddedToLength: 3)` might result in year strings of "005", "024", or "941". - /// - Parameter paddedToLength: The length of the resulting string. Extra leading digits are omitted, and the string is zero-padded to reach the specified length - /// - Returns: A template for the string representation of a calendar year - public static func digits(paddedToLength: Int) -> Template { - guard paddedToLength > 0 else { fatalError("Cannot pad to a length less than 1") } - let template = String(repeating: "y", count: paddedToLength) - return Template(template) - } - + + /// The natural length of a year, such as "5" or "2024" or "23941" + public static let naturalDigits = Template("y") + + /// The last to digits of a year, such as "05", "24" or "41" + public static let twoDigits = digits(paddedToLength: 2) + + /// A year padded or truncated to a fixed length + /// + /// For example, `.digits(paddedToLength: 3)` might result in year strings of "005", "024", or "941". + /// - Parameter paddedToLength: The length of the resulting string. Extra leading digits are omitted, and the string is zero-padded to reach the specified length + /// - Returns: A template for the string representation of a calendar year + public static func digits(paddedToLength: Int) -> Template { + guard paddedToLength > 0 else { fatalError("Cannot pad to a length less than 1") } + let template = String(repeating: "y", count: paddedToLength) + return Template(template) + } + } extension Template where Unit == Month { - - /// The natural length of a numeric month when used as part of a larger date context, such as "2" or "11" - public static let naturalDigits = Template("M") - - /// The numeric month when used as part of a larger date context, zero-padded to two digits, such as "05" or "11 - public static let twoDigits = Template("MM") - - /// The natural name of the month when used as part of a larger date context, such as "February" - public static let naturalName = Template("MMMM") - - /// The abbreviated name of the month when used as part of a larger date context, such as "Feb" - public static let abbreviatedName = Template("MMM") - - /// The narrow name of the month when used as part of a larger date context, such as "F" - public static let narrowName = Template("MMMMM") - + + /// The natural length of a numeric month when used as part of a larger date context, such as "2" or "11" + public static let naturalDigits = Template("M") + + /// The numeric month when used as part of a larger date context, zero-padded to two digits, such as "05" or "11 + public static let twoDigits = Template("MM") + + /// The natural name of the month when used as part of a larger date context, such as "February" + public static let naturalName = Template("MMMM") + + /// The abbreviated name of the month when used as part of a larger date context, such as "Feb" + public static let abbreviatedName = Template("MMM") + + /// The narrow name of the month when used as part of a larger date context, such as "F" + public static let narrowName = Template("MMMMM") + } /// A type used in conjunction with defining templates that are used by themselves. /// /// - SeeAlso: ``Template``, "Formatting Standalone Months" /// - SeeAlso: ``Template``, "Formatting Standalone Weekdays" -public enum Standalone { } +public enum Standalone {} extension Template where Unit == Standalone { - - /// The natural length of a numeric month when used by itself, such as "2" or "11" - public static let naturalDigits = Template("L") - - /// The numeric month when used by itself, zero-padded to two digits, such as "05" or "11 - public static let twoDigits = Template("LL") - - /// The natural name of the month when used by itself, such as "February" - public static let naturalName = Template("LLLL") - - /// The abbreviated name of the month when used by itself, such as "Feb" - public static let abbreviatedName = Template("LLL") - - /// The narrow name of the month when used by itself, such as "F" - public static let narrowName = Template("LLLLL") - + + /// The natural length of a numeric month when used by itself, such as "2" or "11" + public static let naturalDigits = Template("L") + + /// The numeric month when used by itself, zero-padded to two digits, such as "05" or "11 + public static let twoDigits = Template("LL") + + /// The natural name of the month when used by itself, such as "February" + public static let naturalName = Template("LLLL") + + /// The abbreviated name of the month when used by itself, such as "Feb" + public static let abbreviatedName = Template("LLL") + + /// The narrow name of the month when used by itself, such as "F" + public static let narrowName = Template("LLLLL") + } extension Template where Unit == Day { - - /// The natural length of a numeric day, such as "3" or "24" - public static let naturalDigits = Template("d") - - /// The numeric day, zero-padded to two digits, such as "03" or "24" - public static let twoDigits = Template("dd") - + + /// The natural length of a numeric day, such as "3" or "24" + public static let naturalDigits = Template("d") + + /// The numeric day, zero-padded to two digits, such as "03" or "24" + public static let twoDigits = Template("dd") + } /// A type used to construct a format template representing a named weekday /// /// - SeeAlso: ``Template``, "Formatting Weekdays" -public enum Weekday { } +public enum Weekday {} extension Template where Unit == Weekday { - - /// The natural length of the numeric weekday when used as part of a larger date context, such as "1" or "7" - public static let naturalDigits = Template("e") - - /// The numeric weekday when used as part of a larger date context, zero-padded to two digits, such as "01" or "07" - public static let twoDigits = Template("ee") - - /// The natural name of the weekday when used as part of a larger date context, such as "Sunday" or "Thursday" - public static let naturalName = Template("EEEE") - - /// The abbreviated name of the weekday when used as part of a larger date context, such as "Sun" or "Thu" - public static let abbreviatedName = Template("EEE") - - /// The short name of the weekday when used as part of a larger date context, such as "Su" or "Th" - public static let shortName = Template("EEEEEE") - - /// The narrow name of the weekday when used as part of a larger date context, such as "S" or "T" - public static let narrowName = Template("EEEEE") - + + /// The natural length of the numeric weekday when used as part of a larger date context, such as "1" or "7" + public static let naturalDigits = Template("e") + + /// The numeric weekday when used as part of a larger date context, zero-padded to two digits, such as "01" or "07" + public static let twoDigits = Template("ee") + + /// The natural name of the weekday when used as part of a larger date context, such as "Sunday" or "Thursday" + public static let naturalName = Template("EEEE") + + /// The abbreviated name of the weekday when used as part of a larger date context, such as "Sun" or "Thu" + public static let abbreviatedName = Template("EEE") + + /// The short name of the weekday when used as part of a larger date context, such as "Su" or "Th" + public static let shortName = Template("EEEEEE") + + /// The narrow name of the weekday when used as part of a larger date context, such as "S" or "T" + public static let narrowName = Template("EEEEE") + } extension Template where Unit == Standalone { - - /// The natural length of the numeric weekday when used by itself, such as "1" or "7" - public static let naturalDigits = Template("c") - - /// The natural name of the weekday when used by itself, such as "Sunday" or "Thursday" - public static let naturalName = Template("cccc") - - /// The abbreviated name of the weekday when used by itself, such as "Sun" or "Thu" - public static let abbreviatedName = Template("ccc") - - /// The short name of the weekday when used by itself, such as "Su" or "Th" - public static let shortName = Template("cccccc") - - /// The narrow name of the weekday when used by itself, such as "S" or "T" - public static let narrowName = Template("ccccc") - + + /// The natural length of the numeric weekday when used by itself, such as "1" or "7" + public static let naturalDigits = Template("c") + + /// The natural name of the weekday when used by itself, such as "Sunday" or "Thursday" + public static let naturalName = Template("cccc") + + /// The abbreviated name of the weekday when used by itself, such as "Sun" or "Thu" + public static let abbreviatedName = Template("ccc") + + /// The short name of the weekday when used by itself, such as "Su" or "Th" + public static let shortName = Template("cccccc") + + /// The narrow name of the weekday when used by itself, such as "S" or "T" + public static let narrowName = Template("ccccc") + } /// A type used to construct a format template representing day periods (AM and PM) /// /// - SeeAlso: ``Template``, "Formatting Day Periods" -public enum DayPeriod { } +public enum DayPeriod {} extension Template where Unit == DayPeriod { - - /// The natural name of the day period, such as "AM" or "PM" - public static let natural = Template("a") - - /// The wide name of the day period, such as "Ante Meridiem" or "Post Meridiem", depending on the region - public static let wide = Template("aaaa") - - /// The narrow name of the day period, such as "a" or "p" - public static let narrow = Template("aaaaa") - + + /// The natural name of the day period, such as "AM" or "PM" + public static let natural = Template("a") + + /// The wide name of the day period, such as "Ante Meridiem" or "Post Meridiem", depending on the region + public static let wide = Template("aaaa") + + /// The narrow name of the day period, such as "a" or "p" + public static let narrow = Template("aaaaa") + } extension Template where Unit == Hour { - - /// The natural length of the numeric hour, with the natural day period name, depending on the region. - /// - /// For example "3 AM" or "14" - public static let naturalDigits = Template.naturalDigits(dayPeriod: .natural) - - /// The numeric hour, zero-padded to two digits, with the natural day period name, depending on the region. - /// - /// For example, "03 AM" or "14" - public static let twoDigits = Template.twoDigits(dayPeriod: .natural) - - /// The natural length of the numeric hour, optionally with the day period - /// - Parameter dayPeriod: The `Template` to use when formatting; may be `nil` to omit it - /// - Returns: A template for the string representation of an hour - public static func naturalDigits(dayPeriod: Template?) -> Template { - guard let p = dayPeriod else { return Template("J") } - - if p == .wide { - return Template("jjj") - } else if p == .narrow { - return Template("jjjjj") - } else { - return Template("j") - } + + /// The natural length of the numeric hour, with the natural day period name, depending on the region. + /// + /// For example "3 AM" or "14" + public static let naturalDigits = Template.naturalDigits(dayPeriod: .natural) + + /// The numeric hour, zero-padded to two digits, with the natural day period name, depending on the region. + /// + /// For example, "03 AM" or "14" + public static let twoDigits = Template.twoDigits(dayPeriod: .natural) + + /// The natural length of the numeric hour, optionally with the day period + /// - Parameter dayPeriod: The `Template` to use when formatting; may be `nil` to omit it + /// - Returns: A template for the string representation of an hour + public static func naturalDigits(dayPeriod: Template?) -> Template { + guard let p = dayPeriod else { return Template("J") } + + if p == .wide { + return Template("jjj") + } else if p == .narrow { + return Template("jjjjj") + } else { + return Template("j") } - - /// The numeric hour, zero-padded to two digits, optionally with the day period - /// - Parameter dayPeriod: The `Template` to use when formatting; may be `nil` to omit it - /// - Returns: A template for the string representation of an hour - public static func twoDigits(dayPeriod: Template?) -> Template { - guard let p = dayPeriod else { return Template("JJ") } - - if p == .wide { - return Template("jjjj") - } else if p == .narrow { - return Template("jjjjjj") - } else { - return Template("jj") - } + } + + /// The numeric hour, zero-padded to two digits, optionally with the day period + /// - Parameter dayPeriod: The `Template` to use when formatting; may be `nil` to omit it + /// - Returns: A template for the string representation of an hour + public static func twoDigits(dayPeriod: Template?) -> Template { + guard let p = dayPeriod else { return Template("JJ") } + + if p == .wide { + return Template("jjjj") + } else if p == .narrow { + return Template("jjjjjj") + } else { + return Template("jj") } - - + } + } extension Template where Unit == Minute { - - /// The natural length of the numeric minute, such as "6" or "42" - public static let naturalDigits = Template("m") - - /// The numeric minute, zero-padded to two digits, such as "06" or "42" - public static let twoDigits = Template("mm") - + + /// The natural length of the numeric minute, such as "6" or "42" + public static let naturalDigits = Template("m") + + /// The numeric minute, zero-padded to two digits, such as "06" or "42" + public static let twoDigits = Template("mm") + } extension Template where Unit == Second { - - /// The natural length of the numeric second, such as "3" or "27" - public static let naturalDigits = Template("s") - - /// The numeric second, zero-padded to two digits, such as "03" or "27" - public static let twoDigits = Template("ss") - + + /// The natural length of the numeric second, such as "3" or "27" + public static let naturalDigits = Template("s") + + /// The numeric second, zero-padded to two digits, such as "03" or "27" + public static let twoDigits = Template("ss") + } extension Template where Unit == Nanosecond { - - /// The numeric fractional seconds - /// - Parameter length: The number of significant digits to include - /// - Returns: A template for the string representation of a fractional second - public static func digits(_ length: Int) -> Template { - guard length > 0 else { fatalError("Cannot pad to a length less than 1") } - let template = String(repeating: "S", count: length) - return Template(template) - } - + + /// The numeric fractional seconds + /// - Parameter length: The number of significant digits to include + /// - Returns: A template for the string representation of a fractional second + public static func digits(_ length: Int) -> Template { + guard length > 0 else { fatalError("Cannot pad to a length less than 1") } + let template = String(repeating: "S", count: length) + return Template(template) + } + } extension Template where Unit == TimeZone { - - /// The short specific time zone name, such as "MST" - public static let shortSpecific = Template("z") - - /// The long specific time zone name, such as "Mountain Standard Time" - public static let longSpecific = Template("zzzz") - - /// The basic ISO8601 time zone offset, such as "-0700" (no colon) - public static let ISO8601Basic = Template("Z") - - /// The extended ISO8601 time zone offset, such as "-07:00" (with colon) - public static let ISO8601Extended = Template("ZZZZZ") - - /// The GMT-based time zone offset, such as "GMT-7" - public static let shortLocalizedGMT = Template("O") - - /// The long GMT-based time zone offset, such as "GMT-07:00" - public static let longLocalizedGMT = Template("ZZZZ") - - /// The short generic time zone name, such as "MT" - public static let shortGeneric = Template("v") - - /// The long generic time zone name, such as "Mountain Time" - public static let longGeneric = Template("vvvv") - - /// The short time zone identifier, such as "usden" - public static let shortID = Template("V") - - /// The long time zone identifier, such as "America/Denver" - public static let longID = Template("VV") - - /// The name of an "exemplar" city in the time zone, such as "Denver". If the time zone does not have an exemplar city, it produces "Unknown City" - public static let exemplarCity = Template("VVV") - - /// The generic location name of the time zone, such as "Denver Time". If the time zone does not have an exemplar city, it uses the `.longLocalizedGMT` name. - public static let genericLocation = Template("VVVV") - - /// The ISO8601 time zone hour offset, or potentially Z for UTC+0 time zones. - /// - /// May include minutes for time zones with fractional hour offsets from UTC+0. - /// - /// For example, "-08", "+0530", or "Z" - /// - /// - Parameter includingZ: Whether UTC+0 time zones format as "Z" - /// - Returns: A template for the string representation of a time zone - public static func ISO8601BasicWithHours(includingZ: Bool = false) -> Template { - return Template(includingZ ? "X" : "x") - } - - /// The ISO8601 time zone offset, with hours and minutes, or potentially Z for UTC+0 time zones - /// - /// For example, "-0800", "+05:30", or "Z" - /// - /// - Parameters: - /// - extended: Whether formatted time zone offsets should incldue a colon between the hour and minute fields - /// - includingZ: Whether UTC+0 time zones format as "Z" - /// - Returns: A template for the string representation of a time zone - public static func ISO8601WithHoursAndMinutes(extended: Bool = false, includingZ: Bool = false) -> Template { - if extended { - return Template(includingZ ? "XXX" : "xxx") - } else { - return Template(includingZ ? "XX" : "xx") - } + + /// The short specific time zone name, such as "MST" + public static let shortSpecific = Template("z") + + /// The long specific time zone name, such as "Mountain Standard Time" + public static let longSpecific = Template("zzzz") + + /// The basic ISO8601 time zone offset, such as "-0700" (no colon) + public static let ISO8601Basic = Template("Z") + + /// The extended ISO8601 time zone offset, such as "-07:00" (with colon) + public static let ISO8601Extended = Template("ZZZZZ") + + /// The GMT-based time zone offset, such as "GMT-7" + public static let shortLocalizedGMT = Template("O") + + /// The long GMT-based time zone offset, such as "GMT-07:00" + public static let longLocalizedGMT = Template("ZZZZ") + + /// The short generic time zone name, such as "MT" + public static let shortGeneric = Template("v") + + /// The long generic time zone name, such as "Mountain Time" + public static let longGeneric = Template("vvvv") + + /// The short time zone identifier, such as "usden" + public static let shortID = Template("V") + + /// The long time zone identifier, such as "America/Denver" + public static let longID = Template("VV") + + /// The name of an "exemplar" city in the time zone, such as "Denver". If the time zone does not have an exemplar city, it produces "Unknown City" + public static let exemplarCity = Template("VVV") + + /// The generic location name of the time zone, such as "Denver Time". If the time zone does not have an exemplar city, it uses the `.longLocalizedGMT` name. + public static let genericLocation = Template("VVVV") + + /// The ISO8601 time zone hour offset, or potentially Z for UTC+0 time zones. + /// + /// May include minutes for time zones with fractional hour offsets from UTC+0. + /// + /// For example, "-08", "+0530", or "Z" + /// + /// - Parameter includingZ: Whether UTC+0 time zones format as "Z" + /// - Returns: A template for the string representation of a time zone + public static func ISO8601BasicWithHours(includingZ: Bool = false) -> Template { + return Template(includingZ ? "X" : "x") + } + + /// The ISO8601 time zone offset, with hours and minutes, or potentially Z for UTC+0 time zones + /// + /// For example, "-0800", "+05:30", or "Z" + /// + /// - Parameters: + /// - extended: Whether formatted time zone offsets should incldue a colon between the hour and minute fields + /// - includingZ: Whether UTC+0 time zones format as "Z" + /// - Returns: A template for the string representation of a time zone + public static func ISO8601WithHoursAndMinutes(extended: Bool = false, includingZ: Bool = false) + -> Template + { + if extended { + return Template(includingZ ? "XXX" : "xxx") + } else { + return Template(includingZ ? "XX" : "xx") } - + } + } diff --git a/Sources/Time/8-Formatting/PartialFormatting-1Unit.swift b/Sources/Time/8-Formatting/PartialFormatting-1Unit.swift index 238b237..f56b85f 100644 --- a/Sources/Time/8-Formatting/PartialFormatting-1Unit.swift +++ b/Sources/Time/8-Formatting/PartialFormatting-1Unit.swift @@ -1,122 +1,138 @@ import Foundation extension Fixed { - - /// Format the time zone of a fixed value - /// - Parameter timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted time zone information - public func format(timeZone: Template) -> String { - return format([timeZone]) - } - + + /// Format the time zone of a fixed value + /// - Parameter timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted time zone information + public func format(timeZone: Template) -> String { + return format([timeZone]) + } + } extension Fixed where Granularity: LTOEYear { - - /// Format the year of a fixed value - /// - Parameters: - /// - year: The template for formatting the year - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted year information - public func format(year: Template, - timeZone: Template? = nil) -> String { - return format([year, timeZone]) - } - + + /// Format the year of a fixed value + /// - Parameters: + /// - year: The template for formatting the year + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted year information + public func format( + year: Template, + timeZone: Template? = nil + ) -> String { + return format([year, timeZone]) + } + } extension Fixed where Granularity: LTOEMonth { - - /// Format the month of a fixed value - /// - Parameters: - /// - month: The template for formatting the month - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted month information - public func format(month: Template>, - timeZone: Template? = nil) -> String { - return format([month, timeZone]) - } - + + /// Format the month of a fixed value + /// - Parameters: + /// - month: The template for formatting the month + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted month information + public func format( + month: Template>, + timeZone: Template? = nil + ) -> String { + return format([month, timeZone]) + } + } extension Fixed where Granularity: LTOEDay { - - /// Format the day of a fixed value - /// - Parameters: - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted day information - public func format(day: Template, - weekday: Template? = nil, - timeZone: Template? = nil) -> String { - return format([day, weekday, timeZone]) - } - - /// Format the weekday of a fixed value - /// - Parameters: - /// - weekday: The template for formatting the day of the week - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted weekday information - public func format(weekday: Template>, - timeZone: Template? = nil) -> String { - return format([weekday, timeZone]) - } - + + /// Format the day of a fixed value + /// - Parameters: + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted day information + public func format( + day: Template, + weekday: Template? = nil, + timeZone: Template? = nil + ) -> String { + return format([day, weekday, timeZone]) + } + + /// Format the weekday of a fixed value + /// - Parameters: + /// - weekday: The template for formatting the day of the week + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted weekday information + public func format( + weekday: Template>, + timeZone: Template? = nil + ) -> String { + return format([weekday, timeZone]) + } + } extension Fixed where Granularity: LTOEHour { - - /// Format the hour of a fixed value - /// - Parameters: - /// - hour: The template for formatting the hour - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted hour information - public func format(hour: Template, - timeZone: Template? = nil) -> String { - return format([hour, timeZone]) - } - + + /// Format the hour of a fixed value + /// - Parameters: + /// - hour: The template for formatting the hour + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted hour information + public func format( + hour: Template, + timeZone: Template? = nil + ) -> String { + return format([hour, timeZone]) + } + } extension Fixed where Granularity: LTOEMinute { - - /// Format the minute of a fixed value - /// - Parameters: - /// - minute: The template for formatting the minute - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted minute information - public func format(minute: Template, - timeZone: Template? = nil) -> String { - return format([minute, timeZone]) - } - + + /// Format the minute of a fixed value + /// - Parameters: + /// - minute: The template for formatting the minute + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted minute information + public func format( + minute: Template, + timeZone: Template? = nil + ) -> String { + return format([minute, timeZone]) + } + } extension Fixed where Granularity: LTOESecond { - - /// Format the second of a fixed value - /// - Parameters: - /// - second: The template for formatting the second - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted second information - public func format(second: Template, - timeZone: Template? = nil) -> String { - return format([second, timeZone]) - } - + + /// Format the second of a fixed value + /// - Parameters: + /// - second: The template for formatting the second + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted second information + public func format( + second: Template, + timeZone: Template? = nil + ) -> String { + return format([second, timeZone]) + } + } extension Fixed where Granularity: LTOENanosecond { - - /// Format the nanosecond of a fixed value - /// - Parameters: - /// - nanosecond: The template for formatting the nanosecond - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted nanosecond information - public func format(nanosecond: Template, - timeZone: Template? = nil) -> String { - return format([nanosecond, timeZone]) - } - + + /// Format the nanosecond of a fixed value + /// - Parameters: + /// - nanosecond: The template for formatting the nanosecond + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted nanosecond information + public func format( + nanosecond: Template, + timeZone: Template? = nil + ) -> String { + return format([nanosecond, timeZone]) + } + } diff --git a/Sources/Time/8-Formatting/PartialFormatting-2Units.swift b/Sources/Time/8-Formatting/PartialFormatting-2Units.swift index 8d63fec..5e41c1f 100644 --- a/Sources/Time/8-Formatting/PartialFormatting-2Units.swift +++ b/Sources/Time/8-Formatting/PartialFormatting-2Units.swift @@ -1,101 +1,113 @@ import Foundation extension Fixed where Granularity: LTOEMonth { - - /// Format the year and month of a fixed value - /// - Parameters: - /// - year: The template for formatting the year - /// - month: The template for formatting the month - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(year: Template, - month: Template, - timeZone: Template? = nil) -> String { - return format([year, month, timeZone]) - } - + + /// Format the year and month of a fixed value + /// - Parameters: + /// - year: The template for formatting the year + /// - month: The template for formatting the month + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + year: Template, + month: Template, + timeZone: Template? = nil + ) -> String { + return format([year, month, timeZone]) + } + } extension Fixed where Granularity: LTOEDay { - - /// Format the month and day of a fixed value - /// - Parameters: - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(month: Template, - day: Template, - weekday: Template? = nil, - timeZone: Template? = nil) -> String { - return format([month, day, weekday, timeZone]) - } - + + /// Format the month and day of a fixed value + /// - Parameters: + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + month: Template, + day: Template, + weekday: Template? = nil, + timeZone: Template? = nil + ) -> String { + return format([month, day, weekday, timeZone]) + } + } extension Fixed where Granularity: LTOEHour { - - /// Format the day and hour of a fixed value - /// - Parameters: - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(day: Template, - weekday: Template? = nil, - hour: Template, - timeZone: Template? = nil) -> String { - return format([day, weekday, hour, timeZone]) - } - + + /// Format the day and hour of a fixed value + /// - Parameters: + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + day: Template, + weekday: Template? = nil, + hour: Template, + timeZone: Template? = nil + ) -> String { + return format([day, weekday, hour, timeZone]) + } + } extension Fixed where Granularity: LTOEMinute { - - /// Format the hour and minute of a fixed value - /// - Parameters: - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(hour: Template, - minute: Template, - timeZone: Template? = nil) -> String { - return format([hour, minute, timeZone]) - } - + + /// Format the hour and minute of a fixed value + /// - Parameters: + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + hour: Template, + minute: Template, + timeZone: Template? = nil + ) -> String { + return format([hour, minute, timeZone]) + } + } extension Fixed where Granularity: LTOESecond { - - /// Format the minute and second of a fixed value - /// - Parameters: - /// - minute: The template for formatting the minute - /// - second: The template for formatting the second - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(minute: Template, - second: Template, - timeZone: Template? = nil) -> String { - return format([minute, second, timeZone]) - } - + + /// Format the minute and second of a fixed value + /// - Parameters: + /// - minute: The template for formatting the minute + /// - second: The template for formatting the second + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + minute: Template, + second: Template, + timeZone: Template? = nil + ) -> String { + return format([minute, second, timeZone]) + } + } extension Fixed where Granularity: LTOENanosecond { - - /// Format the second and nanosecond of a fixed value - /// - Parameters: - /// - second: The template for formatting the second - /// - nanosecond: The template for formatting the nanosecond - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(second: Template, - nanosecond: Template, - timeZone: Template? = nil) -> String { - return format([second, nanosecond, timeZone]) - } - + + /// Format the second and nanosecond of a fixed value + /// - Parameters: + /// - second: The template for formatting the second + /// - nanosecond: The template for formatting the nanosecond + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + second: Template, + nanosecond: Template, + timeZone: Template? = nil + ) -> String { + return format([second, nanosecond, timeZone]) + } + } diff --git a/Sources/Time/8-Formatting/PartialFormatting-3Units.swift b/Sources/Time/8-Formatting/PartialFormatting-3Units.swift index 98bfe2a..a4bb5eb 100644 --- a/Sources/Time/8-Formatting/PartialFormatting-3Units.swift +++ b/Sources/Time/8-Formatting/PartialFormatting-3Units.swift @@ -1,96 +1,106 @@ import Foundation extension Fixed where Granularity: LTOEDay { - - /// Format the year, month, and day of a fixed value - /// - Parameters: - /// - year: The template for formatting the year - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - Returns: A string with the formatted date components - public func format(year: Template, - month: Template, - day: Template, - weekday: Template? = nil, - timeZone: Template? = nil) -> String { - return format([year, month, day, weekday, timeZone]) - } - + + /// Format the year, month, and day of a fixed value + /// - Parameters: + /// - year: The template for formatting the year + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - Returns: A string with the formatted date components + public func format( + year: Template, + month: Template, + day: Template, + weekday: Template? = nil, + timeZone: Template? = nil + ) -> String { + return format([year, month, day, weekday, timeZone]) + } + } extension Fixed where Granularity: LTOEHour { - - /// Format the month, day, and hour of a fixed value - /// - Parameters: - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(month: Template, - day: Template, - weekday: Template? = nil, - hour: Template, - timeZone: Template? = nil) -> String { - return format([month, day, weekday, hour, timeZone]) - } - + + /// Format the month, day, and hour of a fixed value + /// - Parameters: + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + month: Template, + day: Template, + weekday: Template? = nil, + hour: Template, + timeZone: Template? = nil + ) -> String { + return format([month, day, weekday, hour, timeZone]) + } + } extension Fixed where Granularity: LTOEMinute { - - /// Format the day, hour, and minute of a fixed value - /// - Parameters: - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(day: Template, - weekday: Template? = nil, - hour: Template, - minute: Template, - timeZone: Template? = nil) -> String { - return format([day, weekday, hour, minute, timeZone]) - } - + + /// Format the day, hour, and minute of a fixed value + /// - Parameters: + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + day: Template, + weekday: Template? = nil, + hour: Template, + minute: Template, + timeZone: Template? = nil + ) -> String { + return format([day, weekday, hour, minute, timeZone]) + } + } extension Fixed where Granularity: LTOESecond { - - /// Format the hour, minute, and second of a fixed value - /// - Parameters: - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - second: The template for formatting the second - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(hour: Template, - minute: Template, - second: Template, - timeZone: Template? = nil) -> String { - return format([hour, minute, second, timeZone]) - } - + + /// Format the hour, minute, and second of a fixed value + /// - Parameters: + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - second: The template for formatting the second + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + hour: Template, + minute: Template, + second: Template, + timeZone: Template? = nil + ) -> String { + return format([hour, minute, second, timeZone]) + } + } extension Fixed where Granularity: LTOENanosecond { - - /// Format the minute, second, and nanosecond of a fixed value - /// - Parameters: - /// - minute: The template for formatting the minute - /// - second: The template for formatting the second - /// - nanosecond: The template for formatting the nanosecond - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(minute:Template, - second: Template, - nanosecond: Template, - timeZone: Template? = nil) -> String { - return format([minute, second, nanosecond, timeZone]) - } - + + /// Format the minute, second, and nanosecond of a fixed value + /// - Parameters: + /// - minute: The template for formatting the minute + /// - second: The template for formatting the second + /// - nanosecond: The template for formatting the nanosecond + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + minute: Template, + second: Template, + nanosecond: Template, + timeZone: Template? = nil + ) -> String { + return format([minute, second, nanosecond, timeZone]) + } + } diff --git a/Sources/Time/8-Formatting/PartialFormatting-4Units.swift b/Sources/Time/8-Formatting/PartialFormatting-4Units.swift index b9b63c6..1bcc307 100644 --- a/Sources/Time/8-Formatting/PartialFormatting-4Units.swift +++ b/Sources/Time/8-Formatting/PartialFormatting-4Units.swift @@ -1,87 +1,95 @@ import Foundation extension Fixed where Granularity: LTOEHour { - - /// Format the year, month, day, and hour of a fixed value - /// - Parameters: - /// - year: The template for formatting the year - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(year: Template, - month: Template, - day: Template, - weekday: Template? = nil, - hour: Template, - timeZone: Template? = nil) -> String { - return format([year, month, day, weekday, hour, timeZone]) - } - + + /// Format the year, month, day, and hour of a fixed value + /// - Parameters: + /// - year: The template for formatting the year + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + year: Template, + month: Template, + day: Template, + weekday: Template? = nil, + hour: Template, + timeZone: Template? = nil + ) -> String { + return format([year, month, day, weekday, hour, timeZone]) + } + } extension Fixed where Granularity: LTOEMinute { - - /// Format the month, day, hour, and minute of a fixed value - /// - Parameters: - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(month: Template, - day: Template, - weekday: Template? = nil, - hour: Template, - minute: Template, - timeZone: Template? = nil) -> String { - return format([month, day, weekday, hour, minute, timeZone]) - } - + + /// Format the month, day, hour, and minute of a fixed value + /// - Parameters: + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + month: Template, + day: Template, + weekday: Template? = nil, + hour: Template, + minute: Template, + timeZone: Template? = nil + ) -> String { + return format([month, day, weekday, hour, minute, timeZone]) + } + } extension Fixed where Granularity: LTOESecond { - - /// Format the day, hour, minute, and second of a fixed value - /// - Parameters: - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - second: The template for formatting the second - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(day: Template, - weekday: Template? = nil, - hour: Template, - minute: Template, - second: Template, - timeZone: Template? = nil) -> String { - return format([day, weekday, hour, minute, second, timeZone]) - } - + + /// Format the day, hour, minute, and second of a fixed value + /// - Parameters: + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - second: The template for formatting the second + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + day: Template, + weekday: Template? = nil, + hour: Template, + minute: Template, + second: Template, + timeZone: Template? = nil + ) -> String { + return format([day, weekday, hour, minute, second, timeZone]) + } + } extension Fixed where Granularity: LTOENanosecond { - - /// Format the hour, minute, second, and nanosecond of a fixed value - /// - Parameters: - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - second: The template for formatting the second - /// - nanosecond: The template for formatting the nanosecond - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(hour: Template, - minute: Template, - second: Template, - nanosecond: Template, - timeZone: Template? = nil) -> String { - return format([hour, minute, second, nanosecond, timeZone]) - } - + + /// Format the hour, minute, second, and nanosecond of a fixed value + /// - Parameters: + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - second: The template for formatting the second + /// - nanosecond: The template for formatting the nanosecond + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + hour: Template, + minute: Template, + second: Template, + nanosecond: Template, + timeZone: Template? = nil + ) -> String { + return format([hour, minute, second, nanosecond, timeZone]) + } + } diff --git a/Sources/Time/8-Formatting/PartialFormatting-Others.swift b/Sources/Time/8-Formatting/PartialFormatting-Others.swift index 015e0b7..9b61a42 100644 --- a/Sources/Time/8-Formatting/PartialFormatting-Others.swift +++ b/Sources/Time/8-Formatting/PartialFormatting-Others.swift @@ -1,155 +1,165 @@ import Foundation extension Fixed where Granularity: LTOEMinute { - - /// Format the year, month, day, hour, and minute of a fixed value - /// - Parameters: - /// - year: The template for formatting the year - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(year: Template, - month: Template, - day: Template, - weekday: Template? = nil, - hour: Template, - minute: Template, - timeZone: Template? = nil) -> String { - return format([year, month, day, weekday, hour, minute, timeZone]) - } - + + /// Format the year, month, day, hour, and minute of a fixed value + /// - Parameters: + /// - year: The template for formatting the year + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + year: Template, + month: Template, + day: Template, + weekday: Template? = nil, + hour: Template, + minute: Template, + timeZone: Template? = nil + ) -> String { + return format([year, month, day, weekday, hour, minute, timeZone]) + } + } extension Fixed where Granularity: LTOESecond { - - /// Format the month, day, hour, minute, and second of a fixed value - /// - Parameters: - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - second: The template for formatting the second - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(month: Template, - day: Template, - weekday: Template? = nil, - hour: Template, - minute: Template, - second: Template, - timeZone: Template? = nil) -> String { - return format([month, day, weekday, hour, minute, second, timeZone]) - } - + + /// Format the month, day, hour, minute, and second of a fixed value + /// - Parameters: + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - second: The template for formatting the second + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + month: Template, + day: Template, + weekday: Template? = nil, + hour: Template, + minute: Template, + second: Template, + timeZone: Template? = nil + ) -> String { + return format([month, day, weekday, hour, minute, second, timeZone]) + } + } extension Fixed where Granularity: LTOENanosecond { - - /// Format the day, hour, minute, second, and nanosecond of a fixed value - /// - Parameters: - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - second: The template for formatting the second - /// - nanosecond: The template for formatting the nanosecond - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(day: Template, - weekday: Template? = nil, - hour: Template, - minute: Template, - second: Template, - nanosecond: Template, - timeZone: Template? = nil) -> String { - return format([day, weekday, hour, minute, second, nanosecond, timeZone]) - } - -} + /// Format the day, hour, minute, second, and nanosecond of a fixed value + /// - Parameters: + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - second: The template for formatting the second + /// - nanosecond: The template for formatting the nanosecond + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + day: Template, + weekday: Template? = nil, + hour: Template, + minute: Template, + second: Template, + nanosecond: Template, + timeZone: Template? = nil + ) -> String { + return format([day, weekday, hour, minute, second, nanosecond, timeZone]) + } + +} extension Fixed where Granularity: LTOESecond { - - /// Format the year, month, day, hour, minute, and second of a fixed value - /// - Parameters: - /// - year: The template for formatting the year - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - second: The template for formatting the second - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(year: Template, - month: Template, - day: Template, - weekday: Template? = nil, - hour: Template, - minute: Template, - second: Template, - timeZone: Template? = nil) -> String { - return format([year, month, day, weekday, hour, minute, second, timeZone]) - } - + + /// Format the year, month, day, hour, minute, and second of a fixed value + /// - Parameters: + /// - year: The template for formatting the year + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - second: The template for formatting the second + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + year: Template, + month: Template, + day: Template, + weekday: Template? = nil, + hour: Template, + minute: Template, + second: Template, + timeZone: Template? = nil + ) -> String { + return format([year, month, day, weekday, hour, minute, second, timeZone]) + } + } extension Fixed where Granularity: LTOENanosecond { - - /// Format the month, day, hour, minute, second, and nanosecond of a fixed value - /// - Parameters: - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - weekday: The template for formatting the day of the week - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - second: The template for formatting the second - /// - nanosecond: The template for formatting the nanosecond - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(month: Template, - day: Template, - weekday: Template? = nil, - hour: Template, - minute: Template, - second: Template, - nanosecond: Template, - timeZone: Template? = nil) -> String { - return format([month, day, weekday, hour, minute, second, nanosecond, timeZone]) - } - -} + /// Format the month, day, hour, minute, second, and nanosecond of a fixed value + /// - Parameters: + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - weekday: The template for formatting the day of the week + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - second: The template for formatting the second + /// - nanosecond: The template for formatting the nanosecond + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + month: Template, + day: Template, + weekday: Template? = nil, + hour: Template, + minute: Template, + second: Template, + nanosecond: Template, + timeZone: Template? = nil + ) -> String { + return format([month, day, weekday, hour, minute, second, nanosecond, timeZone]) + } + +} extension Fixed where Granularity: LTOENanosecond { - - /// Format the year, month, day, hour, minute, second, and nanosecond of a fixed value - /// - Parameters: - /// - era: The template for formatting the era - /// - year: The template for formatting the year - /// - month: The template for formatting the month - /// - day: The template for formatting the day - /// - hour: The template for formatting the hour - /// - minute: The template for formatting the minute - /// - second: The template for formatting the second - /// - nanosecond: The template for formatting the nanosecond - /// - timeZone: The template for formatting the time zone - /// - Returns: A string with the formatted date components - public func format(year: Template, - month: Template, - day: Template, - weekday: Template? = nil, - hour: Template, - minute: Template, - second: Template, - nanosecond: Template, - timeZone: Template? = nil) -> String { - return format([year, month, day, weekday, hour, minute, second, nanosecond, timeZone]) - } - + + /// Format the year, month, day, hour, minute, second, and nanosecond of a fixed value + /// - Parameters: + /// - era: The template for formatting the era + /// - year: The template for formatting the year + /// - month: The template for formatting the month + /// - day: The template for formatting the day + /// - hour: The template for formatting the hour + /// - minute: The template for formatting the minute + /// - second: The template for formatting the second + /// - nanosecond: The template for formatting the nanosecond + /// - timeZone: The template for formatting the time zone + /// - Returns: A string with the formatted date components + public func format( + year: Template, + month: Template, + day: Template, + weekday: Template? = nil, + hour: Template, + minute: Template, + second: Template, + nanosecond: Template, + timeZone: Template? = nil + ) -> String { + return format([year, month, day, weekday, hour, minute, second, nanosecond, timeZone]) + } + } diff --git a/Sources/Time/9-Parsing/Fixed+Parsing.swift b/Sources/Time/9-Parsing/Fixed+Parsing.swift index 6f7095b..bd3f847 100644 --- a/Sources/Time/9-Parsing/Fixed+Parsing.swift +++ b/Sources/Time/9-Parsing/Fixed+Parsing.swift @@ -1,20 +1,20 @@ import Foundation extension Fixed { - - /// Attempt to create a fixed value from a string and raw format - /// - Parameters: - /// - stringValue: A string to parse, such as `"2024-01-30"` - /// - rawFormat: An format string to use as the tempalte for parsing, such as `"y-MM-dd"` - /// - region: The ``Region`` to be used for parsing the string - /// - Throws: A ``TimeError`` if the string cannot be parsed using the provided format and region. - public init(stringValue: String, rawFormat: String, region: Region) throws { - let df = DateFormatter.formatter(for: rawFormat, region: region) - if let date = df.date(from: stringValue) { - self.init(region: region, date: date) - } else { - throw TimeError.cannotParseString(stringValue, formatString: rawFormat, in: region) - } + + /// Attempt to create a fixed value from a string and raw format + /// - Parameters: + /// - stringValue: A string to parse, such as `"2024-01-30"` + /// - rawFormat: An format string to use as the tempalte for parsing, such as `"y-MM-dd"` + /// - region: The ``Region`` to be used for parsing the string + /// - Throws: A ``TimeError`` if the string cannot be parsed using the provided format and region. + public init(stringValue: String, rawFormat: String, region: Region) throws { + let df = DateFormatter.formatter(for: rawFormat, region: region) + if let date = df.date(from: stringValue) { + self.init(region: region, date: date) + } else { + throw TimeError.cannotParseString(stringValue, formatString: rawFormat, in: region) } - + } + } diff --git a/Sources/Time/Internals/Assertions.swift b/Sources/Time/Internals/Assertions.swift index 53ee028..a1b49ea 100644 --- a/Sources/Time/Internals/Assertions.swift +++ b/Sources/Time/Internals/Assertions.swift @@ -1,22 +1,29 @@ import Foundation -internal func require(_ condition: @autoclosure () -> Bool, _ why: @autoclosure () -> String, file: StaticString = #fileID, line: UInt = #line) { - guard condition() == true else { - fatalError(why(), file: file, line: line) - } +internal func require( + _ condition: @autoclosure () -> Bool, _ why: @autoclosure () -> String, + file: StaticString = #fileID, line: UInt = #line +) { + guard condition() == true else { + fatalError(why(), file: file, line: line) + } } -internal extension Optional { - - func unwrap(_ why: @autoclosure () -> String, file: StaticString = #fileID, line: UInt = #line) -> Wrapped { - guard let value = self else { - fatalError(why(), file: file, line: line) - } - return value +extension Optional { + + func unwrap(_ why: @autoclosure () -> String, file: StaticString = #fileID, line: UInt = #line) + -> Wrapped + { + guard let value = self else { + fatalError(why(), file: file, line: line) } - + return value + } + } -internal func invalid(_ function: StaticString = #function, file: StaticString = #fileID, line: UInt = #line) -> Never { - fatalError("\(function) is invalid", file: file, line: line) +internal func invalid( + _ function: StaticString = #function, file: StaticString = #fileID, line: UInt = #line +) -> Never { + fatalError("\(function) is invalid", file: file, line: line) } diff --git a/Sources/Time/Internals/DateFormatterCache.swift b/Sources/Time/Internals/DateFormatterCache.swift index 8c0cf3a..b2ed9ec 100644 --- a/Sources/Time/Internals/DateFormatterCache.swift +++ b/Sources/Time/Internals/DateFormatterCache.swift @@ -1,72 +1,77 @@ import Foundation internal enum FormatConfiguration: Hashable, Sendable { - case template(String) - case raw(String) - case styles(DateFormatter.Style, DateFormatter.Style) + case template(String) + case raw(String) + case styles(DateFormatter.Style, DateFormatter.Style) } extension DateFormatter { - - internal struct Key: Hashable { - let configuration: FormatConfiguration - let calendar: Calendar - let locale: Locale - let timeZone: TimeZone - } - - internal static func formatter(for key: Key) -> DateFormatter { - return DateFormatterCache.shared.formatter(for: key) - } - - internal static func formatter(for rawFormat: String, region: Region) -> DateFormatter { - let key = Key(configuration: .raw(rawFormat), calendar: region.calendar, locale: region.locale, timeZone: region.timeZone) - return self.formatter(for: key) - } - - internal static func formatter(for templates: Array, region: Region) -> DateFormatter { - let template = templates.compactMap { $0?.template }.joined() - if template.isEmpty { fatalError("Somehow have an empty template? this should not happen") } - let key = Key(configuration: .template(template), calendar: region.calendar, locale: region.locale, timeZone: region.timeZone) - return self.formatter(for: key) - } + + internal struct Key: Hashable { + let configuration: FormatConfiguration + let calendar: Calendar + let locale: Locale + let timeZone: TimeZone + } + + internal static func formatter(for key: Key) -> DateFormatter { + return DateFormatterCache.shared.formatter(for: key) + } + + internal static func formatter(for rawFormat: String, region: Region) -> DateFormatter { + let key = Key( + configuration: .raw(rawFormat), calendar: region.calendar, locale: region.locale, + timeZone: region.timeZone) + return self.formatter(for: key) + } + + internal static func formatter(for templates: [Format?], region: Region) -> DateFormatter { + let template = templates.compactMap { $0?.template }.joined() + if template.isEmpty { fatalError("Somehow have an empty template? this should not happen") } + let key = Key( + configuration: .template(template), calendar: region.calendar, locale: region.locale, + timeZone: region.timeZone) + return self.formatter(for: key) + } } private class DateFormatterCache { - - static let shared = DateFormatterCache() - - private let lock = NSLock() - private var formatters = Dictionary() - - private init() { } - - func formatter(for key: DateFormatter.Key) -> DateFormatter { - lock.lock() - let returnValue: DateFormatter - - if let existing = formatters[key] { - returnValue = existing - } else { - let formatter = DateFormatter() - formatter.locale = key.locale - formatter.calendar = key.calendar - formatter.timeZone = key.timeZone - switch key.configuration { - case .template(let template): - formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: template, options: 0, locale: key.locale) - case .raw(let format): - formatter.dateFormat = format - case .styles(let date, let time): - formatter.dateStyle = date - formatter.timeStyle = time - } - formatters[key] = formatter - returnValue = formatter - } - - lock.unlock() - return returnValue + + static let shared = DateFormatterCache() + + private let lock = NSLock() + private var formatters = [DateFormatter.Key: DateFormatter]() + + private init() {} + + func formatter(for key: DateFormatter.Key) -> DateFormatter { + lock.lock() + let returnValue: DateFormatter + + if let existing = formatters[key] { + returnValue = existing + } else { + let formatter = DateFormatter() + formatter.locale = key.locale + formatter.calendar = key.calendar + formatter.timeZone = key.timeZone + switch key.configuration { + case .template(let template): + formatter.dateFormat = DateFormatter.dateFormat( + fromTemplate: template, options: 0, locale: key.locale) + case .raw(let format): + formatter.dateFormat = format + case .styles(let date, let time): + formatter.dateStyle = date + formatter.timeStyle = time + } + formatters[key] = formatter + returnValue = formatter } - + + lock.unlock() + return returnValue + } + } diff --git a/Sources/Time/Internals/Fixed+Internal.swift b/Sources/Time/Internals/Fixed+Internal.swift index c62f169..52ac42b 100644 --- a/Sources/Time/Internals/Fixed+Internal.swift +++ b/Sources/Time/Internals/Fixed+Internal.swift @@ -1,231 +1,248 @@ import Foundation extension Fixed { - - internal func truncated() -> Fixed { - return Fixed(region: region, instant: self.instant) - } - - internal func value(for unit: Calendar.Component) -> Int { - return dateComponents.value(for: unit).unwrap("A Fixed<\(Granularity.self)> does not contain a represented \(unit)") + + internal func truncated() -> Fixed { + return Fixed(region: region, instant: self.instant) + } + + internal func value(for unit: Calendar.Component) -> Int { + return dateComponents.value(for: unit).unwrap( + "A Fixed<\(Granularity.self)> does not contain a represented \(unit)") + } + + internal var approximateMidPoint: Instant { + let r = self.range + let lower = r.lowerBound + let upper = r.upperBound.converted(to: lower.epoch) + let duration = upper.intervalSinceEpoch - lower.intervalSinceEpoch + let midPoint = lower + (duration / 2.0) + return max(lower, midPoint) + } + + internal func value(for unit: U.Type) -> Int? { + guard representedComponents.contains(U.component) else { return nil } + return dateComponents.value(for: U.component) + } + + internal func first() -> Fixed { + return Fixed(region: region, instant: range.lowerBound) + } + + internal func last() -> Fixed { + return Fixed(region: region, instant: range.upperBound) + - TimeDifference(value: 1, unit: U.component) + } + + internal func nth(_ ordinal: Int) throws -> Fixed { + let target = DateComponents(value: ordinal, component: U.component) + + guard ordinal >= 1 else { + throw TimeError.invalidDateComponents( + target, in: region, description: "Invalid ordinal of \(ordinal) \(U.component)") } - - internal var approximateMidPoint: Instant { - let r = self.range - let lower = r.lowerBound - let upper = r.upperBound.converted(to: lower.epoch) - let duration = upper.intervalSinceEpoch - lower.intervalSinceEpoch - let midPoint = lower + (duration / 2.0) - return max(lower, midPoint) + + let offset: Fixed = first() + TimeDifference(value: ordinal - 1, unit: U.component) + + let parentRange = self.range + let childRange = offset.range + + guard parentRange.lowerBound <= childRange.lowerBound else { + throw TimeError.invalidDateComponents( + target, in: region, + description: "\(U.component) #\(ordinal) is outside the range of \(self)") } - - internal func value(for unit: U.Type) -> Int? { - guard representedComponents.contains(U.component) else { return nil } - return dateComponents.value(for: U.component) + + guard childRange.upperBound <= parentRange.upperBound else { + throw TimeError.invalidDateComponents( + target, in: region, + description: "\(U.component) #\(ordinal) is outside the range of \(self)") } - - internal func first() -> Fixed { - return Fixed(region: region, instant: range.lowerBound) + + return offset + } + + internal func numbered(_ number: Int) -> Fixed? { + guard let potential: Fixed = try? nth(number - 1) else { return nil } + guard let value = potential.value(for: U.self) else { return nil } + if value == number { return potential } + + let incrementing = (value < number) + + let delta = TimeDifference(value: incrementing ? 1 : -1, unit: U.component) + let tooFar: (Fixed) -> Bool = { + let value = $0.value(for: U.self)! + if incrementing { return value > number } + return value < number } - - internal func last() -> Fixed { - return Fixed(region: region, instant: range.upperBound) - TimeDifference(value: 1, unit: U.component) + + var current = potential + while true { + let next = current.applying(difference: delta) + if next.value(for: U.self) == number { return next } + if tooFar(next) { break } + current = next } - - internal func nth(_ ordinal: Int) throws -> Fixed { - let target = DateComponents(value: ordinal, component: U.component) - - guard ordinal >= 1 else { - throw TimeError.invalidDateComponents(target, in: region, description: "Invalid ordinal of \(ordinal) \(U.component)") - } - - let offset: Fixed = first() + TimeDifference(value: ordinal - 1, unit: U.component) - - let parentRange = self.range - let childRange = offset.range - - guard parentRange.lowerBound <= childRange.lowerBound else { - throw TimeError.invalidDateComponents(target, in: region, description: "\(U.component) #\(ordinal) is outside the range of \(self)") - } - - guard childRange.upperBound <= parentRange.upperBound else { - throw TimeError.invalidDateComponents(target, in: region, description: "\(U.component) #\(ordinal) is outside the range of \(self)") - } - - return offset + return nil + } + + internal func computeDifference(to other: Fixed) + -> TimeDifference + { + + let units = Calendar.Component.from(lower: Min.self, to: Max.self) + let difference = calendar.dateComponents( + units, + from: self.firstInstant.date, + to: other.firstInstant.date) + return TimeDifference(difference) + } + + internal func roundEra(direction: RoundingDirection) -> Self { + // for gregorian calendars, this returns 0 ..< 2 + guard let maxRange = self.calendar.maximumRange(of: .era) else { return self } + + // working with a closed range (0 ... 1) is much nicer + let closedMaxRange = maxRange.lowerBound...(maxRange.upperBound - 1) + + let thisRangeStart = self.firstInstant + + if self.era == closedMaxRange.upperBound { + // there's no way to round up, so we MUST round down + return Self(region: self.region, instant: thisRangeStart) } - - internal func numbered(_ number: Int) -> Fixed? { - guard let potential: Fixed = try? nth(number - 1) else { return nil } - guard let value = potential.value(for: U.self) else { return nil } - if value == number { return potential } - - let incrementing = (value < number) - - let delta = TimeDifference(value: incrementing ? 1 : -1, unit: U.component) - let tooFar: (Fixed) -> Bool = { - let value = $0.value(for: U.self)! - if incrementing { return value > number } - return value < number - } - - var current = potential - while true { - let next = current.applying(difference: delta) - if next.value(for: U.self) == number { return next } - if tooFar(next) { break } - current = next - } - return nil + + return round(to: Era.self, direction: direction) + } + + internal func round(to unit: U.Type = U.self, direction: RoundingDirection) -> Self { + guard let maxRange = self.calendar.maximumRange(of: unit.component) else { return self } + + let roundedDown: Fixed = self.truncated() + let roundedDownStart = roundedDown.firstInstant + + if unit == Era.self { + let closedMaxRange = maxRange.lowerBound...(maxRange.upperBound - 1) + + if self.value(for: unit) == closedMaxRange.upperBound { + return Self(region: self.region, instant: roundedDownStart) + } } - - internal func computeDifference(to other: Fixed) -> TimeDifference { - - let units = Calendar.Component.from(lower: Min.self, to: Max.self) - let difference = calendar.dateComponents(units, - from: self.firstInstant.date, - to: other.firstInstant.date) - return TimeDifference(difference) + + if direction == .backward { + return Self(region: self.region, instant: roundedDownStart) } - - internal func roundEra(direction: RoundingDirection) -> Self { - // for gregorian calendars, this returns 0 ..< 2 - guard let maxRange = self.calendar.maximumRange(of: .era) else { return self } - - // working with a closed range (0 ... 1) is much nicer - let closedMaxRange = maxRange.lowerBound ... (maxRange.upperBound - 1) - - let thisRangeStart = self.firstInstant - - if self.era == closedMaxRange.upperBound { - // there's no way to round up, so we MUST round down - return Self(region: self.region, instant: thisRangeStart) - } - - return round(to: Era.self, direction: direction) + + let roundedUp: Fixed = roundedDown.next + let roundedUpStart = roundedUp.firstInstant + if direction == .forward { + return Self(region: self.region, instant: roundedUpStart) } - - internal func round(to unit: U.Type = U.self, direction: RoundingDirection) -> Self { - guard let maxRange = self.calendar.maximumRange(of: unit.component) else { return self } - - let roundedDown: Fixed = self.truncated() - let roundedDownStart = roundedDown.firstInstant - - if unit == Era.self { - let closedMaxRange = maxRange.lowerBound ... (maxRange.upperBound - 1) - - if self.value(for: unit) == closedMaxRange.upperBound { - return Self(region: self.region, instant: roundedDownStart) - } - } - - if direction == .backward { - return Self(region: self.region, instant: roundedDownStart) - } - - let roundedUp: Fixed = roundedDown.next - let roundedUpStart = roundedUp.firstInstant - if direction == .forward { - return Self(region: self.region, instant: roundedUpStart) - } - - if roundedDownStart == roundedUpStart { - return Self(region: self.region, instant: roundedDownStart) - } - - let thisStart = self.approximateMidPoint - - let intervalToLowerDown = (thisStart - roundedDownStart).magnitude - let intervalToUpStart = (roundedUpStart - thisStart).magnitude - - let nearestStart = (intervalToLowerDown <= intervalToUpStart) ? roundedDownStart : roundedUpStart - return Self(region: self.region, instant: nearestStart) + + if roundedDownStart == roundedUpStart { + return Self(region: self.region, instant: roundedDownStart) } - - internal func roundToMultiple(of match: TimeDifference, direction: RoundingDirection) -> Self where Granularity: LTOEYear { - /* + + let thisStart = self.approximateMidPoint + + let intervalToLowerDown = (thisStart - roundedDownStart).magnitude + let intervalToUpStart = (roundedUpStart - thisStart).magnitude + + let nearestStart = + (intervalToLowerDown <= intervalToUpStart) ? roundedDownStart : roundedUpStart + return Self(region: self.region, instant: nearestStart) + } + + internal func roundToMultiple( + of match: TimeDifference, direction: RoundingDirection + ) -> Self where Granularity: LTOEYear { + /* PREMISE: - figure out the smallest represented unit in the matching components - that becomes the unit that we start iterating - iteration starts from the top of the (smallest unit) - we iterate until we find the value just before self and the value just after self - + NOTE: - this iteration is NOT typical iteration. For example if we're finding the nearest "13 minutes", then we'll iterate and check :00, :13, :26, :39, :52, :00, :13 ... etc - + ALSO: - github issue #70 tracks improving the performance of this method */ - - let represented = match.dateComponents.representedComponents - guard let smallest = Calendar.Component.ascendingOrder.first(where: { represented.contains($0) }) else { - // throw? - return self - } - - guard let nextLargest = smallest.nextLargest else { - // this should always succeed, since this unit is LTOEYear; at least the ERA should always be larger - fatalError("Unable to determine next unit larger from \(smallest)") - } - - let baseIterationRange = self.calendar.range(of: nextLargest, containing: self.instant.date) - - let iterationStart = Self(region: self.region, date: baseIterationRange.lowerBound) - - let stride: TimeDifference - let boundaryStride = TimeDifference(value: 1, unit: nextLargest) - - if represented.count == 1 { - // this is the simplest case where we can do direct iteration by the specified stride - stride = match - } else { - stride = TimeDifference(DateComponents(value: 1, component: smallest)) - } - - let sequence = BoundaryAlignedSequence(start: iterationStart, stride: stride, boundaryStride: boundaryStride) - - var smaller: Self? - var larger: Self? - - for option in sequence { - - if option <= self { smaller = option } - if option >= self { larger = option } - - if smaller != nil && larger != nil { break } - } - - if direction == .backward { - return smaller! - } else if direction == .forward { - return larger! - } else { - let differenceToSmaller = self.firstInstant - smaller!.firstInstant - let differenceToLarger = larger!.firstInstant - self.firstInstant - - if differenceToSmaller < differenceToLarger { - return smaller! - } else { - return larger! - } - } + + let represented = match.dateComponents.representedComponents + guard + let smallest = Calendar.Component.ascendingOrder.first(where: { represented.contains($0) }) + else { + // throw? + return self } - - internal func format(_ style: FixedFormat) -> String { - let key = DateFormatter.Key(configuration: style.configuration, - calendar: self.region.calendar, - locale: self.region.locale, - timeZone: self.region.timeZone) - - let formatter = DateFormatter.formatter(for: key) - - return formatter.string(from: self.dateForFormatting()) + + guard let nextLargest = smallest.nextLargest else { + // this should always succeed, since this unit is LTOEYear; at least the ERA should always be larger + fatalError("Unable to determine next unit larger from \(smallest)") } - - internal func format(_ templates: Array) -> String { - let style = FixedFormat(templates: templates) - return format(style) + + let baseIterationRange = self.calendar.range(of: nextLargest, containing: self.instant.date) + + let iterationStart = Self(region: self.region, date: baseIterationRange.lowerBound) + + let stride: TimeDifference + let boundaryStride = TimeDifference(value: 1, unit: nextLargest) + + if represented.count == 1 { + // this is the simplest case where we can do direct iteration by the specified stride + stride = match + } else { + stride = TimeDifference(DateComponents(value: 1, component: smallest)) } - + + let sequence = BoundaryAlignedSequence( + start: iterationStart, stride: stride, boundaryStride: boundaryStride) + + var smaller: Self? + var larger: Self? + + for option in sequence { + + if option <= self { smaller = option } + if option >= self { larger = option } + + if smaller != nil && larger != nil { break } + } + + if direction == .backward { + return smaller! + } else if direction == .forward { + return larger! + } else { + let differenceToSmaller = self.firstInstant - smaller!.firstInstant + let differenceToLarger = larger!.firstInstant - self.firstInstant + + if differenceToSmaller < differenceToLarger { + return smaller! + } else { + return larger! + } + } + } + + internal func format(_ style: FixedFormat) -> String { + let key = DateFormatter.Key( + configuration: style.configuration, + calendar: self.region.calendar, + locale: self.region.locale, + timeZone: self.region.timeZone) + + let formatter = DateFormatter.formatter(for: key) + + return formatter.string(from: self.dateForFormatting()) + } + + internal func format(_ templates: [Format?]) -> String { + let style = FixedFormat(templates: templates) + return format(style) + } + } diff --git a/Sources/Time/Internals/Formatting.swift b/Sources/Time/Internals/Formatting.swift index b980a31..480abb9 100644 --- a/Sources/Time/Internals/Formatting.swift +++ b/Sources/Time/Internals/Formatting.swift @@ -1,56 +1,56 @@ import Foundation internal protocol Format { - var template: String { get } + var template: String { get } } extension Fixed { - - static func naturalFormats(in calendar: Calendar) -> Array { - var f = Array() - - let order = Calendar.Component.descendingOrder - let represented = Self.representedComponents - var hasTime = false - - for unit in order { - guard represented.contains(unit) else { continue } - switch unit { - case .era: - if calendar.isEraRelevant { f.append(Template.abbreviated) } - case .year: - f.append(Template.naturalDigits) - case .month: - f.append(Template.naturalName) - case .day: - f.append(Template.naturalName) - f.append(Template.naturalDigits) - case .hour: - f.append(Template.naturalDigits) - hasTime = true - case .minute: - f.append(Template.naturalDigits) - hasTime = true - case .second: - f.append(Template.naturalDigits) - hasTime = true - case .nanosecond: - f.append(Template.digits(4)) - hasTime = true - default: - continue - } - } - - if hasTime { - f.append(Template.shortSpecific) - } - - return f + + static func naturalFormats(in calendar: Calendar) -> [Format?] { + var f = [Format?]() + + let order = Calendar.Component.descendingOrder + let represented = Self.representedComponents + var hasTime = false + + for unit in order { + guard represented.contains(unit) else { continue } + switch unit { + case .era: + if calendar.isEraRelevant { f.append(Template.abbreviated) } + case .year: + f.append(Template.naturalDigits) + case .month: + f.append(Template.naturalName) + case .day: + f.append(Template.naturalName) + f.append(Template.naturalDigits) + case .hour: + f.append(Template.naturalDigits) + hasTime = true + case .minute: + f.append(Template.naturalDigits) + hasTime = true + case .second: + f.append(Template.naturalDigits) + hasTime = true + case .nanosecond: + f.append(Template.digits(4)) + hasTime = true + default: + continue + } } - - internal func dateForFormatting() -> Date { - return firstInstant.date + + if hasTime { + f.append(Template.shortSpecific) } - + + return f + } + + internal func dateForFormatting() -> Date { + return firstInstant.date + } + } diff --git a/Sources/Time/Internals/ParsedFormat.swift b/Sources/Time/Internals/ParsedFormat.swift index baf264f..e57218d 100644 --- a/Sources/Time/Internals/ParsedFormat.swift +++ b/Sources/Time/Internals/ParsedFormat.swift @@ -1,113 +1,118 @@ import Foundation internal struct ParsedFormat { - - enum FormatComponent { - case literal(String) - case format(Character, Int) - case template(Character, Int) - - var isTemplate: Bool { - if case .template = self { return true } - return false - } - - var unit: Calendar.Component? { - switch self { - case .literal: - return nil - case .format(let char, _): - return Calendar.Component(formatCharacter: char).unwrap("Somehow got an invalid format character while parsing?") - case .template(let char, _): - return Calendar.Component(formatCharacter: char).unwrap("Somehow got an invalid template character while parsing?") - } + + enum FormatComponent { + case literal(String) + case format(Character, Int) + case template(Character, Int) + + var isTemplate: Bool { + if case .template = self { return true } + return false + } + + var unit: Calendar.Component? { + switch self { + case .literal: + return nil + case .format(let char, _): + return Calendar.Component(formatCharacter: char).unwrap( + "Somehow got an invalid format character while parsing?") + case .template(let char, _): + return Calendar.Component(formatCharacter: char).unwrap( + "Somehow got an invalid template character while parsing?") + } + } + + func compact(with subsequent: FormatComponent) -> (FormatComponent, FormatComponent?) { + switch (self, subsequent) { + case (.literal(let left), .literal(let right)): + return (.literal(left + right), nil) + + case (.format(let leftChar, let leftCount), .format(let rightChar, let rightCount)): + if leftChar == rightChar { + return (.format(leftChar, leftCount + rightCount), nil) } - - func compact(with subsequent: FormatComponent) -> (FormatComponent, FormatComponent?) { - switch (self, subsequent) { - case (.literal(let left), .literal(let right)): - return (.literal(left + right), nil) - - case (.format(let leftChar, let leftCount), .format(let rightChar, let rightCount)): - if leftChar == rightChar { - return (.format(leftChar, leftCount + rightCount), nil) - } - break - - case (.template(let leftChar, let leftCount), .template(let rightChar, let rightCount)): - if leftChar == rightChar { - return (.template(leftChar, leftCount + rightCount), nil) - } - break - - default: - break - } - return (self, subsequent) + break + + case (.template(let leftChar, let leftCount), .template(let rightChar, let rightCount)): + if leftChar == rightChar { + return (.template(leftChar, leftCount + rightCount), nil) } + break + + default: + break + } + return (self, subsequent) } - - var components: Array - - var isTemplate: Bool { components.contains(where: \.isTemplate) } - - init(formatString: String) throws { - var components = Array() - - var isEscaped = false - var previousChar: Character? - - for currentChar in formatString { - if currentChar == SingleQuote { - if isEscaped && previousChar == SingleQuote { - // double-escaped single-quote - components.append(.literal(String(SingleQuote))) - } - isEscaped.toggle() - } else if isEscaped { - components.append(.literal(String(currentChar))) - } else { - if templateCharacters.contains(currentChar) { - // it's a template character - components.append(.template(currentChar, 1)) - } else if formatCharacters.contains(currentChar) { - // it's a format character - components.append(.format(currentChar, 1)) - } else { - // it's a literal character - components.append(.literal(String(currentChar))) - } - } - - previousChar = currentChar + } + + var components: [FormatComponent] + + var isTemplate: Bool { components.contains(where: \.isTemplate) } + + init(formatString: String) throws { + var components = [FormatComponent]() + + var isEscaped = false + var previousChar: Character? + + for currentChar in formatString { + if currentChar == SingleQuote { + if isEscaped && previousChar == SingleQuote { + // double-escaped single-quote + components.append(.literal(String(SingleQuote))) } - - var compacted = Array() - - if var current = components.first { - for next in components.dropFirst() { - let (left, right) = current.compact(with: next) - - if let right { - compacted.append(left) - current = right - } else { - current = left - } - } - - compacted.append(current) + isEscaped.toggle() + } else if isEscaped { + components.append(.literal(String(currentChar))) + } else { + if templateCharacters.contains(currentChar) { + // it's a template character + components.append(.template(currentChar, 1)) + } else if formatCharacters.contains(currentChar) { + // it's a format character + components.append(.format(currentChar, 1)) + } else { + // it's a literal character + components.append(.literal(String(currentChar))) } - - if isEscaped || compacted.isEmpty { - // if we still think things are escaped, then we have imbalanced quotes and an invalid format string - throw TimeError.invalidFormatString(formatString) + } + + previousChar = currentChar + } + + var compacted = [FormatComponent]() + + if var current = components.first { + for next in components.dropFirst() { + let (left, right) = current.compact(with: next) + + if let right { + compacted.append(left) + current = right + } else { + current = left } - - self.components = compacted + } + + compacted.append(current) } + + if isEscaped || compacted.isEmpty { + // if we still think things are escaped, then we have imbalanced quotes and an invalid format string + throw TimeError.invalidFormatString(formatString) + } + + self.components = compacted + } } private let templateCharacters: Set = ["j", "J", "C"] -private let formatCharacters: Set = ["G", "y", "Y", "u", "U", "r", "Q", "q", "M", "L", "w", "W", "d", "D", "F", "g", "E", "e", "c", "a", "b", "B", "h", "H", "k", "K", "m", "s", "S", "A", "z", "Z", "O", "v", "V", "X", "x"] +private let formatCharacters: Set = [ + "G", "y", "Y", "u", "U", "r", "Q", "q", "M", "L", "w", "W", "d", "D", "F", "g", "E", "e", "c", + "a", "b", "B", "h", "H", "k", "K", "m", "s", "S", "A", "z", "Z", "O", "v", "V", "X", "x", +] private let SingleQuote: Character = "'" diff --git a/Sources/Time/Internals/Region+Equivalence.swift b/Sources/Time/Internals/Region+Equivalence.swift index ddd3c86..ea32c90 100644 --- a/Sources/Time/Internals/Region+Equivalence.swift +++ b/Sources/Time/Internals/Region+Equivalence.swift @@ -1,11 +1,10 @@ import Foundation extension Region { - - func isEquivalent(to other: Region) -> Bool { - return calendar.isEquivalent(to: other.calendar) && - timeZone.isEquivalent(to: other.timeZone) && - locale.isEquivalent(to: other.locale) - } - + + func isEquivalent(to other: Region) -> Bool { + return calendar.isEquivalent(to: other.calendar) && timeZone.isEquivalent(to: other.timeZone) + && locale.isEquivalent(to: other.locale) + } + } diff --git a/Sources/Time/Internals/RegionalClock+Internal.swift b/Sources/Time/Internals/RegionalClock+Internal.swift index 3cbf947..4f691a8 100644 --- a/Sources/Time/Internals/RegionalClock+Internal.swift +++ b/Sources/Time/Internals/RegionalClock+Internal.swift @@ -1,66 +1,74 @@ -import Foundation import Dispatch +import Foundation internal class CancellationToken { - - private let lock = NSLock() - private var _isCancelled = false - - var isCancelled: Bool { - lock.lock() - let c = self._isCancelled - lock.unlock() - return c - } - - func cancel() { - lock.lock() - self._isCancelled = true - lock.unlock() - } - + + private let lock = NSLock() + private var _isCancelled = false + + var isCancelled: Bool { + lock.lock() + let c = self._isCancelled + lock.unlock() + return c + } + + func cancel() { + lock.lock() + self._isCancelled = true + lock.unlock() + } + } +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) extension RegionalClock { - - internal func sleep(until deadline: Instant, tolerance: Instant.Duration?, token: CancellationToken?) async throws { - // this is in an infinite loop to account for the tolerance causing the clock to wake up - // BEFORE the desired time has been hit on the clock - repeat { - let clockNow = self.now - let timeUntilInstant = deadline - clockNow - // timeUntilInstant is how long we need to wait relative to the passed-in clock - // however, that clock may have a different flow-rate of time relative to the system clock - // and we need to reconcile the two - - let realTimeToWait = timeUntilInstant / self.SISecondsPerClockSecond - let realTolerance = tolerance.map { $0 / self.SISecondsPerClockSecond } - - if realTimeToWait <= .zero { - return - } - - // we care about the passage of real time, which happens regardless of whether the process is suspended or not - // therefore we calculate the REAL time to wait based on a continuous clock - let realTimeClock = ContinuousClock() - try await Task.sleep(until: realTimeClock.now + realTimeToWait.rawValue, - tolerance: realTolerance?.rawValue, - clock: realTimeClock) - - } while token?.isCancelled != true - } - - @discardableResult - internal func wait(until instant: Instant, tolerance: Instant.Duration?, strike: @escaping () -> Void) -> CancellationToken { - let token = CancellationToken() - Task { - do { - try await self.sleep(until: instant, tolerance: tolerance, token: token) - } catch { } - if token.isCancelled == false { strike() } - } - return token + internal func sleep( + until deadline: Instant, tolerance: Instant.Duration?, token: CancellationToken? + ) async throws { + // this is in an infinite loop to account for the tolerance causing the clock to wake up + // BEFORE the desired time has been hit on the clock + repeat { + let clockNow = self.now + let timeUntilInstant = deadline - clockNow + + // timeUntilInstant is how long we need to wait relative to the passed-in clock + // however, that clock may have a different flow-rate of time relative to the system clock + // and we need to reconcile the two + + let realTimeToWait = timeUntilInstant / self.SISecondsPerClockSecond + let realTolerance = tolerance.map { $0 / self.SISecondsPerClockSecond } + + if realTimeToWait <= .zero { + return + } + + // we care about the passage of real time, which happens regardless of whether the process is suspended or not + // therefore we calculate the REAL time to wait based on a continuous clock + let realTimeClock = ContinuousClock() + + try await Task.sleep( + until: realTimeClock.now + realTimeToWait.rawValue.realDuration, + tolerance: realTolerance?.rawValue.realDuration, + clock: realTimeClock + ) + + } while token?.isCancelled != true + } + + @discardableResult + internal func wait( + until instant: Instant, tolerance: Instant.Duration?, strike: @escaping () -> Void + ) -> CancellationToken { + let token = CancellationToken() + Task { + do { + try await self.sleep(until: instant, tolerance: tolerance, token: token) + } catch {} + if token.isCancelled == false { strike() } } - + return token + } + } diff --git a/Sources/Time/Internals/SimpleCache.swift b/Sources/Time/Internals/SimpleCache.swift index be6766d..a17122d 100644 --- a/Sources/Time/Internals/SimpleCache.swift +++ b/Sources/Time/Internals/SimpleCache.swift @@ -1,48 +1,48 @@ import Foundation extension Locale { - private static let cache = SimpleCache() - - static func standard(_ id: String) -> Locale { - return cache.get(id, create: { Locale(identifier: id) }) - } + private static let cache = SimpleCache() + + static func standard(_ id: String) -> Locale { + return cache.get(id, create: { Locale(identifier: id) }) + } } extension Calendar { - private static let cache = SimpleCache() - - static func standard(_ id: Calendar.Identifier) -> Calendar { - return cache.get(id, create: { Calendar(identifier: id) }) - } + private static let cache = SimpleCache() + + static func standard(_ id: Calendar.Identifier) -> Calendar { + return cache.get(id, create: { Calendar(identifier: id) }) + } } extension TimeZone { - private static let cache = SimpleCache() - - static func standard(_ id: String) -> TimeZone { - return cache.get(id, create: { TimeZone(identifier: id)! }) - } + private static let cache = SimpleCache() + + static func standard(_ id: String) -> TimeZone { + return cache.get(id, create: { TimeZone(identifier: id)! }) + } } private class SimpleCache { - - private var storage = Dictionary() - private let lock = NSLock() - - init() { } - - func get(_ id: Key, create: () -> T) -> T { - lock.lock() - - let returnValue: T - if let existing = storage[id] { - returnValue = existing - } else { - returnValue = create() - storage[id] = returnValue - } - - lock.unlock() - return returnValue + + private var storage = [Key: T]() + private let lock = NSLock() + + init() {} + + func get(_ id: Key, create: () -> T) -> T { + lock.lock() + + let returnValue: T + if let existing = storage[id] { + returnValue = existing + } else { + returnValue = create() + storage[id] = returnValue } + + lock.unlock() + return returnValue + } } diff --git a/Sources/Time/Internals/Snapshot.swift b/Sources/Time/Internals/Snapshot.swift index 7ce5c64..9de3b6b 100644 --- a/Sources/Time/Internals/Snapshot.swift +++ b/Sources/Time/Internals/Snapshot.swift @@ -1,121 +1,134 @@ import Foundation extension Locale { - - private static let currentSnapshot: Snapshot = Snapshot(notification: NSLocale.currentLocaleDidChangeNotification, createSnapshot: { - let auto = Locale.autoupdatingCurrent - - let standard = Locale.standard(auto.identifier) - - #if os(Linux) + + private static let currentSnapshot: Snapshot = Snapshot( + notification: NSLocale.currentLocaleDidChangeNotification, + createSnapshot: { + let auto = Locale.autoupdatingCurrent + + let standard = Locale.standard(auto.identifier) + + #if os(Linux) return standard - #else - if auto.isEquivalent(to: standard) { return standard } - var components = Locale.Components() - - components.calendar = auto.calendar.identifier - components.firstDayOfWeek = auto.firstDayOfWeek - components.hourCycle = auto.hourCycle - components.languageComponents = .init(languageCode: auto.language.languageCode, - script: auto.language.script, - region: auto.language.region) - components.measurementSystem = auto.measurementSystem - components.numberingSystem = auto.numberingSystem - components.timeZone = auto.timeZone - components.variant = auto.variant - - return Locale(components: components) - #endif + #else + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) { + if auto.isEquivalent(to: standard) { return standard } + var components = Locale.Components() + + components.calendar = auto.calendar.identifier + components.firstDayOfWeek = auto.firstDayOfWeek + components.hourCycle = auto.hourCycle + components.languageComponents = .init( + languageCode: auto.language.languageCode, + script: auto.language.script, + region: auto.language.region) + components.measurementSystem = auto.measurementSystem + components.numberingSystem = auto.numberingSystem + components.timeZone = auto.timeZone + components.variant = auto.variant + + return Locale(components: components) + } else { + return standard + } + #endif }) - - func snapshot(forcedCopy: Bool) -> Self { - if forcedCopy == false && self != .autoupdatingCurrent { return self } - return Self.currentSnapshot.snapshot(forcedCopy: forcedCopy) - } + + func snapshot(forcedCopy: Bool) -> Self { + if forcedCopy == false && self != .autoupdatingCurrent { return self } + return Self.currentSnapshot.snapshot(forcedCopy: forcedCopy) + } } extension TimeZone { - - private static let currentSnapshot: Snapshot = Snapshot(notification: .NSSystemTimeZoneDidChange, createSnapshot: { - return TimeZone.standard(TimeZone.autoupdatingCurrent.identifier) + + private static let currentSnapshot: Snapshot = Snapshot( + notification: .NSSystemTimeZoneDidChange, + createSnapshot: { + return TimeZone.standard(TimeZone.autoupdatingCurrent.identifier) }) - - func snapshot(forcedCopy: Bool) -> Self { - if forcedCopy == false && self != .autoupdatingCurrent { return self } - return Self.currentSnapshot.snapshot(forcedCopy: forcedCopy) - } - + + func snapshot(forcedCopy: Bool) -> Self { + if forcedCopy == false && self != .autoupdatingCurrent { return self } + return Self.currentSnapshot.snapshot(forcedCopy: forcedCopy) + } + } extension Calendar { - - private static let currentSnapshot: Snapshot = Snapshot(notification: NSLocale.currentLocaleDidChangeNotification, createSnapshot: { - let auto = Calendar.autoupdatingCurrent - - let standard = Calendar.standard(auto.identifier) - if auto.isEquivalent(to: standard) { - return standard - } - - var snapshot = Calendar(identifier: auto.identifier) - - // don't bother snapshotting the time zone and locale, - // because the values in the region itself take precedence - - snapshot.firstWeekday = auto.firstWeekday - snapshot.minimumDaysInFirstWeek = auto.minimumDaysInFirstWeek - - return snapshot + + private static let currentSnapshot: Snapshot = Snapshot( + notification: NSLocale.currentLocaleDidChangeNotification, + createSnapshot: { + let auto = Calendar.autoupdatingCurrent + + let standard = Calendar.standard(auto.identifier) + if auto.isEquivalent(to: standard) { + return standard + } + + var snapshot = Calendar(identifier: auto.identifier) + + // don't bother snapshotting the time zone and locale, + // because the values in the region itself take precedence + + snapshot.firstWeekday = auto.firstWeekday + snapshot.minimumDaysInFirstWeek = auto.minimumDaysInFirstWeek + + return snapshot }) - - func snapshot(forcedCopy: Bool) -> Self { - if forcedCopy == false && self != .autoupdatingCurrent { return self } - return Self.currentSnapshot.snapshot(forcedCopy: forcedCopy) - } - + + func snapshot(forcedCopy: Bool) -> Self { + if forcedCopy == false && self != .autoupdatingCurrent { return self } + return Self.currentSnapshot.snapshot(forcedCopy: forcedCopy) + } + } private class Snapshot: @unchecked Sendable { - - private let createSnapshot: () -> T - - private var _snapshot: T? - private var observationToken: NSObjectProtocol? - private let lock = NSLock() - - func snapshot(forcedCopy: Bool) -> T { - if forcedCopy { return createSnapshot() } - - lock.lock() - let returnValue: T - if let _snapshot { - returnValue = _snapshot - } else { - returnValue = createSnapshot() - _snapshot = returnValue - } - lock.unlock() - return returnValue - } - - init(notification: Notification.Name, createSnapshot: @escaping () -> T) { - self.createSnapshot = createSnapshot - self.observationToken = NotificationCenter.default - .addObserver(forName: notification, object: nil, queue: .main, using: { [unowned self] _ in - self.resetSnapshot() - }) - } - - deinit { - if let observationToken { - NotificationCenter.default.removeObserver(observationToken) - } + + private let createSnapshot: () -> T + + private var _snapshot: T? + private var observationToken: NSObjectProtocol? + private let lock = NSLock() + + func snapshot(forcedCopy: Bool) -> T { + if forcedCopy { return createSnapshot() } + + lock.lock() + let returnValue: T + if let _snapshot { + returnValue = _snapshot + } else { + returnValue = createSnapshot() + _snapshot = returnValue } - - private func resetSnapshot() { - lock.lock() - _snapshot = nil - lock.unlock() + lock.unlock() + return returnValue + } + + init(notification: Notification.Name, createSnapshot: @escaping () -> T) { + self.createSnapshot = createSnapshot + self.observationToken = NotificationCenter.default + .addObserver( + forName: notification, object: nil, queue: .main, + using: { [unowned self] _ in + self.resetSnapshot() + }) + } + + deinit { + if let observationToken { + NotificationCenter.default.removeObserver(observationToken) } - + } + + private func resetSnapshot() { + lock.lock() + _snapshot = nil + lock.unlock() + } + } diff --git a/Sources/Time/Internals/Time+Calendar.swift b/Sources/Time/Internals/Time+Calendar.swift index 872b4ca..e32d2cb 100644 --- a/Sources/Time/Internals/Time+Calendar.swift +++ b/Sources/Time/Internals/Time+Calendar.swift @@ -1,182 +1,188 @@ import Foundation extension Calendar { - - /// Different calendars may have different definitions of what a "second" is. - /// For example, on Earth, calendars all have the convention that one calendar-second - /// is the same as one SI Second. However, on Mars, the days are slightly longer, - /// which means that dividing the slightly-longer day in to 86,400 slices results - /// in "seconds" that are slightly longer than Earth seconds. - /// Therefore, to accommodate this, the calendar needs to define how many - /// SI Seconds are in each calendar-second. - /// note: This does NOT affect how physics calculations are done (or velocities, etc) - /// because those are all defined relative to SI Seconds. - internal var SISecondsPerSecond: Double { return 1.0 } - - /// For most calendars, the Era is not very relevant. For example "2019" is unambiguously - /// understood to be "2019 CE", not "2019 BCE". However, there are some calendars - /// (most notably the Japanese calendar) for which the era is extremely relevant. - /// The relevancy of the era is taken into account when doing default formatting - /// of calendar Values. - internal var isEraRelevant: Bool { return (maximumRange(of: .era)?.upperBound ?? 0) > 2 } - - internal var lenientUnitsForFixedTimePeriods: Set { - if isEraRelevant { return [] } - return [.era] - } - - internal func exactDate(from components: DateComponents, in timeZone: TimeZone, matching: Set) throws -> (Date, DateComponents) { - var restrictedComponents = try components.requireAndRestrict(to: matching, lenient: self.lenientUnitsForFixedTimePeriods) - restrictedComponents.timeZone = timeZone - - guard let proposedDate = self.date(from: restrictedComponents) else { - let r = Region(calendar: self, timeZone: timeZone, locale: self.locale ?? .current) - throw TimeError.invalidDateComponents(restrictedComponents, in: r) - } - - let proposedComponents = self.dateComponents(in: timeZone, from: proposedDate) - - if isEraRelevant == false && restrictedComponents.era == nil { - restrictedComponents.era = proposedComponents.era - } - - for unit in matching { - // we'll skip validating nanoseconds, because the precision of the Double backing a Foundation.Date - // is not enough to fully and completely represent all nanoseconds - - // however, basic experimentation shows that the drift from "requested" to "actual" nanoseconds - // appears to be restricted to within about 24,000 nanoseconds - if unit == .nanosecond { continue } - - guard proposedComponents.value(for: unit) == restrictedComponents.value(for: unit) else { - let r = Region(calendar: self, timeZone: self.timeZone, locale: self.locale ?? .current) - throw TimeError.invalidDateComponents(restrictedComponents, in: r) - } - } - - let actualComponents = try! proposedComponents.requireAndRestrict(to: matching, lenient: []) - - return (proposedDate, actualComponents) - } - - internal func range(of unit: Calendar.Component, containing date: Date) -> Range { - var start = Date() - var length: TimeInterval = 0 - let succeeded = self.dateInterval(of: unit, start: &start, interval: &length, for: date) - require(succeeded, "We should always be able to get the range of a calendar component") - - return start ..< start.addingTimeInterval(length) + + /// Different calendars may have different definitions of what a "second" is. + /// For example, on Earth, calendars all have the convention that one calendar-second + /// is the same as one SI Second. However, on Mars, the days are slightly longer, + /// which means that dividing the slightly-longer day in to 86,400 slices results + /// in "seconds" that are slightly longer than Earth seconds. + /// Therefore, to accommodate this, the calendar needs to define how many + /// SI Seconds are in each calendar-second. + /// note: This does NOT affect how physics calculations are done (or velocities, etc) + /// because those are all defined relative to SI Seconds. + internal var SISecondsPerSecond: Double { return 1.0 } + + /// For most calendars, the Era is not very relevant. For example "2019" is unambiguously + /// understood to be "2019 CE", not "2019 BCE". However, there are some calendars + /// (most notably the Japanese calendar) for which the era is extremely relevant. + /// The relevancy of the era is taken into account when doing default formatting + /// of calendar Values. + internal var isEraRelevant: Bool { return (maximumRange(of: .era)?.upperBound ?? 0) > 2 } + + internal var lenientUnitsForFixedTimePeriods: Set { + if isEraRelevant { return [] } + return [.era] + } + + internal func exactDate( + from components: DateComponents, in timeZone: TimeZone, matching: Set + ) throws -> (Date, DateComponents) { + var restrictedComponents = try components.requireAndRestrict( + to: matching, lenient: self.lenientUnitsForFixedTimePeriods) + restrictedComponents.timeZone = timeZone + + guard let proposedDate = self.date(from: restrictedComponents) else { + let r = Region(calendar: self, timeZone: timeZone, locale: self.locale ?? .current) + throw TimeError.invalidDateComponents(restrictedComponents, in: r) } - - internal func range(containing date: Date, in units: Set) -> Range { - let smallest = Calendar.Component.smallest(from: units) - return self.range(of: smallest, containing: date) + + let proposedComponents = self.dateComponents(in: timeZone, from: proposedDate) + + if isEraRelevant == false && restrictedComponents.era == nil { + restrictedComponents.era = proposedComponents.era } - - func isEquivalent(to other: Calendar) -> Bool { - guard identifier == other.identifier else { return false } - guard timeZone.isEquivalent(to: other.timeZone) else { return false } - guard firstWeekday == other.firstWeekday else { return false } - guard minimumDaysInFirstWeek == other.minimumDaysInFirstWeek else { return false } - - return true + + for unit in matching { + // we'll skip validating nanoseconds, because the precision of the Double backing a Foundation.Date + // is not enough to fully and completely represent all nanoseconds + + // however, basic experimentation shows that the drift from "requested" to "actual" nanoseconds + // appears to be restricted to within about 24,000 nanoseconds + if unit == .nanosecond { continue } + + guard proposedComponents.value(for: unit) == restrictedComponents.value(for: unit) else { + let r = Region(calendar: self, timeZone: self.timeZone, locale: self.locale ?? .current) + throw TimeError.invalidDateComponents(restrictedComponents, in: r) + } } - - var isLikelyAutoupdating: Bool { self == .autoupdatingCurrent } - - var loggingDescription: String { - if isEquivalent(to: Calendar.standard(identifier)) { - return (try? identifier.encodingIdentifier) ?? "\(identifier)" - } - return self.debugDescription + + let actualComponents = try! proposedComponents.requireAndRestrict(to: matching, lenient: []) + + return (proposedDate, actualComponents) + } + + internal func range(of unit: Calendar.Component, containing date: Date) -> Range { + var start = Date() + var length: TimeInterval = 0 + let succeeded = self.dateInterval(of: unit, start: &start, interval: &length, for: date) + require(succeeded, "We should always be able to get the range of a calendar component") + + return start..) -> Range { + let smallest = Calendar.Component.smallest(from: units) + return self.range(of: smallest, containing: date) + } + + func isEquivalent(to other: Calendar) -> Bool { + guard identifier == other.identifier else { return false } + guard timeZone.isEquivalent(to: other.timeZone) else { return false } + guard firstWeekday == other.firstWeekday else { return false } + guard minimumDaysInFirstWeek == other.minimumDaysInFirstWeek else { return false } + + return true + } + + var isLikelyAutoupdating: Bool { self == .autoupdatingCurrent } + + var loggingDescription: String { + if isEquivalent(to: Calendar.standard(identifier)) { + return (try? identifier.encodingIdentifier) ?? "\(identifier)" } - + return self.debugDescription + } + } extension Calendar.Identifier { - - var encodingIdentifier: String { - get throws { - switch self { - case .gregorian: return "gregorian" - case .buddhist: return "buddhist" - case .chinese: return "chinese" - case .coptic: return "coptic" - case .ethiopicAmeteMihret: return "ethiopic" - case .ethiopicAmeteAlem: return "ethiopic-amete-alem" - case .hebrew: return "hebrew" - case .iso8601: return "iso8601" - case .indian: return "indian" - case .islamic: return "islamic" - case .islamicCivil: return "islamic-civil" - case .japanese: return "japanese" - case .persian: return "persian" - case .republicOfChina: return "roc" - case .islamicTabular: return "islamic-tbla" - case .islamicUmmAlQura: return "islamic-umalqura" - default: - let ctx = EncodingError.Context(codingPath: [], debugDescription: "Unknown calendar identifier: '\(self)'") - throw TimeError.encodingError(EncodingError.invalidValue(self, ctx)) - } - } + + var encodingIdentifier: String { + get throws { + switch self { + case .gregorian: return "gregorian" + case .buddhist: return "buddhist" + case .chinese: return "chinese" + case .coptic: return "coptic" + case .ethiopicAmeteMihret: return "ethiopic" + case .ethiopicAmeteAlem: return "ethiopic-amete-alem" + case .hebrew: return "hebrew" + case .iso8601: return "iso8601" + case .indian: return "indian" + case .islamic: return "islamic" + case .islamicCivil: return "islamic-civil" + case .japanese: return "japanese" + case .persian: return "persian" + case .republicOfChina: return "roc" + case .islamicTabular: return "islamic-tbla" + case .islamicUmmAlQura: return "islamic-umalqura" + default: + let ctx = EncodingError.Context( + codingPath: [], debugDescription: "Unknown calendar identifier: '\(self)'") + throw TimeError.encodingError(EncodingError.invalidValue(self, ctx)) + } } - - init(encodingIdentifier: String) throws { - switch encodingIdentifier { - case "gregorian": self = .gregorian - case "buddhist": self = .buddhist - case "chinese": self = .chinese - case "coptic": self = .coptic - case "ethiopic": self = .ethiopicAmeteMihret - case "ethiopic-amete-alem": self = .ethiopicAmeteAlem - case "hebrew": self = .hebrew - case "iso8601": self = .iso8601 - case "indian": self = .indian - case "islamic": self = .islamic - case "islamic-civil": self = .islamicCivil - case "japanese": self = .japanese - case "persian": self = .persian - case "roc": self = .republicOfChina - case "islamic-tbla": self = .islamicTabular - case "islamic-umalqura": self = .islamicUmmAlQura - default: - let ctx = DecodingError.Context(codingPath: [], debugDescription: "Unknown calendar identifier: '\(encodingIdentifier)'") - throw TimeError.decodingError(DecodingError.dataCorrupted(ctx)) - } + } + + init(encodingIdentifier: String) throws { + switch encodingIdentifier { + case "gregorian": self = .gregorian + case "buddhist": self = .buddhist + case "chinese": self = .chinese + case "coptic": self = .coptic + case "ethiopic": self = .ethiopicAmeteMihret + case "ethiopic-amete-alem": self = .ethiopicAmeteAlem + case "hebrew": self = .hebrew + case "iso8601": self = .iso8601 + case "indian": self = .indian + case "islamic": self = .islamic + case "islamic-civil": self = .islamicCivil + case "japanese": self = .japanese + case "persian": self = .persian + case "roc": self = .republicOfChina + case "islamic-tbla": self = .islamicTabular + case "islamic-umalqura": self = .islamicUmmAlQura + default: + let ctx = DecodingError.Context( + codingPath: [], debugDescription: "Unknown calendar identifier: '\(encodingIdentifier)'") + throw TimeError.decodingError(DecodingError.dataCorrupted(ctx)) } - + } + } #if !os(Linux) -extension Locale.Weekday { - + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) + extension Locale.Weekday { + internal init(dayOfWeek: Int) { - switch dayOfWeek { - case 1: self = .sunday - case 2: self = .monday - case 3: self = .tuesday - case 4: self = .wednesday - case 5: self = .thursday - case 6: self = .friday - case 7: self = .saturday - default: fatalError("Invalid dayOfWeek: \(dayOfWeek)") - } + switch dayOfWeek { + case 1: self = .sunday + case 2: self = .monday + case 3: self = .tuesday + case 4: self = .wednesday + case 5: self = .thursday + case 6: self = .friday + case 7: self = .saturday + default: fatalError("Invalid dayOfWeek: \(dayOfWeek)") + } } - + internal var dayOfWeek: Int { - switch self { - case .sunday: return 1 - case .monday: return 2 - case .tuesday: return 3 - case .wednesday: return 4 - case .thursday: return 5 - case .friday: return 6 - case .saturday: return 7 - @unknown default: - print("Unknown weekday \(self); assuming Sunday") - return 1 - } + switch self { + case .sunday: return 1 + case .monday: return 2 + case .tuesday: return 3 + case .wednesday: return 4 + case .thursday: return 5 + case .friday: return 6 + case .saturday: return 7 + @unknown default: + print("Unknown weekday \(self); assuming Sunday") + return 1 + } } - -} + + } #endif diff --git a/Sources/Time/Internals/Time+CalendarComponent.swift b/Sources/Time/Internals/Time+CalendarComponent.swift index da8da64..45e3c53 100644 --- a/Sources/Time/Internals/Time+CalendarComponent.swift +++ b/Sources/Time/Internals/Time+CalendarComponent.swift @@ -1,138 +1,144 @@ import Foundation extension Calendar.Component { - - internal static let all: Array = numericComponents + [.calendar, .timeZone] - - internal static let numericComponents: Array = [ - .era, .year, .month, .day, - .hour, .minute, .second, .nanosecond, - .weekday, .weekdayOrdinal, .quarter, - .weekOfMonth, .weekOfYear, .yearForWeekOfYear - ] - - internal static let ascendingOrder: Array = [.nanosecond, .second, .minute, .hour, .day, .month, .year, .era] - internal static let descendingOrder: Array = [.era, .year, .month, .day, .hour, .minute, .second, .nanosecond] - - internal static func smallest(from units: Set) -> Calendar.Component { - for unit in ascendingOrder { - if units.contains(unit) { return unit } - } - fatalError("Cannot determine smallest unit in \(units)") - } - - internal static func largest(from units: Set) -> Calendar.Component { - for unit in descendingOrder { - if units.contains(unit) { return unit } - } - fatalError("Cannot determine largest unit in \(units)") - } - - internal static func from(lower: L.Type, to upper: U.Type) -> Set { - let order = Calendar.Component.ascendingOrder - guard let lowerIndex = order.firstIndex(of: L.component) else { return [] } - guard let upperIndex = order.firstIndex(of: U.component) else { return [] } - guard lowerIndex <= upperIndex else { return [] } - - let components = order[lowerIndex ... upperIndex] - return Set(components) - } - - internal var nextLargest: Self? { - guard let index = Self.ascendingOrder.firstIndex(of: self) else { return nil } - let nextIndex = index + 1 - guard nextIndex < Self.ascendingOrder.endIndex else { return nil } - return Self.ascendingOrder[nextIndex] + + internal static let all: [Calendar.Component] = numericComponents + [.calendar, .timeZone] + + internal static let numericComponents: [Calendar.Component] = [ + .era, .year, .month, .day, + .hour, .minute, .second, .nanosecond, + .weekday, .weekdayOrdinal, .quarter, + .weekOfMonth, .weekOfYear, .yearForWeekOfYear, + ] + + internal static let ascendingOrder: [Calendar.Component] = [ + .nanosecond, .second, .minute, .hour, .day, .month, .year, .era, + ] + internal static let descendingOrder: [Calendar.Component] = [ + .era, .year, .month, .day, .hour, .minute, .second, .nanosecond, + ] + + internal static func smallest(from units: Set) -> Calendar.Component { + for unit in ascendingOrder { + if units.contains(unit) { return unit } } - - internal var nextSmallest: Self? { - guard let index = Self.ascendingOrder.firstIndex(of: self) else { return nil } - let nextIndex = index - 1 - guard nextIndex > 0 else { return nil } - return Self.ascendingOrder[nextIndex] + fatalError("Cannot determine smallest unit in \(units)") + } + + internal static func largest(from units: Set) -> Calendar.Component { + for unit in descendingOrder { + if units.contains(unit) { return unit } } - - internal var minimumRequiredComponent: Self { - switch self { - // all fixed values have a time zone, - // so as long as it's a fixed value, the time zone requirement is satisfied - case .era: return .era - case .timeZone: return .era - - case .year: return .year - - case .month: return .month - case .quarter: return .month - - // for some reason, including .isLeapMonth is causing linking issues - // case .isLeapMonth: return .month - - case .day: return .day - case .weekday: return .day - case .weekdayOrdinal: return .day - case .weekOfMonth: return .day - case .weekOfYear: return .day - case .yearForWeekOfYear: return .day - - case .hour: return .hour - case .minute: return .minute - case .second: return .second - case .nanosecond: return .nanosecond - - case .calendar: - fatalError("Invalid calendar component: .calendar") - - // TODO: replace @unknown after the "isLeapMonth" issue is resolved - /* @unknown */ default: return .day - - } + fatalError("Cannot determine largest unit in \(units)") + } + + internal static func from(lower: L.Type, to upper: U.Type) -> Set< + Calendar.Component + > { + let order = Calendar.Component.ascendingOrder + guard let lowerIndex = order.firstIndex(of: L.component) else { return [] } + guard let upperIndex = order.firstIndex(of: U.component) else { return [] } + guard lowerIndex <= upperIndex else { return [] } + + let components = order[lowerIndex...upperIndex] + return Set(components) + } + + internal var nextLargest: Self? { + guard let index = Self.ascendingOrder.firstIndex(of: self) else { return nil } + let nextIndex = index + 1 + guard nextIndex < Self.ascendingOrder.endIndex else { return nil } + return Self.ascendingOrder[nextIndex] + } + + internal var nextSmallest: Self? { + guard let index = Self.ascendingOrder.firstIndex(of: self) else { return nil } + let nextIndex = index - 1 + guard nextIndex > 0 else { return nil } + return Self.ascendingOrder[nextIndex] + } + + internal var minimumRequiredComponent: Self { + switch self { + // all fixed values have a time zone, + // so as long as it's a fixed value, the time zone requirement is satisfied + case .era: return .era + case .timeZone: return .era + + case .year: return .year + + case .month: return .month + case .quarter: return .month + + // for some reason, including .isLeapMonth is causing linking issues + // case .isLeapMonth: return .month + + case .day: return .day + case .weekday: return .day + case .weekdayOrdinal: return .day + case .weekOfMonth: return .day + case .weekOfYear: return .day + case .yearForWeekOfYear: return .day + + case .hour: return .hour + case .minute: return .minute + case .second: return .second + case .nanosecond: return .nanosecond + + case .calendar: + fatalError("Invalid calendar component: .calendar") + + // TODO: replace @unknown after the "isLeapMonth" issue is resolved + /* @unknown */ default: return .day + } - - init?(formatCharacter: Character) { - switch formatCharacter { - case "j": self = .day - case "J": self = .day - case "C": self = .hour - case "G": self = .era - case "y": self = .year - case "Y": self = .yearForWeekOfYear - case "u": self = .year - case "U": self = .year - case "r": self = .year - case "Q": self = .quarter - case "q": self = .quarter - case "M": self = .month - case "L": self = .month - case "w": self = .weekOfYear - case "W": self = .weekOfMonth - case "d": self = .day - - // TODO: dayOfYear: https://github.com/apple/swift-foundation/blob/main/Proposals/0001-calendar-improvements.md - case "D": self = .day - case "F": self = .weekdayOrdinal - case "g": self = .day // Julian day, technically - case "E": self = .weekday - case "e": self = .weekday - case "c": self = .weekday - case "a": self = .hour - case "b": self = .hour - case "B": self = .hour - case "h": self = .hour - case "H": self = .hour - case "k": self = .hour - case "K": self = .hour - case "m": self = .minute - case "s": self = .second - case "S": self = .nanosecond - case "A": self = .nanosecond - case "z": self = .timeZone - case "Z": self = .timeZone - case "O": self = .timeZone - case "v": self = .timeZone - case "V": self = .timeZone - case "X": self = .timeZone - case "x": self = .timeZone - default: return nil - } + } + + init?(formatCharacter: Character) { + switch formatCharacter { + case "j": self = .day + case "J": self = .day + case "C": self = .hour + case "G": self = .era + case "y": self = .year + case "Y": self = .yearForWeekOfYear + case "u": self = .year + case "U": self = .year + case "r": self = .year + case "Q": self = .quarter + case "q": self = .quarter + case "M": self = .month + case "L": self = .month + case "w": self = .weekOfYear + case "W": self = .weekOfMonth + case "d": self = .day + + // TODO: dayOfYear: https://github.com/apple/swift-foundation/blob/main/Proposals/0001-calendar-improvements.md + case "D": self = .day + case "F": self = .weekdayOrdinal + case "g": self = .day // Julian day, technically + case "E": self = .weekday + case "e": self = .weekday + case "c": self = .weekday + case "a": self = .hour + case "b": self = .hour + case "B": self = .hour + case "h": self = .hour + case "H": self = .hour + case "k": self = .hour + case "K": self = .hour + case "m": self = .minute + case "s": self = .second + case "S": self = .nanosecond + case "A": self = .nanosecond + case "z": self = .timeZone + case "Z": self = .timeZone + case "O": self = .timeZone + case "v": self = .timeZone + case "V": self = .timeZone + case "X": self = .timeZone + case "x": self = .timeZone + default: return nil } + } } diff --git a/Sources/Time/Internals/Time+DateComponents.swift b/Sources/Time/Internals/Time+DateComponents.swift index 3142186..2fe47d9 100644 --- a/Sources/Time/Internals/Time+DateComponents.swift +++ b/Sources/Time/Internals/Time+DateComponents.swift @@ -4,135 +4,144 @@ import Foundation // We'll work around it by redefining the value ourselves private let FoundationNotFound = Int.max -internal extension DateComponents { - - init(value: Int, component: Calendar.Component) { - self.init() - self.setValue(value, for: component) - } - - /// Restrict the receiver to only the provided set of calendar components - /// - /// This is used for constructing ``Fixed`` values, because fixed time periods must contain - /// values for all relevant calendar components. The exception to this is that some calendars can omit the `.era` unit - /// and still correctly interpret the set of component values. In those cases, `[.era]` is typically passed in as the set - /// of "lenient" components for which this code will allow a missing component value. - /// - /// - Parameters: - /// - components: the set of ``Calendar.Component`` values that must all be present in the returned value - /// - lenient: a set of ``Calendar.Component`` values that may be omitted from the returned value - /// - Returns: a ``DateComponents`` value that will only contain the required components. If a component is missing from the receiver, - /// and that component is *not* present in the `lenient` set, then this will throw an error - func requireAndRestrict(to components: Set, lenient: Set) throws -> DateComponents { - - var final = DateComponents() - var missing = Set() - for component in components { - if let value = self.value(for: component), value != FoundationNotFound { - final.setValue(value, for: component) - } else if lenient.contains(component) == false { - missing.insert(component) - } - } - - if missing.isEmpty == false { - throw TimeError.missingCalendarComponents(missing, in: self) - } - - return final - } - - /// Restrict the receiver to only the provided set of calendar components - /// - Parameter components: The set of ``Calendar.Component`` values that *may* be present in the returned value - /// - Returns: A ``DateComponents`` value that will contain zero or more calendar components - func restrict(to components: Set) -> DateComponents { - var final = DateComponents() - for component in components { - if let value = self.value(for: component), value != FoundationNotFound { - final.setValue(value, for: component) - } - } - return final - } - - func scale(by factor: Int) -> DateComponents { - let s: (Int?) -> Int? = { $0.map { $0 * factor } } - - return DateComponents(calendar: calendar, - timeZone: timeZone, - era: s(era), - year: s(year), - month: s(month), - day: s(day), - hour: s(hour), - minute: s(minute), - second: s(second), - nanosecond: s(nanosecond), - weekday: s(weekday), - weekdayOrdinal: s(weekdayOrdinal), - quarter: s(quarter), - weekOfMonth: s(weekOfMonth), - weekOfYear: s(weekOfYear), - yearForWeekOfYear: s(yearForWeekOfYear)) - } - - func has(component: Calendar.Component) -> Bool { - let val = self.value(for: component) - return val != nil && val != FoundationNotFound - } - - var representedComponents: Set { - let contained = Calendar.Component.ascendingOrder.filter { self.has(component: $0) } - return Set(contained) - } - - var smallestRepresentedComponent: Calendar.Component? { - return Calendar.Component.ascendingOrder.first(where: { self.has(component: $0) }) - } - - var loggingDescription: String { - return "{" + Calendar.Component.numericComponents.compactMap({ unit -> String? in - guard let v = self.value(for: unit), v != FoundationNotFound else { return nil } - guard self.has(component: unit) else { return nil } - return "\(unit): \(v)" - }).joined(separator: ", ") + "}" +extension DateComponents { + + init(value: Int, component: Calendar.Component) { + self.init() + self.setValue(value, for: component) + } + + /// Restrict the receiver to only the provided set of calendar components + /// + /// This is used for constructing ``Fixed`` values, because fixed time periods must contain + /// values for all relevant calendar components. The exception to this is that some calendars can omit the `.era` unit + /// and still correctly interpret the set of component values. In those cases, `[.era]` is typically passed in as the set + /// of "lenient" components for which this code will allow a missing component value. + /// + /// - Parameters: + /// - components: the set of ``Calendar.Component`` values that must all be present in the returned value + /// - lenient: a set of ``Calendar.Component`` values that may be omitted from the returned value + /// - Returns: a ``DateComponents`` value that will only contain the required components. If a component is missing from the receiver, + /// and that component is *not* present in the `lenient` set, then this will throw an error + func requireAndRestrict(to components: Set, lenient: Set) + throws -> DateComponents + { + + var final = DateComponents() + var missing = Set() + for component in components { + if let value = self.value(for: component), value != FoundationNotFound { + final.setValue(value, for: component) + } else if lenient.contains(component) == false { + missing.insert(component) + } } - - func isLessThan(other: DateComponents) -> Bool { - for unit in Calendar.Component.descendingOrder { - let lValue = self.value(for: unit) - let rValue = other.value(for: unit) - - switch (lValue, rValue) { - case (.none, .none): continue - case (.none, .some(_)): return true - case (.some(_), .none): return false - case (.some(let l), .some(let r)): - if l < r { return true } - if l > r { return false } - continue - } - } - return false + + if missing.isEmpty == false { + throw TimeError.missingCalendarComponents(missing, in: self) } - - func isGreaterThan(other: DateComponents) -> Bool { - return isLessThan(other: other) == false && (self != other) + + return final + } + + /// Restrict the receiver to only the provided set of calendar components + /// - Parameter components: The set of ``Calendar.Component`` values that *may* be present in the returned value + /// - Returns: A ``DateComponents`` value that will contain zero or more calendar components + func restrict(to components: Set) -> DateComponents { + var final = DateComponents() + for component in components { + if let value = self.value(for: component), value != FoundationNotFound { + final.setValue(value, for: component) + } } - - func setting(era: Int? = nil, year: Int? = nil, month: Int? = nil, day: Int? = nil, hour: Int? = nil, minute: Int? = nil, second: Int? = nil, nanosecond: Int? = nil) -> DateComponents { - let merge = DateComponents(era: era, year: year, month: month, day: day, hour: hour, minute: minute, second: second, nanosecond: nanosecond) - return merging(merge) + return final + } + + func scale(by factor: Int) -> DateComponents { + let s: (Int?) -> Int? = { $0.map { $0 * factor } } + + return DateComponents( + calendar: calendar, + timeZone: timeZone, + era: s(era), + year: s(year), + month: s(month), + day: s(day), + hour: s(hour), + minute: s(minute), + second: s(second), + nanosecond: s(nanosecond), + weekday: s(weekday), + weekdayOrdinal: s(weekdayOrdinal), + quarter: s(quarter), + weekOfMonth: s(weekOfMonth), + weekOfYear: s(weekOfYear), + yearForWeekOfYear: s(yearForWeekOfYear)) + } + + func has(component: Calendar.Component) -> Bool { + let val = self.value(for: component) + return val != nil && val != FoundationNotFound + } + + var representedComponents: Set { + let contained = Calendar.Component.ascendingOrder.filter { self.has(component: $0) } + return Set(contained) + } + + var smallestRepresentedComponent: Calendar.Component? { + return Calendar.Component.ascendingOrder.first(where: { self.has(component: $0) }) + } + + var loggingDescription: String { + return "{" + + Calendar.Component.numericComponents.compactMap({ unit -> String? in + guard let v = self.value(for: unit), v != FoundationNotFound else { return nil } + guard self.has(component: unit) else { return nil } + return "\(unit): \(v)" + }).joined(separator: ", ") + "}" + } + + func isLessThan(other: DateComponents) -> Bool { + for unit in Calendar.Component.descendingOrder { + let lValue = self.value(for: unit) + let rValue = other.value(for: unit) + + switch (lValue, rValue) { + case (.none, .none): continue + case (.none, .some(_)): return true + case (.some(_), .none): return false + case (.some(let l), .some(let r)): + if l < r { return true } + if l > r { return false } + continue + } } - - func merging(_ other: DateComponents) -> DateComponents { - var copy = self - for unit in Calendar.Component.descendingOrder { - if let value = other.value(for: unit), value != FoundationNotFound { - copy.setValue(value, for: unit) - } - } - return copy + return false + } + + func isGreaterThan(other: DateComponents) -> Bool { + return isLessThan(other: other) == false && (self != other) + } + + func setting( + era: Int? = nil, year: Int? = nil, month: Int? = nil, day: Int? = nil, hour: Int? = nil, + minute: Int? = nil, second: Int? = nil, nanosecond: Int? = nil + ) -> DateComponents { + let merge = DateComponents( + era: era, year: year, month: month, day: day, hour: hour, minute: minute, second: second, + nanosecond: nanosecond) + return merging(merge) + } + + func merging(_ other: DateComponents) -> DateComponents { + var copy = self + for unit in Calendar.Component.descendingOrder { + if let value = other.value(for: unit), value != FoundationNotFound { + copy.setValue(value, for: unit) + } } - + return copy + } + } diff --git a/Sources/Time/Internals/Time+Duration.swift b/Sources/Time/Internals/Time+Duration.swift deleted file mode 100644 index 757000f..0000000 --- a/Sources/Time/Internals/Time+Duration.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation - -extension Swift.Duration { - - // Swift.Duration has += defined, but not binary + - internal static func +(lhs: Self, rhs: Self) -> Self { - var copy = lhs - copy += rhs - return copy - } - - // Swift.Duration has prefix + defined, but not prefix - - internal prefix static func -(rhs: Self) -> Self { - var copy = Duration.zero - copy -= rhs - return copy - } - -} diff --git a/Sources/Time/Internals/Time+Locale.swift b/Sources/Time/Internals/Time+Locale.swift index c9f0cf7..ef94ade 100644 --- a/Sources/Time/Internals/Time+Locale.swift +++ b/Sources/Time/Internals/Time+Locale.swift @@ -1,70 +1,76 @@ import Foundation extension Locale { - - func isEquivalent(to other: Locale) -> Bool { - #if os(Linux) - guard identifier == other.identifier else { return false } - guard bcp47HourCycle == other.bcp47HourCycle else { return false } - guard bcp47FirstWeekday == other.bcp47FirstWeekday else { return false } - - #else + + func isEquivalent(to other: Locale) -> Bool { + #if os(Linux) + guard identifier == other.identifier else { return false } + guard bcp47HourCycle == other.bcp47HourCycle else { return false } + guard bcp47FirstWeekday == other.bcp47FirstWeekday else { return false } + + #else + if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, macCatalyst 16, *) { guard calendar.identifier == other.calendar.identifier else { return false } guard collation == other.collation else { return false } guard currency == other.currency else { return false } guard firstDayOfWeek == other.firstDayOfWeek else { return false } guard hourCycle == other.hourCycle else { return false } - + guard measurementSystem == other.measurementSystem else { return false } guard numberingSystem == other.numberingSystem else { return false } guard region == other.region else { return false } guard subdivision == other.subdivision else { return false } guard variant == other.variant else { return false } - + guard language == other.language else { return false } - #endif - - return true - } - - var isLikelyAutoupdating: Bool { self == .autoupdatingCurrent } - - var loggingDescription: String { - if self.isEquivalent(to: Locale.standard(self.identifier)) { return self.identifier } - return self.debugDescription - } - - internal var bcp47FirstWeekday: String? { - switch calendar.firstWeekday { - case 1: return "sun" - case 2: return "mon" - case 3: return "tue" - case 4: return "wed" - case 5: return "thu" - case 6: return "fri" - case 7: return "sat" - default: return nil - } + } else { + guard identifier == other.identifier else { return false } + guard bcp47HourCycle == other.bcp47HourCycle else { return false } + guard bcp47FirstWeekday == other.bcp47FirstWeekday else { return false } + } + #endif + + return true + } + + var isLikelyAutoupdating: Bool { self == .autoupdatingCurrent } + + var loggingDescription: String { + if self.isEquivalent(to: Locale.standard(self.identifier)) { return self.identifier } + return self.debugDescription + } + + internal var bcp47FirstWeekday: String? { + switch calendar.firstWeekday { + case 1: return "sun" + case 2: return "mon" + case 3: return "tue" + case 4: return "wed" + case 5: return "thu" + case 6: return "fri" + case 7: return "sat" + default: return nil } - - internal var bcp47HourCycle: String? { - let formatString = DateFormatter.dateFormat(fromTemplate: "J", options: 0, locale: self) - switch formatString { - case "h": return "h12" // 1-12 - case "H": return "h23" // 0-23 - case "K": return "h11" // 0-11 - case "k": return "h24" // 1-24 - default: return nil - } + } + + internal var bcp47HourCycle: String? { + let formatString = DateFormatter.dateFormat(fromTemplate: "J", options: 0, locale: self) + switch formatString { + case "h": return "h12" // 1-12 + case "H": return "h23" // 0-23 + case "K": return "h11" // 0-11 + case "k": return "h24" // 1-24 + default: return nil } - - internal var wants24HourTime: Bool { - if let cycle = bcp47HourCycle { - return cycle == "h23" || cycle == "h24" - } - - let hour = DateFormatter.dateFormat(fromTemplate: "J", options: 0, locale: self) - return hour?.contains("H") == true || hour?.contains("k") == true + } + + internal var wants24HourTime: Bool { + if let cycle = bcp47HourCycle { + return cycle == "h23" || cycle == "h24" } - + + let hour = DateFormatter.dateFormat(fromTemplate: "J", options: 0, locale: self) + return hour?.contains("H") == true || hour?.contains("k") == true + } + } diff --git a/Sources/Time/Internals/Time+TimeZone.swift b/Sources/Time/Internals/Time+TimeZone.swift index 43a0c58..e14287f 100644 --- a/Sources/Time/Internals/Time+TimeZone.swift +++ b/Sources/Time/Internals/Time+TimeZone.swift @@ -1,13 +1,13 @@ import Foundation extension TimeZone { - - func isEquivalent(to other: TimeZone) -> Bool { - guard identifier == other.identifier else { return false } - - return true - } - - var isLikelyAutoupdating: Bool { self == .autoupdatingCurrent } - + + func isEquivalent(to other: TimeZone) -> Bool { + guard identifier == other.identifier else { return false } + + return true + } + + var isLikelyAutoupdating: Bool { self == .autoupdatingCurrent } + } diff --git a/Tests/TimeTests/ClockStrikeTests.swift b/Tests/TimeTests/ClockStrikeTests.swift index 3132f85..6e4bbde 100644 --- a/Tests/TimeTests/ClockStrikeTests.swift +++ b/Tests/TimeTests/ClockStrikeTests.swift @@ -1,308 +1,348 @@ -import XCTest import Time +import XCTest #if canImport(Combine) -import Combine + import Combine + + class SkipUnavailableTestCase: XCTestCase { + override func setUpWithError() throws { + try super.setUpWithError() + + if #unavailable(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, macCatalyst 16.0) { + try XCTSkipIf( + true, + "Skipping unrelated platform test" + ) + } + } + } + + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + final class ClockStrikeTests: SkipUnavailableTestCase { -@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -final class ClockStrikeTests: XCTestCase { - static var allTests = [ - ("testPastStrike", testPastStrike), - ("testImmediateStrike", testImmediateStrike), - ("testStrikeAtSpecificValue", testStrikeAtSpecificValue), - ("testScaledStrikeAtSpecificValue", testScaledStrikeAtSpecificValue), - ("testFixedStrikeCancel", testFixedStrikeCancel), - ("testIntervalStrike", testIntervalStrike), - ("testIntervalStrikeWithPastStart", testIntervalStrikeWithPastStart), - ("testIntervalStrikeCancel", testIntervalStrikeCancel), - ("testPredicateStrike", testPredicateStrike), + ("testPastStrike", testPastStrike), + ("testImmediateStrike", testImmediateStrike), + ("testStrikeAtSpecificValue", testStrikeAtSpecificValue), + ("testScaledStrikeAtSpecificValue", testScaledStrikeAtSpecificValue), + ("testFixedStrikeCancel", testFixedStrikeCancel), + ("testIntervalStrike", testIntervalStrike), + ("testIntervalStrikeWithPastStart", testIntervalStrikeWithPastStart), + ("testIntervalStrikeCancel", testIntervalStrikeCancel), + ("testPredicateStrike", testPredicateStrike), ] - - let clock = Clocks.system + + var clock: (any RegionalClock)! var cancellables = Set() - + + override func setUpWithError() throws { + try super.setUpWithError() + clock = Clocks.system + } + override func tearDown() { - cancellables.removeAll() - super.tearDown() + cancellables.removeAll() + super.tearDown() } - + func testPastStrike() { - let dontStrike = expectation(description: "Clock never strikes") - dontStrike.isInverted = true - - let completes = expectation(description: "Strike completes") - let lastMinute = clock.previousMinute - - clock - .strike(at: lastMinute) // Should fire ASAP - .publisher - .sink(receiveCompletion: { _ in - completes.fulfill() - }, receiveValue: { _ in - dontStrike.fulfill() - }) - .store(in: &cancellables) - - wait(for: [completes, dontStrike], timeout: 0.01, enforceOrder: true) + let dontStrike = expectation(description: "Clock never strikes") + dontStrike.isInverted = true + + let completes = expectation(description: "Strike completes") + let lastMinute = clock.previousMinute + + clock + .strike(at: lastMinute) // Should fire ASAP + .publisher + .sink( + receiveCompletion: { _ in + completes.fulfill() + }, + receiveValue: { _ in + dontStrike.fulfill() + } + ) + .store(in: &cancellables) + + wait(for: [completes, dontStrike], timeout: 0.01, enforceOrder: true) } - + func testPastStrikeAsync() async throws { - let lastMinute = clock.previousMinute - - var strikeCount = 0 - for try await _ in clock.strike(at: lastMinute).asyncValues { - XCTFail("Clock strikes should skip times in the past") - strikeCount += 1 - } - XCTAssertEqual(strikeCount, 0) + let lastMinute = clock.previousMinute + + var strikeCount = 0 + for try await _ in clock.strike(at: lastMinute).asyncValues { + XCTFail("Clock strikes should skip times in the past") + strikeCount += 1 + } + XCTAssertEqual(strikeCount, 0) } - + func testImmediateStrike() { - let strikesOnce = expectation(description: "Clock strikes immediately") - let completes = expectation(description: "Strike completes") - let now = clock.currentSecond - - clock - .strike(at: now) // Should fire immediately - .publisher - .sink(receiveCompletion: { _ in - completes.fulfill() - }, receiveValue: { value in - XCTAssertEqual(now, value) - strikesOnce.fulfill() - }) - .store(in: &cancellables) - - wait(for: [strikesOnce, completes], timeout: 0.5, enforceOrder: true) + let strikesOnce = expectation(description: "Clock strikes immediately") + let completes = expectation(description: "Strike completes") + let now = clock.currentSecond + + clock + .strike(at: now) // Should fire immediately + .publisher + .sink( + receiveCompletion: { _ in + completes.fulfill() + }, + receiveValue: { value in + XCTAssertEqual(now, value) + strikesOnce.fulfill() + } + ) + .store(in: &cancellables) + + wait(for: [strikesOnce, completes], timeout: 0.5, enforceOrder: true) } - + func testImmediateStrikeAsync() async throws { - let start = clock.currentSecond - - var strikeCount = 0 - for try await time in clock.strike(at: start).asyncValues { - let now = clock.currentSecond - XCTAssertEqual(start, now) - XCTAssertEqual(time, now) - strikeCount += 1 - } - XCTAssertEqual(strikeCount, 1) + let start = clock.currentSecond + + var strikeCount = 0 + for try await time in clock.strike(at: start).asyncValues { + let now = clock.currentSecond + XCTAssertEqual(start, now) + XCTAssertEqual(time, now) + strikeCount += 1 + } + XCTAssertEqual(strikeCount, 1) } - + func testStrikeAtSpecificValue() { - let strikesOnce = expectation(description: "Clock strikes shortly") - let completes = expectation(description: "Strike completes") - - let nextSecond = clock.nextSecond - clock - .strike(at: nextSecond) - .publisher - .sink(receiveCompletion: { _ in - completes.fulfill() - }, receiveValue: { value in - XCTAssertEqual(value, nextSecond) - strikesOnce.fulfill() - }) - .store(in: &cancellables) - - wait(for: [strikesOnce, completes], timeout: 1.5, enforceOrder: true) + let strikesOnce = expectation(description: "Clock strikes shortly") + let completes = expectation(description: "Strike completes") + + let nextSecond = clock.nextSecond + clock + .strike(at: nextSecond) + .publisher + .sink( + receiveCompletion: { _ in + completes.fulfill() + }, + receiveValue: { value in + XCTAssertEqual(value, nextSecond) + strikesOnce.fulfill() + } + ) + .store(in: &cancellables) + + wait(for: [strikesOnce, completes], timeout: 1.5, enforceOrder: true) } - + func testStrikeAtSpecificValueAsync() async throws { - let nextSecond = clock.nextSecond - var strikeCount = 0 - - for try await time in clock.strike(at: nextSecond).asyncValues { - XCTAssertEqual(time, nextSecond) - strikeCount += 1 - } - XCTAssertEqual(strikeCount, 1) + let nextSecond = clock.nextSecond + var strikeCount = 0 + + for try await time in clock.strike(at: nextSecond).asyncValues { + XCTAssertEqual(time, nextSecond) + strikeCount += 1 + } + XCTAssertEqual(strikeCount, 1) } - + func testScaledStrikeAtSpecificValue() { - let strikesOnce = expectation(description: "Clock strikes shortly") - let completes = expectation(description: "Strike completes") - - let sixtyXClock = Clocks.custom(startingFrom: clock.now, rate: 60.0, region: clock.region) - let nextMinute = sixtyXClock.nextMinute - - sixtyXClock - .strike(at: nextMinute) - .publisher - .sink(receiveCompletion: { _ in - completes.fulfill() - }, receiveValue: { value in - XCTAssertEqual(value, nextMinute) - strikesOnce.fulfill() - }) - .store(in: &cancellables) - - wait(for: [strikesOnce, completes], timeout: 1, enforceOrder: true) + let strikesOnce = expectation(description: "Clock strikes shortly") + let completes = expectation(description: "Strike completes") + + let sixtyXClock = Clocks.custom(startingFrom: clock.now, rate: 60.0, region: clock.region) + let nextMinute = sixtyXClock.nextMinute + + sixtyXClock + .strike(at: nextMinute) + .publisher + .sink( + receiveCompletion: { _ in + completes.fulfill() + }, + receiveValue: { value in + XCTAssertEqual(value, nextMinute) + strikesOnce.fulfill() + } + ) + .store(in: &cancellables) + + wait(for: [strikesOnce, completes], timeout: 1, enforceOrder: true) } - + func testScaledStrikeAtSpecificValueAsync() async throws { - let sixtyXClock = Clocks.custom(startingFrom: clock.now, rate: 60.0, region: clock.region) - let nextMinute = sixtyXClock.nextMinute - - var strikeCount = 0 - for try await time in sixtyXClock.strike(at: nextMinute).asyncValues { - XCTAssertEqual(time, nextMinute) - strikeCount += 1 - } - XCTAssertEqual(strikeCount, 1) + let sixtyXClock = Clocks.custom(startingFrom: clock.now, rate: 60.0, region: clock.region) + let nextMinute = sixtyXClock.nextMinute + + var strikeCount = 0 + for try await time in sixtyXClock.strike(at: nextMinute).asyncValues { + XCTAssertEqual(time, nextMinute) + strikeCount += 1 + } + XCTAssertEqual(strikeCount, 1) } - + func testFixedStrikeCancel() { - let nextSecond = clock.nextSecond - let dontStrike = expectation(description: "Clock does not strike") - dontStrike.isInverted = true - let strike = clock.strike(at: nextSecond) - .publisher - .sink { value in - dontStrike.fulfill() - } - strike.cancel() - wait(for: [dontStrike], timeout: 1.0) + let nextSecond = clock.nextSecond + let dontStrike = expectation(description: "Clock does not strike") + dontStrike.isInverted = true + let strike = clock.strike(at: nextSecond) + .publisher + .sink { value in + dontStrike.fulfill() + } + strike.cancel() + wait(for: [dontStrike], timeout: 1.0) } - + // There's no way to make a `testFixedStrikeCancelAsync()` test, because callers cannot cancel an async for loop - + func testIntervalStrike() { - let start = clock.nextSecond - var results = [start, start.nextSecond] - print("Expecting \(results.count) strikes at \(results.map(\.debugDescription))") - - let strikesTwice = expectation(description: "Clock strikes twice, once per second") - strikesTwice.expectedFulfillmentCount = results.count - - clock - .strike(every: TimeDifference.seconds(1), startingFrom: start) - .publisher - .sink(receiveCompletion: { (completion) in - XCTFail("Repeating strike completed: \(completion)") - }, receiveValue: { value in - if results.isEmpty { - XCTFail("Received unexpected strike at \(value.debugDescription)") - } else { - let expected = results.removeFirst() - XCTAssertEqual(value, expected) - print(value.debugDescription, expected.debugDescription) - strikesTwice.fulfill() - } - }) - .store(in: &cancellables) - - wait(for: [strikesTwice], timeout: 2.0) + let start = clock.nextSecond + var results = [start, start.nextSecond] + print("Expecting \(results.count) strikes at \(results.map(\.debugDescription))") + + let strikesTwice = expectation(description: "Clock strikes twice, once per second") + strikesTwice.expectedFulfillmentCount = results.count + + clock + .strike(every: TimeDifference.seconds(1), startingFrom: start) + .publisher + .sink( + receiveCompletion: { (completion) in + XCTFail("Repeating strike completed: \(completion)") + }, + receiveValue: { value in + if results.isEmpty { + XCTFail("Received unexpected strike at \(value.debugDescription)") + } else { + let expected = results.removeFirst() + XCTAssertEqual(value, expected) + print(value.debugDescription, expected.debugDescription) + strikesTwice.fulfill() + } + } + ) + .store(in: &cancellables) + + wait(for: [strikesTwice], timeout: 2.0) } - + func testIntervalStrikeAsync() async throws { - let start = clock.nextSecond - - var expected = [start, start.nextSecond] - var strikeCount = 0 - - for try await time in clock.strike(every: .seconds(1), startingFrom: start).asyncValues { - XCTAssertEqual(time, expected.removeFirst()) - strikeCount += 1 - if expected.isEmpty { break } - } - - XCTAssertEqual(strikeCount, 2) + let start = clock.nextSecond + + var expected = [start, start.nextSecond] + var strikeCount = 0 + + for try await time in clock.strike(every: .seconds(1), startingFrom: start).asyncValues { + XCTAssertEqual(time, expected.removeFirst()) + strikeCount += 1 + if expected.isEmpty { break } + } + + XCTAssertEqual(strikeCount, 2) } - + func testIntervalStrikeWithPastStart() { - let strikes = expectation(description: "Clock strikes once") - - let thisSecond = clock.currentSecond - let aMinuteAgo = thisSecond.subtracting(minutes: 1) - - clock - .strike(every: .seconds(1), startingFrom: aMinuteAgo) - .publisher - .sink(receiveCompletion: { (completion) in - XCTFail("Repeating strike completed: \(completion)") - }, receiveValue: { value in - XCTAssertEqual(value, thisSecond) - strikes.fulfill() - }) - .store(in: &cancellables) - - wait(for: [strikes], timeout: 1) + let strikes = expectation(description: "Clock strikes once") + + let thisSecond = clock.currentSecond + let aMinuteAgo = thisSecond.subtracting(minutes: 1) + + clock + .strike(every: .seconds(1), startingFrom: aMinuteAgo) + .publisher + .sink( + receiveCompletion: { (completion) in + XCTFail("Repeating strike completed: \(completion)") + }, + receiveValue: { value in + XCTAssertEqual(value, thisSecond) + strikes.fulfill() + } + ) + .store(in: &cancellables) + + wait(for: [strikes], timeout: 1) } - + func testIntervalStrikeWithPastStartAsync() async throws { - let thisSecond = clock.currentSecond - let aMinuteAgo = thisSecond.subtracting(minutes: 1) - - var strikeCount = 0 - for try await time in clock.strike(every: .seconds(1), startingFrom: aMinuteAgo).asyncValues { - XCTAssertEqual(time, thisSecond) - strikeCount += 1 - break - } - XCTAssertEqual(strikeCount, 1) + let thisSecond = clock.currentSecond + let aMinuteAgo = thisSecond.subtracting(minutes: 1) + + var strikeCount = 0 + for try await time in clock.strike(every: .seconds(1), startingFrom: aMinuteAgo).asyncValues { + XCTAssertEqual(time, thisSecond) + strikeCount += 1 + break + } + XCTAssertEqual(strikeCount, 1) } - + func testIntervalStrikeCancel() { - let blueMoon = TimeDifference.nanoseconds(1_000_000_000 / 12) - let bellOfStJohn = expectation(description: "The bell rings twelve times") - bellOfStJohn.expectedFulfillmentCount = 12 - clock.strike(every: blueMoon) - .publisher - .sink { _ in - bellOfStJohn.fulfill() - }.store(in: &cancellables) - - wait(for: [bellOfStJohn], timeout: 2) + let blueMoon = TimeDifference.nanoseconds(1_000_000_000 / 12) + let bellOfStJohn = expectation(description: "The bell rings twelve times") + bellOfStJohn.expectedFulfillmentCount = 12 + clock.strike(every: blueMoon) + .publisher + .sink { _ in + bellOfStJohn.fulfill() + }.store(in: &cancellables) + + wait(for: [bellOfStJohn], timeout: 2) } - + func testPredicateStrike() { - // Set to the next hour - let justBeforeNextMinute = clock.nextMinute.firstSecond.previous - - // every second is a minute - let fastClock = Clocks.custom(startingFrom: justBeforeNextMinute.firstInstant, rate: 60.0, region: clock.region) - var results = [0, 13, 26, 39, 52] - - let strikes = expectation(description: "Clock strikes 5 times in an hour") - strikes.expectedFulfillmentCount = 5 - - fastClock - .strike(when: { (value: Fixed) -> Bool in - return value.second % 13 == 0 - }) - .publisher - .sink(receiveValue: { value in - let expected = results.removeFirst() - XCTAssertEqual(value.second, expected) - strikes.fulfill() - }) - .store(in: &cancellables) - - wait(for: [strikes], timeout: 10.0) + // Set to the next hour + let justBeforeNextMinute = clock.nextMinute.firstSecond.previous + + // every second is a minute + let fastClock = Clocks.custom( + startingFrom: justBeforeNextMinute.firstInstant, rate: 60.0, region: clock.region) + var results = [0, 13, 26, 39, 52] + + let strikes = expectation(description: "Clock strikes 5 times in an hour") + strikes.expectedFulfillmentCount = 5 + + fastClock + .strike(when: { (value: Fixed) -> Bool in + return value.second % 13 == 0 + }) + .publisher + .sink(receiveValue: { value in + let expected = results.removeFirst() + XCTAssertEqual(value.second, expected) + strikes.fulfill() + }) + .store(in: &cancellables) + + wait(for: [strikes], timeout: 10.0) } - + func testPredicateStrikeAsync() async throws { - let justBeforeTheNextMinute = clock.nextMinute.firstSecond.previous - - let fastClock = Clocks.custom(startingFrom: justBeforeTheNextMinute.firstInstant, rate: 60.0, region: clock.region) - var results = [0, 13, 26, 39, 52] - - for try await time in fastClock.strike(producing: Second.self, when: { $0.second.isMultiple(of: 13) }).asyncValues { - XCTAssertEqual(time.second, results.removeFirst()) - if results.isEmpty { break } - } - - XCTAssertTrue(results.isEmpty) + let justBeforeTheNextMinute = clock.nextMinute.firstSecond.previous + + let fastClock = Clocks.custom( + startingFrom: justBeforeTheNextMinute.firstInstant, rate: 60.0, region: clock.region) + var results = [0, 13, 26, 39, 52] + + for try await time in fastClock.strike( + producing: Second.self, when: { $0.second.isMultiple(of: 13) } + ).asyncValues { + XCTAssertEqual(time.second, results.removeFirst()) + if results.isEmpty { break } + } + + XCTAssertTrue(results.isEmpty) } - -} + + } #else -final class ClockStrikeTests: XCTestCase { - static var allTests: [(String, (ClockStrikeTests) -> () throws -> ())] = [] -} + final class ClockStrikeTests: XCTestCase { + static var allTests: [(String, (ClockStrikeTests) -> () throws -> Void)] = [] + } #endif diff --git a/Tests/TimeTests/ClockTests.swift b/Tests/TimeTests/ClockTests.swift index 90c333e..9217710 100644 --- a/Tests/TimeTests/ClockTests.swift +++ b/Tests/TimeTests/ClockTests.swift @@ -1,244 +1,259 @@ import XCTest + @testable import Time extension Collection { - func slice(between: (Element, Element) -> Bool) -> Array { - var slices = Array() - - var startOfCurrentSlice = startIndex - - var previousIndex = startOfCurrentSlice - var currentIndex = index(after: startOfCurrentSlice) - - while currentIndex < endIndex { - let p = self[previousIndex] - let c = self[currentIndex] - - if between(p, c) { - slices.append(self[startOfCurrentSlice.. startOfCurrentSlice { - slices.append(self[startOfCurrentSlice ..< currentIndex]) - } - - return slices - } -} + func slice(between: (Element, Element) -> Bool) -> [SubSequence] { + var slices = [SubSequence]() -class ClockTests: XCTestCase { - - static var allTests = [ - ("testSystem", testSystem), - ("testExplicit", testExplicit), - ("testAccelerated_2x", testAccelerated_2x), - ("testAccelerated_10x", testAccelerated_10x), - ("testDecelerated_2x", testDecelerated_2x), - ("testDecelerated_10x", testDecelerated_10x), - ("testNextDSTTransitionForTimeZoneWithDST", testNextDSTTransitionForTimeZoneWithDST), - ("testNextDSTTransitionNextYearForTimeZoneWithDST", testNextDSTTransitionNextYearForTimeZoneWithDST), - ("testNextDSTTransitionForTimeZoneWithoutDST", testNextDSTTransitionForTimeZoneWithoutDST), - ("testNextDSTTransitionNextYearForTimeZoneWithoutDST", testNextDSTTransitionNextYearForTimeZoneWithoutDST) - ] - - func testWeeksInYear() { - let thisYear = Clocks.system.currentYear - let daysInTheYear = Array(thisYear.days) - - let weeks = daysInTheYear.slice(between: { $0.weekOfYear != $1.weekOfYear }) - let weekLengths = weeks.map(\.count) - XCTAssertTrue(weekLengths.allSatisfy({ $0 > 0 && $0 <= 7 })) - } - - func testSystem() { - - let c = Clocks.system - let now = c.now - - XCTAssertEqual(now.intervalSinceEpoch.timeInterval, Date.timeIntervalSinceReferenceDate, accuracy: 0.001) - } - - func testExplicit() { - let c = Clocks.posix - - let now = c.now - XCTAssertEqual(now.intervalSinceEpoch.timeInterval, Date.timeIntervalSinceReferenceDate, accuracy: 0.001) - - let today = c.today - XCTAssertEqual(today.region, c.region) - } - - func testAccelerated_2x() { - let now = Clocks.system.now - let c = Clocks.custom(startingFrom: now, rate: 2.0, region: Region.current) - - let thisSecond = c.now - wait(1) - let nextSecond = c.now - - let elapsedTime = nextSecond - thisSecond - - XCTAssertEqual(elapsedTime.timeInterval, 2.0, accuracy: 0.3) // 15% margin for error - } - - func testAccelerated_10x() { - let now = Clocks.system.now - let c = Clocks.custom(startingFrom: now, rate: 10.0, region: Region.current) - - let thisSecond = c.now - wait(1) - let nextSecond = c.now - - let elapsedTime = nextSecond - thisSecond - - // we need a larger margin for error here so the CI tests can handle this - XCTAssertEqual(elapsedTime.timeInterval, 10.0, accuracy: 1.5) // 15% margin for error - } - - func testDecelerated_2x() { - let now = Clocks.system.now - let c = Clocks.custom(startingFrom: now, rate: 0.5, region: Region.current) - - let thisSecond = c.now - wait(1) - let nextSecond = c.now - - let elapsedTime = nextSecond - thisSecond - - XCTAssertEqual(elapsedTime.timeInterval, 0.5, accuracy: 0.075) // 15% margin for error + var startOfCurrentSlice = startIndex + + var previousIndex = startOfCurrentSlice + var currentIndex = index(after: startOfCurrentSlice) + + while currentIndex < endIndex { + let p = self[previousIndex] + let c = self[currentIndex] + + if between(p, c) { + slices.append(self[startOfCurrentSlice.. startOfCurrentSlice { + slices.append(self[startOfCurrentSlice.. 0 && $0 <= 7 })) + } + + func testSystem() { + + let c = Clocks.system + let now = c.now + + XCTAssertEqual( + now.intervalSinceEpoch.timeInterval, Date.timeIntervalSinceReferenceDate, accuracy: 0.001) + } + + func testExplicit() { + let c = Clocks.posix + + let now = c.now + XCTAssertEqual( + now.intervalSinceEpoch.timeInterval, Date.timeIntervalSinceReferenceDate, accuracy: 0.001) + + let today = c.today + XCTAssertEqual(today.region, c.region) + } + + func testAccelerated_2x() { + let now = Clocks.system.now + let c = Clocks.custom(startingFrom: now, rate: 2.0, region: Region.current) + + let thisSecond = c.now + wait(1) + let nextSecond = c.now + + let elapsedTime = nextSecond - thisSecond + + XCTAssertEqual(elapsedTime.timeInterval, 2.0, accuracy: 0.3) // 15% margin for error + } + + func testAccelerated_10x() { + let now = Clocks.system.now + let c = Clocks.custom(startingFrom: now, rate: 10.0, region: Region.current) + + let thisSecond = c.now + wait(1) + let nextSecond = c.now + + let elapsedTime = nextSecond - thisSecond + + // we need a larger margin for error here so the CI tests can handle this + XCTAssertEqual(elapsedTime.timeInterval, 10.0, accuracy: 1.5) // 15% margin for error + } + + func testDecelerated_2x() { + let now = Clocks.system.now + let c = Clocks.custom(startingFrom: now, rate: 0.5, region: Region.current) + + let thisSecond = c.now + wait(1) + let nextSecond = c.now + + let elapsedTime = nextSecond - thisSecond + + XCTAssertEqual(elapsedTime.timeInterval, 0.5, accuracy: 0.075) // 15% margin for error + } + + func testDecelerated_10x() { + let now = Clocks.system.now + let c = Clocks.custom(startingFrom: now, rate: 0.1, region: Region.current) + + let thisSecond = c.now + wait(1) + let nextSecond = c.now + + let elapsedTime = nextSecond - thisSecond + + XCTAssertEqual(elapsedTime.timeInterval, 0.1, accuracy: 0.015) // 15% margin for error + } } // MARK: - Next Daylight Saving Time Transition +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ClockTests { - - func testNextDSTTransitionForTimeZoneWithDST() { - let timeZone = TimeZone(identifier: "Europe/London")! - - let region = Region( - calendar: .autoupdatingCurrent, - timeZone: timeZone, - locale: .autoupdatingCurrent) - - let clock = Clocks.system(in: region) - - let instant = clock.nextDaylightSavingTimeTransition() - XCTAssertNotNil(instant) - - guard let instant else { return } - - let accuracy = 0.000001 - if #available(macOS 14, iOS 17, watchOS 11, tvOS 17, *) { - XCTAssertEqualWithAccuracyWorkaround( - instant.intervalSinceReferenceEpoch.timeInterval, - timeZone.nextDaylightSavingTimeTransition?.timeIntervalSinceReferenceDate ?? 0, - accuracy: accuracy - ) - } else { - // https://github.com/apple/swift/pull/66111 - XCTAssertEqual( - instant.intervalSinceReferenceEpoch.timeInterval, - timeZone.nextDaylightSavingTimeTransition!.timeIntervalSinceReferenceDate, - accuracy: accuracy - ) - } - } - - func testNextDSTTransitionNextYearForTimeZoneWithDST() { - let timeZone = TimeZone(identifier: "Europe/London")! - - let region = Region( - calendar: .autoupdatingCurrent, - timeZone: timeZone, - locale: .autoupdatingCurrent) - - let clock = Clocks.system(in: region) - let instantNextYear = (clock.currentDay + .years(1)).firstInstant - - let nextDSTSeconds = clock - .nextDaylightSavingTimeTransition(after: instantNextYear)?.intervalSinceEpoch.timeInterval - - let expectedDSTSeconds = timeZone - .nextDaylightSavingTimeTransition(after: instantNextYear.date)?.timeIntervalSinceReferenceDate - - XCTAssertNotNil(nextDSTSeconds) - XCTAssertEqual(nextDSTSeconds, expectedDSTSeconds) - } - - func testNextDSTTransitionForTimeZoneWithoutDST() { - let timeZone = TimeZone(identifier: "Europe/Moscow")! - - let region = Region( - calendar: .autoupdatingCurrent, - timeZone: timeZone, - locale: .autoupdatingCurrent) - - let clock = Clocks.system(in: region) - - XCTAssertNil(clock.nextDaylightSavingTimeTransition()) - } - - func testNextDSTTransitionNextYearForTimeZoneWithoutDST() { - let timeZone = TimeZone(identifier: "Europe/Moscow")! - - let region = Region( - calendar: .autoupdatingCurrent, - timeZone: timeZone, - locale: .autoupdatingCurrent) - - let clock = Clocks.system(in: region) - let instantNextYear = (clock.currentDay + .years(1)).firstInstant - - let nextDSTSeconds = clock - .nextDaylightSavingTimeTransition(after: instantNextYear)?.intervalSinceEpoch.rawValue - - XCTAssertNil(nextDSTSeconds) + + func testNextDSTTransitionForTimeZoneWithDST() { + let timeZone = TimeZone(identifier: "Europe/London")! + + let region = Region( + calendar: .autoupdatingCurrent, + timeZone: timeZone, + locale: .autoupdatingCurrent) + + let clock = Clocks.system(in: region) + + let instant = clock.nextDaylightSavingTimeTransition() + XCTAssertNotNil(instant) + + guard let instant else { return } + + let accuracy = 0.000001 + if #available(macOS 14, iOS 17, watchOS 11, tvOS 17, *) { + XCTAssertEqualWithAccuracyWorkaround( + instant.intervalSinceReferenceEpoch.timeInterval, + timeZone.nextDaylightSavingTimeTransition?.timeIntervalSinceReferenceDate ?? 0, + accuracy: accuracy + ) + } else { + // https://github.com/apple/swift/pull/66111 + XCTAssertEqual( + instant.intervalSinceReferenceEpoch.timeInterval, + timeZone.nextDaylightSavingTimeTransition!.timeIntervalSinceReferenceDate, + accuracy: accuracy + ) } - + } + + func testNextDSTTransitionNextYearForTimeZoneWithDST() { + let timeZone = TimeZone(identifier: "Europe/London")! + + let region = Region( + calendar: .autoupdatingCurrent, + timeZone: timeZone, + locale: .autoupdatingCurrent) + + let clock = Clocks.system(in: region) + let instantNextYear = (clock.currentDay + .years(1)).firstInstant + + let nextDSTSeconds = + clock + .nextDaylightSavingTimeTransition(after: instantNextYear)?.intervalSinceEpoch.timeInterval + + let expectedDSTSeconds = + timeZone + .nextDaylightSavingTimeTransition(after: instantNextYear.date)?.timeIntervalSinceReferenceDate + + XCTAssertNotNil(nextDSTSeconds) + XCTAssertEqual(nextDSTSeconds, expectedDSTSeconds) + } + + func testNextDSTTransitionForTimeZoneWithoutDST() { + let timeZone = TimeZone(identifier: "Europe/Moscow")! + + let region = Region( + calendar: .autoupdatingCurrent, + timeZone: timeZone, + locale: .autoupdatingCurrent) + + let clock = Clocks.system(in: region) + + XCTAssertNil(clock.nextDaylightSavingTimeTransition()) + } + + func testNextDSTTransitionNextYearForTimeZoneWithoutDST() { + let timeZone = TimeZone(identifier: "Europe/Moscow")! + + let region = Region( + calendar: .autoupdatingCurrent, + timeZone: timeZone, + locale: .autoupdatingCurrent) + + let clock = Clocks.system(in: region) + let instantNextYear = (clock.currentDay + .years(1)).firstInstant + + let nextDSTSeconds = + clock + .nextDaylightSavingTimeTransition(after: instantNextYear)?.intervalSinceEpoch.rawValue + + XCTAssertNil(nextDSTSeconds) + } + } // MARK: Autoupdating Clocks +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ClockTests { - - func testAutoupdatingCalendarProducesStaticCalendar() { - let calendar = Calendar.autoupdatingCurrent - let r = Region(calendar: calendar, timeZone: .current, locale: .current) - let c = Clocks.system(in: r) - - let now = c.currentSecond - - XCTAssertNotEqual(now.calendar, calendar) - } - - func testAutoupdatingRegionProducesStaticRegion() { - let r = Region.autoupdatingCurrent - XCTAssertTrue(r.isAutoupdating) - - let c = Clocks.system(in: r) - XCTAssertTrue(c.region.isAutoupdating) - - let now = c.currentSecond - XCTAssertFalse(now.region.isAutoupdating) - } - + + func testAutoupdatingCalendarProducesStaticCalendar() { + let calendar = Calendar.autoupdatingCurrent + let r = Region(calendar: calendar, timeZone: .current, locale: .current) + let c = Clocks.system(in: r) + + let now = c.currentSecond + + XCTAssertNotEqual(now.calendar, calendar) + } + + func testAutoupdatingRegionProducesStaticRegion() { + let r = Region.autoupdatingCurrent + XCTAssertTrue(r.isAutoupdating) + + let c = Clocks.system(in: r) + XCTAssertTrue(c.region.isAutoupdating) + + let now = c.currentSecond + XCTAssertFalse(now.region.isAutoupdating) + } + } diff --git a/Tests/TimeTests/FixedFormattingTests.swift b/Tests/TimeTests/FixedFormattingTests.swift index 89886a3..f0e18f2 100644 --- a/Tests/TimeTests/FixedFormattingTests.swift +++ b/Tests/TimeTests/FixedFormattingTests.swift @@ -1,141 +1,167 @@ import XCTest + @testable import Time -class FixedFormattingTests: XCTestCase { - - static var allTests = [ - ("testEraFormatting", testEraFormatting), - ("testYearFormatting", testYearFormatting), - ("testMonthFormatting", testMonthFormatting), - ("testDayFormatting", testDayFormatting), - ("testRawFormatting_Strict", testRawFormatting_Strict), - ("testRawFormatting_Lenient", testRawFormatting_Lenient), - ] - - // create a clock that starts at the first instant of the reference era - // the slow rate is just to make sure that small units (seconds, etc) don't move faster than - // the unit tests can reasonably handle - let clock = Clocks.custom(startingFrom: Instant(interval: 0, since: .reference), rate: 0.001, region: .posix) - - func testEraFormatting() { - let v = clock.currentEra - - XCTAssertEqual(v.format(era: .wide), "Anno Domini") - XCTAssertEqual(v.format(era: .abbreviated), "AD") - XCTAssertEqual(v.format(era: .narrow), "A") - } - - func testYearFormatting() { - let v = clock.currentYear - - XCTAssertEqual(v.format(year: .naturalDigits), "2001") - XCTAssertEqual(v.format(year: .twoDigits), "01") - XCTAssertEqual(v.format(year: .digits(paddedToLength: 5)), "02001") - - XCTAssertEqual(v.format(era: .abbreviated, year: .naturalDigits), "2001 AD") - XCTAssertEqual(v.format(era: .abbreviated, year: .twoDigits), "01 AD") - XCTAssertEqual(v.format(era: .wide, year: .digits(paddedToLength: 5)), "02001 Anno Domini") - } - - func testMonthFormatting() { - let v = clock.currentMonth - - XCTAssertEqual(v.format(month: .naturalName), "January") - XCTAssertEqual(v.format(month: .abbreviatedName), "Jan") - XCTAssertEqual(v.format(month: .narrowName), "J") - XCTAssertEqual(v.format(month: .naturalDigits), "1") - XCTAssertEqual(v.format(month: .twoDigits), "01") - - XCTAssertEqual(v.format(year: .naturalDigits, month: .naturalName), "January 2001") - XCTAssertEqual(v.format(year: .naturalDigits, month: .abbreviatedName), "Jan 2001") - XCTAssertEqual(v.format(year: .naturalDigits, month: .narrowName), "J 2001") - XCTAssertEqual(v.format(year: .naturalDigits, month: .naturalDigits), "1/2001") - XCTAssertEqual(v.format(year: .naturalDigits, month: .twoDigits), "01/2001") - - XCTAssertEqual(v.format(era: .abbreviated, year: .naturalDigits, month: .naturalName), "January 2001 AD") - XCTAssertEqual(v.format(era: .abbreviated, year: .naturalDigits, month: .abbreviatedName), "Jan 2001 AD") - XCTAssertEqual(v.format(era: .abbreviated, year: .naturalDigits, month: .narrowName), "J 2001 AD") - XCTAssertEqual(v.format(era: .abbreviated, year: .naturalDigits, month: .naturalDigits), "1 2001 AD") - XCTAssertEqual(v.format(era: .abbreviated, year: .naturalDigits, month: .twoDigits), "01 2001 AD") - } - - func testDayFormatting() { - let v = clock.currentDay - - XCTAssertEqual(v.format(weekday: .naturalName), "Monday") - XCTAssertEqual(v.format(weekday: .abbreviatedName), "Mon") - XCTAssertEqual(v.format(weekday: .shortName), "Mo") - XCTAssertEqual(v.format(weekday: .narrowName), "M") - - XCTAssertEqual(v.format(day: .naturalDigits), "1") - XCTAssertEqual(v.format(day: .twoDigits), "01") - - XCTAssertEqual(v.format(month: .naturalName, day: .naturalDigits), "January 1") - XCTAssertEqual(v.format(month: .naturalName, day: .twoDigits), "January 01") - - XCTAssertEqual(v.format(year: .naturalDigits, month: .naturalName, day: .naturalDigits), "January 1, 2001") - XCTAssertEqual(v.format(year: .naturalDigits, month: .naturalName, day: .twoDigits), "January 01, 2001") - - XCTAssertEqual(v.format(era: .abbreviated, year: .naturalDigits, month: .naturalName, day: .naturalDigits), "January 1, 2001 AD") - XCTAssertEqual(v.format(era: .abbreviated, year: .naturalDigits, month: .naturalName, day: .twoDigits), "January 01, 2001 AD") - - XCTAssertEqual(v.format(year: .naturalDigits, month: .naturalName, day: .naturalDigits, weekday: .naturalName), "Monday, January 1, 2001") - XCTAssertEqual(v.format(year: .naturalDigits, month: .naturalName, day: .twoDigits, weekday: .naturalName), "Monday, January 01, 2001") - - XCTAssertEqual(v.format(date: .full), "Monday, January 1, 2001") - } - - func testRawFormatting_Strict() { - let v = clock.currentMonth - - XCTAssertNoThrow(try v.format(raw: "yyyy"), "This should not throw") - XCTAssertNoThrow(try v.format(raw: "y-MM"), "This should not throw") - XCTAssertNoThrow(try v.format(raw: "y-MM Q"), "This should not throw") - XCTAssertNoThrow(try v.format(raw: "y-MM 'some literal stuff'"), "This should not throw") - - XCTAssertThrowsError(try v.format(raw: "y-MM-dd"), "This should have thrown") - XCTAssertThrowsError(try v.format(raw: "y-MM HH"), "This should have thrown") - XCTAssertThrowsError(try v.format(raw: "y-MM mm"), "This should have thrown") - XCTAssertThrowsError(try v.format(raw: "y-MM ss"), "This should have thrown") - XCTAssertThrowsError(try v.format(raw: "y-MM SSSSSS"), "This should have thrown") - - XCTAssertThrowsError(try v.format(raw: "y-MM 't"), "This should have thrown") - } - - func testRawFormatting_Lenient() { - let v = clock.currentMonth - - XCTAssertEqual(try v.format(raw: "y-MM-dd", strict: false), "2001-01-01") - XCTAssertEqual(try v.format(raw: "y-MM-dd 'at' HH:mm:ss", strict: false), "2001-01-01 at 00:00:00") - } - - func testOmittedTimeZoneInDescription() throws { - let fixedNano = try Fixed(region: .posix, - year: 2024, month: 3, day: 3, - hour: 10, minute: 12, second: 24, nanosecond: 500_000_000) - - // these values have time of day components, - // therefore they should show the time zone - let fixedSecond = fixedNano.fixedSecond - let fixedMinute = fixedNano.fixedMinute - let fixedHour = fixedNano.fixedHour - - XCTAssertTrue(fixedNano.description.hasSuffix(" GMT")) - XCTAssertTrue(fixedSecond.description.hasSuffix(" GMT")) - XCTAssertTrue(fixedMinute.description.hasSuffix(" GMT")) - XCTAssertTrue(fixedHour.description.hasSuffix(" GMT")) - - // these values do not have time of day components, - // therefore they should not show the time zone - let fixedDay = fixedNano.fixedDay - let fixedMonth = fixedNano.fixedMonth - let fixedYear = fixedNano.fixedYear - let fixedEra = fixedNano.fixedEra - - XCTAssertFalse(fixedDay.description.hasSuffix(" GMT")) - XCTAssertFalse(fixedMonth.description.hasSuffix(" GMT")) - XCTAssertFalse(fixedYear.description.hasSuffix(" GMT")) - XCTAssertFalse(fixedEra.description.hasSuffix(" GMT")) - } - +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +class FixedFormattingTests: SkipUnavailableTestCase { + + static var allTests = [ + ("testEraFormatting", testEraFormatting), + ("testYearFormatting", testYearFormatting), + ("testMonthFormatting", testMonthFormatting), + ("testDayFormatting", testDayFormatting), + ("testRawFormatting_Strict", testRawFormatting_Strict), + ("testRawFormatting_Lenient", testRawFormatting_Lenient), + ] + + // create a clock that starts at the first instant of the reference era + // the slow rate is just to make sure that small units (seconds, etc) don't move faster than + // the unit tests can reasonably handle + var clock: (any RegionalClock)! + + override func setUpWithError() throws { + try super.setUpWithError() + clock = Clocks.custom( + startingFrom: Instant(interval: 0, since: .reference), rate: 0.001, region: .posix) + } + + func testEraFormatting() { + let v = clock.currentEra + + XCTAssertEqual(v.format(era: .wide), "Anno Domini") + XCTAssertEqual(v.format(era: .abbreviated), "AD") + XCTAssertEqual(v.format(era: .narrow), "A") + } + + func testYearFormatting() { + let v = clock.currentYear + + XCTAssertEqual(v.format(year: .naturalDigits), "2001") + XCTAssertEqual(v.format(year: .twoDigits), "01") + XCTAssertEqual(v.format(year: .digits(paddedToLength: 5)), "02001") + + XCTAssertEqual(v.format(era: .abbreviated, year: .naturalDigits), "2001 AD") + XCTAssertEqual(v.format(era: .abbreviated, year: .twoDigits), "01 AD") + XCTAssertEqual(v.format(era: .wide, year: .digits(paddedToLength: 5)), "02001 Anno Domini") + } + + func testMonthFormatting() { + let v = clock.currentMonth + + XCTAssertEqual(v.format(month: .naturalName), "January") + XCTAssertEqual(v.format(month: .abbreviatedName), "Jan") + XCTAssertEqual(v.format(month: .narrowName), "J") + XCTAssertEqual(v.format(month: .naturalDigits), "1") + XCTAssertEqual(v.format(month: .twoDigits), "01") + + XCTAssertEqual(v.format(year: .naturalDigits, month: .naturalName), "January 2001") + XCTAssertEqual(v.format(year: .naturalDigits, month: .abbreviatedName), "Jan 2001") + XCTAssertEqual(v.format(year: .naturalDigits, month: .narrowName), "J 2001") + XCTAssertEqual(v.format(year: .naturalDigits, month: .naturalDigits), "1/2001") + XCTAssertEqual(v.format(year: .naturalDigits, month: .twoDigits), "01/2001") + + XCTAssertEqual( + v.format(era: .abbreviated, year: .naturalDigits, month: .naturalName), "January 2001 AD") + XCTAssertEqual( + v.format(era: .abbreviated, year: .naturalDigits, month: .abbreviatedName), "Jan 2001 AD") + XCTAssertEqual( + v.format(era: .abbreviated, year: .naturalDigits, month: .narrowName), "J 2001 AD") + XCTAssertEqual( + v.format(era: .abbreviated, year: .naturalDigits, month: .naturalDigits), "1 2001 AD") + XCTAssertEqual( + v.format(era: .abbreviated, year: .naturalDigits, month: .twoDigits), "01 2001 AD") + } + + func testDayFormatting() { + let v = clock.currentDay + + XCTAssertEqual(v.format(weekday: .naturalName), "Monday") + XCTAssertEqual(v.format(weekday: .abbreviatedName), "Mon") + XCTAssertEqual(v.format(weekday: .shortName), "Mo") + XCTAssertEqual(v.format(weekday: .narrowName), "M") + + XCTAssertEqual(v.format(day: .naturalDigits), "1") + XCTAssertEqual(v.format(day: .twoDigits), "01") + + XCTAssertEqual(v.format(month: .naturalName, day: .naturalDigits), "January 1") + XCTAssertEqual(v.format(month: .naturalName, day: .twoDigits), "January 01") + + XCTAssertEqual( + v.format(year: .naturalDigits, month: .naturalName, day: .naturalDigits), "January 1, 2001") + XCTAssertEqual( + v.format(year: .naturalDigits, month: .naturalName, day: .twoDigits), "January 01, 2001") + + XCTAssertEqual( + v.format(era: .abbreviated, year: .naturalDigits, month: .naturalName, day: .naturalDigits), + "January 1, 2001 AD") + XCTAssertEqual( + v.format(era: .abbreviated, year: .naturalDigits, month: .naturalName, day: .twoDigits), + "January 01, 2001 AD") + + XCTAssertEqual( + v.format( + year: .naturalDigits, month: .naturalName, day: .naturalDigits, weekday: .naturalName), + "Monday, January 1, 2001") + XCTAssertEqual( + v.format(year: .naturalDigits, month: .naturalName, day: .twoDigits, weekday: .naturalName), + "Monday, January 01, 2001") + + XCTAssertEqual(v.format(date: .full), "Monday, January 1, 2001") + } + + func testRawFormatting_Strict() { + let v = clock.currentMonth + + XCTAssertNoThrow(try v.format(raw: "yyyy"), "This should not throw") + XCTAssertNoThrow(try v.format(raw: "y-MM"), "This should not throw") + XCTAssertNoThrow(try v.format(raw: "y-MM Q"), "This should not throw") + XCTAssertNoThrow(try v.format(raw: "y-MM 'some literal stuff'"), "This should not throw") + + XCTAssertThrowsError(try v.format(raw: "y-MM-dd"), "This should have thrown") + XCTAssertThrowsError(try v.format(raw: "y-MM HH"), "This should have thrown") + XCTAssertThrowsError(try v.format(raw: "y-MM mm"), "This should have thrown") + XCTAssertThrowsError(try v.format(raw: "y-MM ss"), "This should have thrown") + XCTAssertThrowsError(try v.format(raw: "y-MM SSSSSS"), "This should have thrown") + + XCTAssertThrowsError(try v.format(raw: "y-MM 't"), "This should have thrown") + } + + func testRawFormatting_Lenient() { + let v = clock.currentMonth + + XCTAssertEqual(try v.format(raw: "y-MM-dd", strict: false), "2001-01-01") + XCTAssertEqual( + try v.format(raw: "y-MM-dd 'at' HH:mm:ss", strict: false), "2001-01-01 at 00:00:00") + } + + func testOmittedTimeZoneInDescription() throws { + let fixedNano = try Fixed( + region: .posix, + year: 2024, month: 3, day: 3, + hour: 10, minute: 12, second: 24, nanosecond: 500_000_000) + + // these values have time of day components, + // therefore they should show the time zone + let fixedSecond = fixedNano.fixedSecond + let fixedMinute = fixedNano.fixedMinute + let fixedHour = fixedNano.fixedHour + + XCTAssertTrue(fixedNano.description.hasSuffix(" GMT")) + XCTAssertTrue(fixedSecond.description.hasSuffix(" GMT")) + XCTAssertTrue(fixedMinute.description.hasSuffix(" GMT")) + XCTAssertTrue(fixedHour.description.hasSuffix(" GMT")) + + // these values do not have time of day components, + // therefore they should not show the time zone + let fixedDay = fixedNano.fixedDay + let fixedMonth = fixedNano.fixedMonth + let fixedYear = fixedNano.fixedYear + let fixedEra = fixedNano.fixedEra + + XCTAssertFalse(fixedDay.description.hasSuffix(" GMT")) + XCTAssertFalse(fixedMonth.description.hasSuffix(" GMT")) + XCTAssertFalse(fixedYear.description.hasSuffix(" GMT")) + XCTAssertFalse(fixedEra.description.hasSuffix(" GMT")) + } + } diff --git a/Tests/TimeTests/FixedTests.swift b/Tests/TimeTests/FixedTests.swift index 06dcd75..d83395d 100644 --- a/Tests/TimeTests/FixedTests.swift +++ b/Tests/TimeTests/FixedTests.swift @@ -1,214 +1,282 @@ import XCTest + @testable import Time class FixedTests: XCTestCase { - - static var allTests = [ - ("testInitializingGregorianDateWithoutEraSucceeds", testInitializingGregorianDateWithoutEraSucceeds), - ("testInitializingGregorianDateWithEraSucceeds", testInitializingGregorianDateWithEraSucceeds), - ("testInitializingJapaneseDateWithoutEraFails", testInitializingJapaneseDateWithoutEraFails), - ("testInitializingJapaneseDateWithEraSucceeds", testInitializingJapaneseDateWithEraSucceeds), - ("testLastMonthOfYear", testLastMonthOfYear), - ("testLastDayOfMonth", testLastDayOfMonth), - ("testLastHourOfDay", testLastHourOfDay), - ("testLastMinuteOfHour", testLastMinuteOfHour), - ("testLastSecondOfMinute", testLastSecondOfMinute), - ("testAddingComponents", testAddingComponents), - ] - - func testInitializingGregorianDateWithoutEraSucceeds() throws { - let day = try Fixed(region: .posix, year: 1970, month: 4, day: 1) - XCTAssertEqual(day.era, 1) - } - - func testInitializingGregorianDateWithEraSucceeds() { - XCTAssertNoThrow(try Fixed(region: .posix, era: 1, year: 1970, month: 4, day: 1)) - } - - func testInitializingJapaneseDateWithoutEraFails() { - let japaneseRegion = Region(calendar: Calendar(identifier: .japanese), timeZone: Region.posix.timeZone, locale: Region.posix.locale) - XCTAssertThrowsError(try Fixed(region: japaneseRegion, year: 2, month: 3, day: 6)) - } - - func testInitializingJapaneseDateWithEraSucceeds() { - let japaneseRegion = Region(calendar: Calendar(identifier: .japanese), timeZone: Region.posix.timeZone, locale: Region.posix.locale) - // March 6, Reiwa 2 - XCTAssertNoThrow(try Fixed(region: japaneseRegion, era: 236, year: 2, month: 3, day: 6)) - } - - func testLastMonthOfYear() { - - let year = try! Fixed(region: .posix, era: 1, year: 2020) - let lastMonth = year.lastMonth - let expectedlastMonth = try! Fixed(region: .posix, era:1, year: 2020, month: 12) - - XCTAssertEqual(lastMonth, expectedlastMonth) - } - - func testLastDayOfMonth() { - - let month = try! Fixed(region: .posix, era: 1, year: 2020, month: 2) - let lastDay = month.lastDay - let expectedLastDay = try! Fixed(region: .posix, era:1, year: 2020, month: 2, day: 29) - - XCTAssertEqual(lastDay, expectedLastDay) - } - - func testLastHourOfDay() { - - let day = try! Fixed(region: .posix, era: 1, year: 2020, month: 2, day: 29) - let lastHour = day.lastHour - let expectedlastHour = try! Fixed(region: .posix, era:1, year: 2020, month: 2, day: 29, hour: 23) - - XCTAssertEqual(lastHour, expectedlastHour) - } - - func testLastMinuteOfHour() { - - let hour = try! Fixed(region: .posix, era: 1, year: 2020, month: 2, day: 29, hour: 13) - let lastMinute = hour.lastMinute - let expectedlastMinute = try! Fixed(region: .posix, era:1, year: 2020, month: 2, day: 29, hour: 13, minute: 59) - - XCTAssertEqual(lastMinute, expectedlastMinute) - } - - func testLastSecondOfMinute() { - - let minute = try! Fixed(region: .posix, era: 1, year: 2020, month: 2, day: 29, hour: 13, minute: 31) - let lastSecond = minute.lastSecond - let expectedlastSecond = try! Fixed(region: .posix, era:1, year: 2020, month: 2, day: 29, hour: 13, minute: 31, second: 59) - - XCTAssertEqual(lastSecond, expectedlastSecond) - } - - func testAddingComponents() throws { - let today: Fixed = Clocks.system.today - let todayAt12: Fixed = try today.setting(hour: 12, minute: 00) - - XCTAssertEqual(todayAt12.fixedDay, today) - XCTAssertEqual(todayAt12.hour, 12) - XCTAssertEqual(todayAt12.minute, 0) - } - - func testValuesWithDifferentInstantsAreStillEqual() throws { - let thisMinute = Clocks.system.currentMinute - - let firstSecond = thisMinute.firstSecond - let secondSecond = firstSecond.nextSecond - - XCTAssertNotEqual(firstSecond, secondSecond) - - let minuteOfFirstSecond = firstSecond.fixedMinute - let minuteOfSecondSecond = secondSecond.fixedMinute - XCTAssertEqual(minuteOfFirstSecond, minuteOfSecondSecond) - } - - func testTotalMinutesCalculations() throws { - let today = Clocks.system.today - - var start = try today.firstMinute.setting(hour: 7, minute: 30) - var end = try today.firstMinute.setting(hour: 20, minute: 45) - XCTAssertEqual(start.differenceInWholeMinutes(to: end).minutes, 795) - - start = try start.setting(hour: 11, minute: 0) - end = try end.setting(hour: 17, minute: 0) - XCTAssertEqual(start.differenceInWholeMinutes(to: end).minutes, 360) - - end = try end.setting(hour: 11, minute: 30) - XCTAssertEqual(start.differenceInWholeMinutes(to: end).minutes, 30) - } - - func testRounding() throws { - let t = try! Fixed(region: .posix, era: 1, year: 2024, month: 2, day: 3, hour: 14, minute: 27, second: 43, nanosecond: 499_000_000) - - XCTAssertEqual(t.nearestEra.era, 1) - XCTAssertEqual(t.nearestYear.year, 2024) - XCTAssertEqual(t.nearestMonth.month, 2) - XCTAssertEqual(t.nearestDay.day, 4) - XCTAssertEqual(t.nearestHour.hour, 14) - XCTAssertEqual(t.nearestMinute.minute, 28) - XCTAssertEqual(t.nearestSecond.second, 43) - - XCTAssertEqual(t.roundedToEra(direction: .forward).era, 1) - XCTAssertEqual(t.roundedToYear(direction: .forward).year, 2025) - XCTAssertEqual(t.roundedToMonth(direction: .forward).month, 3) - XCTAssertEqual(t.roundedToDay(direction: .forward).day, 4) - XCTAssertEqual(t.roundedToHour(direction: .forward).hour, 15) - XCTAssertEqual(t.roundedToMinute(direction: .forward).minute, 28) - XCTAssertEqual(t.roundedToSecond(direction: .forward).second, 44) - - XCTAssertEqual(t.roundedToEra(direction: .backward).era, 1) - XCTAssertEqual(t.roundedToYear(direction: .backward).year, 2024) - XCTAssertEqual(t.roundedToMonth(direction: .backward).month, 2) - XCTAssertEqual(t.roundedToDay(direction: .backward).day, 3) - XCTAssertEqual(t.roundedToHour(direction: .backward).hour, 14) - XCTAssertEqual(t.roundedToMinute(direction: .backward).minute, 27) - XCTAssertEqual(t.roundedToSecond(direction: .backward).second, 43) - - let t2 = try! Fixed(region: .posix, year: 2023, month: 12, day: 31, hour: 23, minute: 59, second: 42) - XCTAssertTime(t2.roundedToYear(direction: .backward), era: 1, year: 2023, month: 1, day: 1, hour: 0, minute: 0, second: 0) - XCTAssertTime(t2.roundedToMonth(direction: .backward), era: 1, year: 2023, month: 12, day: 1, hour: 0, minute: 0, second: 0) - XCTAssertTime(t2.roundedToDay(direction: .backward), era: 1, year: 2023, month: 12, day: 31, hour: 0, minute: 0, second: 0) - XCTAssertTime(t2.roundedToHour(direction: .backward), era: 1, year: 2023, month: 12, day: 31, hour: 23, minute: 0, second: 0) - XCTAssertTime(t2.roundedToMinute(direction: .backward), era: 1, year: 2023, month: 12, day: 31, hour: 23, minute: 59, second: 0) - - XCTAssertTime(t2.roundedToYear(direction: .forward), era: 1, year: 2024, month: 1, day: 1, hour: 0, minute: 0, second: 0) - XCTAssertTime(t2.roundedToMonth(direction: .forward), era: 1, year: 2024, month: 1, day: 1, hour: 0, minute: 0, second: 0) - XCTAssertTime(t2.roundedToDay(direction: .forward), era: 1, year: 2024, month: 1, day: 1, hour: 0, minute: 0, second: 0) - XCTAssertTime(t2.roundedToHour(direction: .forward), era: 1, year: 2024, month: 1, day: 1, hour: 0, minute: 0, second: 0) - XCTAssertTime(t2.roundedToMinute(direction: .forward), era: 1, year: 2024, month: 1, day: 1, hour: 0, minute: 0, second: 0) - - XCTAssertTime(t2.roundedToYear(direction: .nearest), era: 1, year: 2024, month: 1, day: 1, hour: 0, minute: 0, second: 0) - XCTAssertTime(t2.roundedToMonth(direction: .nearest), era: 1, year: 2024, month: 1, day: 1, hour: 0, minute: 0, second: 0) - XCTAssertTime(t2.roundedToDay(direction: .nearest), era: 1, year: 2024, month: 1, day: 1, hour: 0, minute: 0, second: 0) - XCTAssertTime(t2.roundedToHour(direction: .nearest), era: 1, year: 2024, month: 1, day: 1, hour: 0, minute: 0, second: 0) - XCTAssertTime(t2.roundedToMinute(direction: .nearest), era: 1, year: 2024, month: 1, day: 1, hour: 0, minute: 0, second: 0) - } - - func testSimpleMultipleRounding() throws { - let t = try! Fixed(region: .posix, year: 2024, month: 2, day: 10, hour: 18, minute: 52, second: 13) - - let r1 = t.roundedToNearestMultiple(of: .minutes(15)) - XCTAssertTime(r1, era: 1, year: 2024, month: 2, day: 10, hour: 18, minute: 45, second: 0) - - let r2 = t.roundedToNearestMultiple(of: .hours(3)) - XCTAssertTime(r2, era: 1, year: 2024, month: 2, day: 10, hour: 18, minute: 0, second: 0) - - let r3 = t.roundedToNearestMultiple(of: .minutes(29)) - XCTAssertTime(r3, era: 1, year: 2024, month: 2, day: 10, hour: 18, minute: 58, second: 0) - } - - func testBoundaryAlignedSequence() { - let start = try! Fixed(region: .posix, year: 2024, month: 2, day: 1, hour: 0, minute: 0, second: 0) - let s = BoundaryAlignedSequence(start: start, stride: .minutes(29), boundaryStride: .hours(1)) - - let values = Array(s.prefix(6)) - - XCTAssertEqual(values.count, 6) - XCTAssertTime(values[0], era: 1, year: 2024, month: 2, day: 1, hour: 0, minute: 0, second: 0) - XCTAssertTime(values[1], era: 1, year: 2024, month: 2, day: 1, hour: 0, minute: 29, second: 0) - XCTAssertTime(values[2], era: 1, year: 2024, month: 2, day: 1, hour: 0, minute: 58, second: 0) - XCTAssertTime(values[3], era: 1, year: 2024, month: 2, day: 1, hour: 1, minute: 0, second: 0) - XCTAssertTime(values[4], era: 1, year: 2024, month: 2, day: 1, hour: 1, minute: 29, second: 0) - XCTAssertTime(values[5], era: 1, year: 2024, month: 2, day: 1, hour: 1, minute: 58, second: 0) + + static var allTests = [ + ( + "testInitializingGregorianDateWithoutEraSucceeds", + testInitializingGregorianDateWithoutEraSucceeds + ), + ("testInitializingGregorianDateWithEraSucceeds", testInitializingGregorianDateWithEraSucceeds), + ("testInitializingJapaneseDateWithoutEraFails", testInitializingJapaneseDateWithoutEraFails), + ("testInitializingJapaneseDateWithEraSucceeds", testInitializingJapaneseDateWithEraSucceeds), + ("testLastMonthOfYear", testLastMonthOfYear), + ("testLastDayOfMonth", testLastDayOfMonth), + ("testLastHourOfDay", testLastHourOfDay), + ("testLastMinuteOfHour", testLastMinuteOfHour), + ("testLastSecondOfMinute", testLastSecondOfMinute), + ] + + func testInitializingGregorianDateWithoutEraSucceeds() throws { + let day = try Fixed(region: .posix, year: 1970, month: 4, day: 1) + XCTAssertEqual(day.era, 1) + } + + func testInitializingGregorianDateWithEraSucceeds() { + XCTAssertNoThrow(try Fixed(region: .posix, era: 1, year: 1970, month: 4, day: 1)) + } + + func testInitializingJapaneseDateWithoutEraFails() { + let japaneseRegion = Region( + calendar: Calendar(identifier: .japanese), timeZone: Region.posix.timeZone, + locale: Region.posix.locale) + XCTAssertThrowsError(try Fixed(region: japaneseRegion, year: 2, month: 3, day: 6)) + } + + func testInitializingJapaneseDateWithEraSucceeds() { + let japaneseRegion = Region( + calendar: Calendar(identifier: .japanese), timeZone: Region.posix.timeZone, + locale: Region.posix.locale) + // March 6, Reiwa 2 + XCTAssertNoThrow(try Fixed(region: japaneseRegion, era: 236, year: 2, month: 3, day: 6)) + } + + func testLastMonthOfYear() { + + let year = try! Fixed(region: .posix, era: 1, year: 2020) + let lastMonth = year.lastMonth + let expectedlastMonth = try! Fixed(region: .posix, era: 1, year: 2020, month: 12) + + XCTAssertEqual(lastMonth, expectedlastMonth) + } + + func testLastDayOfMonth() { + + let month = try! Fixed(region: .posix, era: 1, year: 2020, month: 2) + let lastDay = month.lastDay + let expectedLastDay = try! Fixed(region: .posix, era: 1, year: 2020, month: 2, day: 29) + + XCTAssertEqual(lastDay, expectedLastDay) + } + + func testLastHourOfDay() { + + let day = try! Fixed(region: .posix, era: 1, year: 2020, month: 2, day: 29) + let lastHour = day.lastHour + let expectedlastHour = try! Fixed( + region: .posix, era: 1, year: 2020, month: 2, day: 29, hour: 23) + + XCTAssertEqual(lastHour, expectedlastHour) + } + + func testLastMinuteOfHour() { + + let hour = try! Fixed(region: .posix, era: 1, year: 2020, month: 2, day: 29, hour: 13) + let lastMinute = hour.lastMinute + let expectedlastMinute = try! Fixed( + region: .posix, era: 1, year: 2020, month: 2, day: 29, hour: 13, minute: 59) + + XCTAssertEqual(lastMinute, expectedlastMinute) + } + + func testLastSecondOfMinute() { + + let minute = try! Fixed( + region: .posix, era: 1, year: 2020, month: 2, day: 29, hour: 13, minute: 31) + let lastSecond = minute.lastSecond + let expectedlastSecond = try! Fixed( + region: .posix, era: 1, year: 2020, month: 2, day: 29, hour: 13, minute: 31, second: 59) + + XCTAssertEqual(lastSecond, expectedlastSecond) + } + + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + func testAddingComponents() throws { + if #unavailable(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, macCatalyst 16.0) { + try XCTSkipIf( + true, + "Skipping unrelated platform test" + ) } - - func testValidConversion() { - let d1 = try! Fixed(region: .posix, year: 2024, month: 2, day: 1, hour: 12) - - let d1_inPST = d1.converted(to: TimeZone(identifier: "America/Los_Angeles")!) - XCTAssertEqual(d1.range, d1_inPST.range) - XCTAssertTime(d1_inPST, era: 1, year: 2024, month: 2, day: 1, hour: 4) - - let d1_inPersian = d1.converted(to: Calendar(identifier: .persian)) - XCTAssertEqual(d1.range, d1_inPersian.range) - XCTAssertTime(d1_inPersian, era: 0, year: 1402, month: 11, day: 12, hour: 12) + + let today: Fixed = Clocks.system.today + let todayAt12: Fixed = try today.setting(hour: 12, minute: 00) + + XCTAssertEqual(todayAt12.fixedDay, today) + XCTAssertEqual(todayAt12.hour, 12) + XCTAssertEqual(todayAt12.minute, 0) + } + + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + func testValuesWithDifferentInstantsAreStillEqual() throws { + if #unavailable(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, macCatalyst 16.0) { + try XCTSkipIf( + true, + "Skipping unrelated platform test" + ) } - - func testInvalidConversion() { - let dec30 = try! Fixed(region: .posix, year: 2011, month: 12, day: 30) - - XCTAssertThrowsError(try dec30.converted(to: TimeZone(identifier: "Pacific/Apia")!)) - + let thisMinute = Clocks.system.currentMinute + + let firstSecond = thisMinute.firstSecond + let secondSecond = firstSecond.nextSecond + + XCTAssertNotEqual(firstSecond, secondSecond) + + let minuteOfFirstSecond = firstSecond.fixedMinute + let minuteOfSecondSecond = secondSecond.fixedMinute + XCTAssertEqual(minuteOfFirstSecond, minuteOfSecondSecond) + } + + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + func testTotalMinutesCalculations() throws { + if #unavailable(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, macCatalyst 16.0) { + try XCTSkipIf( + true, + "Skipping unrelated platform test" + ) } + let today = Clocks.system.today + + var start = try today.firstMinute.setting(hour: 7, minute: 30) + var end = try today.firstMinute.setting(hour: 20, minute: 45) + XCTAssertEqual(start.differenceInWholeMinutes(to: end).minutes, 795) + + start = try start.setting(hour: 11, minute: 0) + end = try end.setting(hour: 17, minute: 0) + XCTAssertEqual(start.differenceInWholeMinutes(to: end).minutes, 360) + + end = try end.setting(hour: 11, minute: 30) + XCTAssertEqual(start.differenceInWholeMinutes(to: end).minutes, 30) + } + + func testRounding() throws { + let t = try! Fixed( + region: .posix, era: 1, year: 2024, month: 2, day: 3, hour: 14, minute: 27, second: 43, + nanosecond: 499_000_000) + + XCTAssertEqual(t.nearestEra.era, 1) + XCTAssertEqual(t.nearestYear.year, 2024) + XCTAssertEqual(t.nearestMonth.month, 2) + XCTAssertEqual(t.nearestDay.day, 4) + XCTAssertEqual(t.nearestHour.hour, 14) + XCTAssertEqual(t.nearestMinute.minute, 28) + XCTAssertEqual(t.nearestSecond.second, 43) + + XCTAssertEqual(t.roundedToEra(direction: .forward).era, 1) + XCTAssertEqual(t.roundedToYear(direction: .forward).year, 2025) + XCTAssertEqual(t.roundedToMonth(direction: .forward).month, 3) + XCTAssertEqual(t.roundedToDay(direction: .forward).day, 4) + XCTAssertEqual(t.roundedToHour(direction: .forward).hour, 15) + XCTAssertEqual(t.roundedToMinute(direction: .forward).minute, 28) + XCTAssertEqual(t.roundedToSecond(direction: .forward).second, 44) + + XCTAssertEqual(t.roundedToEra(direction: .backward).era, 1) + XCTAssertEqual(t.roundedToYear(direction: .backward).year, 2024) + XCTAssertEqual(t.roundedToMonth(direction: .backward).month, 2) + XCTAssertEqual(t.roundedToDay(direction: .backward).day, 3) + XCTAssertEqual(t.roundedToHour(direction: .backward).hour, 14) + XCTAssertEqual(t.roundedToMinute(direction: .backward).minute, 27) + XCTAssertEqual(t.roundedToSecond(direction: .backward).second, 43) + + let t2 = try! Fixed( + region: .posix, year: 2023, month: 12, day: 31, hour: 23, minute: 59, second: 42) + XCTAssertTime( + t2.roundedToYear(direction: .backward), era: 1, year: 2023, month: 1, day: 1, hour: 0, + minute: 0, second: 0) + XCTAssertTime( + t2.roundedToMonth(direction: .backward), era: 1, year: 2023, month: 12, day: 1, hour: 0, + minute: 0, second: 0) + XCTAssertTime( + t2.roundedToDay(direction: .backward), era: 1, year: 2023, month: 12, day: 31, hour: 0, + minute: 0, second: 0) + XCTAssertTime( + t2.roundedToHour(direction: .backward), era: 1, year: 2023, month: 12, day: 31, hour: 23, + minute: 0, second: 0) + XCTAssertTime( + t2.roundedToMinute(direction: .backward), era: 1, year: 2023, month: 12, day: 31, hour: 23, + minute: 59, second: 0) + + XCTAssertTime( + t2.roundedToYear(direction: .forward), era: 1, year: 2024, month: 1, day: 1, hour: 0, + minute: 0, second: 0) + XCTAssertTime( + t2.roundedToMonth(direction: .forward), era: 1, year: 2024, month: 1, day: 1, hour: 0, + minute: 0, second: 0) + XCTAssertTime( + t2.roundedToDay(direction: .forward), era: 1, year: 2024, month: 1, day: 1, hour: 0, + minute: 0, second: 0) + XCTAssertTime( + t2.roundedToHour(direction: .forward), era: 1, year: 2024, month: 1, day: 1, hour: 0, + minute: 0, second: 0) + XCTAssertTime( + t2.roundedToMinute(direction: .forward), era: 1, year: 2024, month: 1, day: 1, hour: 0, + minute: 0, second: 0) + + XCTAssertTime( + t2.roundedToYear(direction: .nearest), era: 1, year: 2024, month: 1, day: 1, hour: 0, + minute: 0, second: 0) + XCTAssertTime( + t2.roundedToMonth(direction: .nearest), era: 1, year: 2024, month: 1, day: 1, hour: 0, + minute: 0, second: 0) + XCTAssertTime( + t2.roundedToDay(direction: .nearest), era: 1, year: 2024, month: 1, day: 1, hour: 0, + minute: 0, second: 0) + XCTAssertTime( + t2.roundedToHour(direction: .nearest), era: 1, year: 2024, month: 1, day: 1, hour: 0, + minute: 0, second: 0) + XCTAssertTime( + t2.roundedToMinute(direction: .nearest), era: 1, year: 2024, month: 1, day: 1, hour: 0, + minute: 0, second: 0) + } + + func testSimpleMultipleRounding() throws { + let t = try! Fixed( + region: .posix, year: 2024, month: 2, day: 10, hour: 18, minute: 52, second: 13) + + let r1 = t.roundedToNearestMultiple(of: .minutes(15)) + XCTAssertTime(r1, era: 1, year: 2024, month: 2, day: 10, hour: 18, minute: 45, second: 0) + + let r2 = t.roundedToNearestMultiple(of: .hours(3)) + XCTAssertTime(r2, era: 1, year: 2024, month: 2, day: 10, hour: 18, minute: 0, second: 0) + + let r3 = t.roundedToNearestMultiple(of: .minutes(29)) + XCTAssertTime(r3, era: 1, year: 2024, month: 2, day: 10, hour: 18, minute: 58, second: 0) + } + + func testBoundaryAlignedSequence() { + let start = try! Fixed( + region: .posix, year: 2024, month: 2, day: 1, hour: 0, minute: 0, second: 0) + let s = BoundaryAlignedSequence(start: start, stride: .minutes(29), boundaryStride: .hours(1)) + + let values = Array(s.prefix(6)) + + XCTAssertEqual(values.count, 6) + XCTAssertTime(values[0], era: 1, year: 2024, month: 2, day: 1, hour: 0, minute: 0, second: 0) + XCTAssertTime(values[1], era: 1, year: 2024, month: 2, day: 1, hour: 0, minute: 29, second: 0) + XCTAssertTime(values[2], era: 1, year: 2024, month: 2, day: 1, hour: 0, minute: 58, second: 0) + XCTAssertTime(values[3], era: 1, year: 2024, month: 2, day: 1, hour: 1, minute: 0, second: 0) + XCTAssertTime(values[4], era: 1, year: 2024, month: 2, day: 1, hour: 1, minute: 29, second: 0) + XCTAssertTime(values[5], era: 1, year: 2024, month: 2, day: 1, hour: 1, minute: 58, second: 0) + } + + func testValidConversion() { + let d1 = try! Fixed(region: .posix, year: 2024, month: 2, day: 1, hour: 12) + + let d1_inPST = d1.converted(to: TimeZone(identifier: "America/Los_Angeles")!) + XCTAssertEqual(d1.range, d1_inPST.range) + XCTAssertTime(d1_inPST, era: 1, year: 2024, month: 2, day: 1, hour: 4) + + let d1_inPersian = d1.converted(to: Calendar(identifier: .persian)) + XCTAssertEqual(d1.range, d1_inPersian.range) + XCTAssertTime(d1_inPersian, era: 0, year: 1402, month: 11, day: 12, hour: 12) + } + + func testInvalidConversion() { + let dec30 = try! Fixed(region: .posix, year: 2011, month: 12, day: 30) + + XCTAssertThrowsError(try dec30.converted(to: TimeZone(identifier: "Pacific/Apia")!)) + + } } diff --git a/Tests/TimeTests/LinuxIssues.swift b/Tests/TimeTests/LinuxIssues.swift index 6417db8..872f8c4 100644 --- a/Tests/TimeTests/LinuxIssues.swift +++ b/Tests/TimeTests/LinuxIssues.swift @@ -1,148 +1,169 @@ import Foundation import XCTest + @testable import Time class LinuxIssues: XCTestCase { - - func testFormatting() throws { - /* + + func testFormatting() throws { + /* While working on the fix for Issue #75, it became apparent that Linux is not formatting dates correctly. - + This was manifest in the unit tests when formatting a date using the "c" format. This format should be the numeric day of the week, but on Linux it was returning the abbreviated name ("Sun" instead of "1", for example */ - let now = try Fixed(region: .posix, year: 2024, month: 3, day: 3, hour: 6, minute: 3, second: 1, nanosecond: 123_000_000) - - let allFormats: Array<(Format, String, String, StaticString, UInt)> = [ - // Date Format Template Raw Format Value Template Format Value File Line - (Template.abbreviated, "AD", "AD", #file, #line), - (Template.narrow, "A", "A", #file, #line), - (Template.wide, "Anno Domini", "Anno Domini", #file, #line), - - (Template.naturalDigits, "2024", "2024", #file, #line), - (Template.twoDigits, "24", "24", #file, #line), - (Template.digits(paddedToLength: 3), "2024", "2024", #file, #line), - (Template.digits(paddedToLength: 4), "2024", "2024", #file, #line), - (Template.digits(paddedToLength: 5), "02024", "02024", #file, #line), - - (Template.naturalName, "March", "March", #file, #line), - (Template.abbreviatedName, "Mar", "Mar", #file, #line), - (Template.narrowName, "M", "M", #file, #line), - (Template.naturalDigits, "3", "3", #file, #line), - (Template.twoDigits, "03", "03", #file, #line), - - (Template>.naturalDigits, "3", "3", #file, #line), - (Template>.twoDigits, "03", "03", #file, #line), - (Template>.naturalName, "March", "March", #file, #line), - (Template>.abbreviatedName, "Mar", "Mar", #file, #line), - (Template>.narrowName, "M", "M", #file, #line), - - (Template.naturalDigits, "3", "3", #file, #line), - (Template.twoDigits, "03", "03", #file, #line), - - (Template.naturalDigits, "1", "1", #file, #line), - (Template.twoDigits, "01", "1", #file, #line), - (Template.naturalName, "Sunday", "Sunday", #file, #line), - (Template.abbreviatedName, "Sun", "Sun", #file, #line), - (Template.shortName, "Su", "Su", #file, #line), - (Template.narrowName, "S", "S", #file, #line), - - (Template>.naturalDigits, "1", "1", #file, #line), - (Template>.naturalName, "Sunday", "Sunday", #file, #line), - (Template>.abbreviatedName, "Sun", "Sun", #file, #line), - (Template>.shortName, "Su", "Su", #file, #line), - (Template>.narrowName, "S", "S", #file, #line), - - (Template.natural, "AM", "AM", #file, #line), - (Template.wide, "AM", "AM", #file, #line), - (Template.narrow, "a", "a", #file, #line), - - // the Template methods can't be used here, because they define templates, - // and this unit test will use the underlying `.template` as a raw dateFormat - (Template("h"), "6", "6 AM", #file, #line), - (Template("h a"), "6 AM", "6 AM", #file, #line), - (Template("h aaaa"), "6 AM", "6 AM", #file, #line), - (Template("h aaaaa"), "6 a", "6 a", #file, #line), - - (Template("hh"), "06", "6 AM", #file, #line), - (Template("hh a"), "06 AM", "6 AM", #file, #line), - (Template("hh aaaa"), "06 AM", "6 AM", #file, #line), - (Template("hh aaaaa"), "06 a", "6 a", #file, #line), - - (Template("H"), "6", "06", #file, #line), - (Template("H a"), "6 AM", "06", #file, #line), - (Template("H aaaa"), "6 AM", "06", #file, #line), - (Template("H aaaaa"), "6 a", "06", #file, #line), - - (Template("HH"), "06", "06", #file, #line), - (Template("HH a"), "06 AM", "06", #file, #line), - (Template("HH aaaa"), "06 AM", "06", #file, #line), - (Template("HH aaaaa"), "06 a", "06", #file, #line), - - (Template("k"), "6", "06", #file, #line), - (Template("k a"), "6 AM", "06", #file, #line), - (Template("k aaaa"), "6 AM", "06", #file, #line), - (Template("k aaaaa"), "6 a", "06", #file, #line), - - (Template("kk"), "06", "06", #file, #line), - (Template("kk a"), "06 AM", "06", #file, #line), - (Template("kk aaaa"), "06 AM", "06", #file, #line), - (Template("kk aaaaa"), "06 a", "06", #file, #line), - - (Template("K"), "6", "6 AM", #file, #line), - (Template("K a"), "6 AM", "6 AM", #file, #line), - (Template("K aaaa"), "6 AM", "6 AM", #file, #line), - (Template("K aaaaa"), "6 a", "6 a", #file, #line), - - (Template("KK"), "06", "6 AM", #file, #line), - (Template("KK a"), "06 AM", "6 AM", #file, #line), - (Template("KK aaaa"), "06 AM", "6 AM", #file, #line), - (Template("KK aaaaa"), "06 a", "6 a", #file, #line), - - (Template.naturalDigits, "3", "3", #file, #line), - (Template.twoDigits, "03", "3", #file, #line), - - (Template.naturalDigits, "1", "1", #file, #line), - (Template.twoDigits, "01", "1", #file, #line), - - (Template.digits(1), "1", "1", #file, #line), - (Template.digits(2), "12", "12", #file, #line), - (Template.digits(3), "123", "123", #file, #line), - - (Template.shortSpecific, "GMT", "GMT", #file, #line), - (Template.longSpecific, "Greenwich Mean Time", "Greenwich Mean Time", #file, #line), - (Template.ISO8601Basic, "+0000", "+0000", #file, #line), - (Template.ISO8601Extended, "Z", "Z", #file, #line), - (Template.shortLocalizedGMT, "GMT", "GMT", #file, #line), - (Template.longLocalizedGMT, "GMT", "GMT", #file, #line), - (Template.shortGeneric, "GMT", "GMT", #file, #line), - (Template.longGeneric, "Greenwich Mean Time", "Greenwich Mean Time", #file, #line), - (Template.shortID, "gmt", "gmt", #file, #line), - (Template.longID, "GMT", "GMT", #file, #line), - (Template.exemplarCity, "Unknown City", "Unknown City", #file, #line), - (Template.genericLocation, "GMT", "GMT", #file, #line), - (Template.ISO8601BasicWithHours(includingZ: true), "Z", "Z", #file, #line), - (Template.ISO8601BasicWithHours(includingZ: false), "+00", "+00", #file, #line), - (Template.ISO8601WithHoursAndMinutes(extended: false, includingZ: false), "+0000", "+0000", #file, #line), - (Template.ISO8601WithHoursAndMinutes(extended: false, includingZ: true), "Z", "Z", #file, #line), - (Template.ISO8601WithHoursAndMinutes(extended: true, includingZ: false), "+00:00", "+00:00", #file, #line), - (Template.ISO8601WithHoursAndMinutes(extended: true, includingZ: true), "Z", "Z", #file, #line), - ] - - for (format, expectedRaw, expectedTemplate, file, line) in allFormats { - let rawStyle = FixedFormat(raw: format.template) - let templateStyle = FixedFormat(templates: [format]) - - // on macOS, some of the formats use unusual whitespace characters in format string - // this replaces them with plain whitespace to make comparison a bit more consistent. - // You should not do this in a production environment. - let rawFormatted = String(now.format(rawStyle).map { $0.isWhitespace ? " " : $0 }) - let templateFormatted = String(now.format(templateStyle).map { $0.isWhitespace ? " " : $0 }) - - XCTAssertEqual(rawFormatted, expectedRaw, "Raw format '\(format.template)' produced '\(rawFormatted)' instead of '\(expectedRaw)'", file: file, line: line) - XCTAssertEqual(templateFormatted, expectedTemplate, "Template '\(format.template)' produced '\(templateFormatted)' instead of '\(expectedTemplate)'", file: file, line: line) - } - + let now = try Fixed( + region: .posix, year: 2024, month: 3, day: 3, hour: 6, minute: 3, second: 1, + nanosecond: 123_000_000) + + let allFormats: [(Format, String, String, StaticString, UInt)] = [ + // Date Format Template Raw Format Value Template Format Value File Line + (Template.abbreviated, "AD", "AD", #file, #line), + (Template.narrow, "A", "A", #file, #line), + (Template.wide, "Anno Domini", "Anno Domini", #file, #line), + + (Template.naturalDigits, "2024", "2024", #file, #line), + (Template.twoDigits, "24", "24", #file, #line), + (Template.digits(paddedToLength: 3), "2024", "2024", #file, #line), + (Template.digits(paddedToLength: 4), "2024", "2024", #file, #line), + (Template.digits(paddedToLength: 5), "02024", "02024", #file, #line), + + (Template.naturalName, "March", "March", #file, #line), + (Template.abbreviatedName, "Mar", "Mar", #file, #line), + (Template.narrowName, "M", "M", #file, #line), + (Template.naturalDigits, "3", "3", #file, #line), + (Template.twoDigits, "03", "03", #file, #line), + + (Template>.naturalDigits, "3", "3", #file, #line), + (Template>.twoDigits, "03", "03", #file, #line), + (Template>.naturalName, "March", "March", #file, #line), + (Template>.abbreviatedName, "Mar", "Mar", #file, #line), + (Template>.narrowName, "M", "M", #file, #line), + + (Template.naturalDigits, "3", "3", #file, #line), + (Template.twoDigits, "03", "03", #file, #line), + + (Template.naturalDigits, "1", "1", #file, #line), + (Template.twoDigits, "01", "1", #file, #line), + (Template.naturalName, "Sunday", "Sunday", #file, #line), + (Template.abbreviatedName, "Sun", "Sun", #file, #line), + (Template.shortName, "Su", "Su", #file, #line), + (Template.narrowName, "S", "S", #file, #line), + + (Template>.naturalDigits, "1", "1", #file, #line), + (Template>.naturalName, "Sunday", "Sunday", #file, #line), + (Template>.abbreviatedName, "Sun", "Sun", #file, #line), + (Template>.shortName, "Su", "Su", #file, #line), + (Template>.narrowName, "S", "S", #file, #line), + + (Template.natural, "AM", "AM", #file, #line), + (Template.wide, "AM", "AM", #file, #line), + (Template.narrow, "a", "a", #file, #line), + + // the Template methods can't be used here, because they define templates, + // and this unit test will use the underlying `.template` as a raw dateFormat + (Template("h"), "6", "6 AM", #file, #line), + (Template("h a"), "6 AM", "6 AM", #file, #line), + (Template("h aaaa"), "6 AM", "6 AM", #file, #line), + (Template("h aaaaa"), "6 a", "6 a", #file, #line), + + (Template("hh"), "06", "6 AM", #file, #line), + (Template("hh a"), "06 AM", "6 AM", #file, #line), + (Template("hh aaaa"), "06 AM", "6 AM", #file, #line), + (Template("hh aaaaa"), "06 a", "6 a", #file, #line), + + (Template("H"), "6", "06", #file, #line), + (Template("H a"), "6 AM", "06", #file, #line), + (Template("H aaaa"), "6 AM", "06", #file, #line), + (Template("H aaaaa"), "6 a", "06", #file, #line), + + (Template("HH"), "06", "06", #file, #line), + (Template("HH a"), "06 AM", "06", #file, #line), + (Template("HH aaaa"), "06 AM", "06", #file, #line), + (Template("HH aaaaa"), "06 a", "06", #file, #line), + + (Template("k"), "6", "06", #file, #line), + (Template("k a"), "6 AM", "06", #file, #line), + (Template("k aaaa"), "6 AM", "06", #file, #line), + (Template("k aaaaa"), "6 a", "06", #file, #line), + + (Template("kk"), "06", "06", #file, #line), + (Template("kk a"), "06 AM", "06", #file, #line), + (Template("kk aaaa"), "06 AM", "06", #file, #line), + (Template("kk aaaaa"), "06 a", "06", #file, #line), + + (Template("K"), "6", "6 AM", #file, #line), + (Template("K a"), "6 AM", "6 AM", #file, #line), + (Template("K aaaa"), "6 AM", "6 AM", #file, #line), + (Template("K aaaaa"), "6 a", "6 a", #file, #line), + + (Template("KK"), "06", "6 AM", #file, #line), + (Template("KK a"), "06 AM", "6 AM", #file, #line), + (Template("KK aaaa"), "06 AM", "6 AM", #file, #line), + (Template("KK aaaaa"), "06 a", "6 a", #file, #line), + + (Template.naturalDigits, "3", "3", #file, #line), + (Template.twoDigits, "03", "3", #file, #line), + + (Template.naturalDigits, "1", "1", #file, #line), + (Template.twoDigits, "01", "1", #file, #line), + + (Template.digits(1), "1", "1", #file, #line), + (Template.digits(2), "12", "12", #file, #line), + (Template.digits(3), "123", "123", #file, #line), + + (Template.shortSpecific, "GMT", "GMT", #file, #line), + (Template.longSpecific, "Greenwich Mean Time", "Greenwich Mean Time", #file, #line), + (Template.ISO8601Basic, "+0000", "+0000", #file, #line), + (Template.ISO8601Extended, "Z", "Z", #file, #line), + (Template.shortLocalizedGMT, "GMT", "GMT", #file, #line), + (Template.longLocalizedGMT, "GMT", "GMT", #file, #line), + (Template.shortGeneric, "GMT", "GMT", #file, #line), + (Template.longGeneric, "Greenwich Mean Time", "Greenwich Mean Time", #file, #line), + (Template.shortID, "gmt", "gmt", #file, #line), + (Template.longID, "GMT", "GMT", #file, #line), + (Template.exemplarCity, "Unknown City", "Unknown City", #file, #line), + (Template.genericLocation, "GMT", "GMT", #file, #line), + (Template.ISO8601BasicWithHours(includingZ: true), "Z", "Z", #file, #line), + (Template.ISO8601BasicWithHours(includingZ: false), "+00", "+00", #file, #line), + ( + Template.ISO8601WithHoursAndMinutes(extended: false, includingZ: false), "+0000", + "+0000", #file, #line + ), + ( + Template.ISO8601WithHoursAndMinutes(extended: false, includingZ: true), "Z", "Z", + #file, #line + ), + ( + Template.ISO8601WithHoursAndMinutes(extended: true, includingZ: false), "+00:00", + "+00:00", #file, #line + ), + ( + Template.ISO8601WithHoursAndMinutes(extended: true, includingZ: true), "Z", "Z", + #file, #line + ), + ] + + for (format, expectedRaw, expectedTemplate, file, line) in allFormats { + let rawStyle = FixedFormat(raw: format.template) + let templateStyle = FixedFormat(templates: [format]) + + // on macOS, some of the formats use unusual whitespace characters in format string + // this replaces them with plain whitespace to make comparison a bit more consistent. + // You should not do this in a production environment. + let rawFormatted = String(now.format(rawStyle).map { $0.isWhitespace ? " " : $0 }) + let templateFormatted = String(now.format(templateStyle).map { $0.isWhitespace ? " " : $0 }) + + XCTAssertEqual( + rawFormatted, expectedRaw, + "Raw format '\(format.template)' produced '\(rawFormatted)' instead of '\(expectedRaw)'", + file: file, line: line) + XCTAssertEqual( + templateFormatted, expectedTemplate, + "Template '\(format.template)' produced '\(templateFormatted)' instead of '\(expectedTemplate)'", + file: file, line: line) } - + + } + } diff --git a/Tests/TimeTests/RegionTests.swift b/Tests/TimeTests/RegionTests.swift index 51cda65..b0a5472 100644 --- a/Tests/TimeTests/RegionTests.swift +++ b/Tests/TimeTests/RegionTests.swift @@ -1,37 +1,39 @@ import XCTest + @testable import Time class RegionTests: XCTestCase { - static var allTests = [ - ("test24HourPreference", test24HourPreference), - ] - - func test24HourPreference() { - - XCTAssertFalse(Region.posix.wants24HourTime) - - let france = Region(calendar: .current, timeZone: .current, locale: Locale(identifier: "fr_FR")) - XCTAssertTrue(france.wants24HourTime) - - } - - func testCannotCreateAutoupdatingRegion() { - let auto = Region.autoupdatingCurrent - XCTAssertTrue(auto.isAutoupdating) - XCTAssertTrue(auto.calendar.isLikelyAutoupdating) - XCTAssertTrue(auto.timeZone.isLikelyAutoupdating) - XCTAssertTrue(auto.locale.isLikelyAutoupdating) - - let autoAttempt = Region(calendar: .autoupdatingCurrent, - timeZone: .autoupdatingCurrent, - locale: .autoupdatingCurrent) - - XCTAssertEqual(auto, autoAttempt) - XCTAssertFalse(autoAttempt.isAutoupdating) - XCTAssertFalse(autoAttempt.calendar.isLikelyAutoupdating) - XCTAssertFalse(autoAttempt.timeZone.isLikelyAutoupdating) - XCTAssertFalse(autoAttempt.locale.isLikelyAutoupdating) - } + static var allTests = [ + ("test24HourPreference", test24HourPreference) + ] + + func test24HourPreference() { + + XCTAssertFalse(Region.posix.wants24HourTime) + + let france = Region(calendar: .current, timeZone: .current, locale: Locale(identifier: "fr_FR")) + XCTAssertTrue(france.wants24HourTime) + + } + + func testCannotCreateAutoupdatingRegion() { + let auto = Region.autoupdatingCurrent + XCTAssertTrue(auto.isAutoupdating) + XCTAssertTrue(auto.calendar.isLikelyAutoupdating) + XCTAssertTrue(auto.timeZone.isLikelyAutoupdating) + XCTAssertTrue(auto.locale.isLikelyAutoupdating) + + let autoAttempt = Region( + calendar: .autoupdatingCurrent, + timeZone: .autoupdatingCurrent, + locale: .autoupdatingCurrent) + + // XCTAssertEqual(auto, autoAttempt) + XCTAssertFalse(autoAttempt.isAutoupdating) + XCTAssertFalse(autoAttempt.calendar.isLikelyAutoupdating) + XCTAssertFalse(autoAttempt.timeZone.isLikelyAutoupdating) + XCTAssertFalse(autoAttempt.locale.isLikelyAutoupdating) + } } diff --git a/Tests/TimeTests/RelationTests.swift b/Tests/TimeTests/RelationTests.swift index 15203bc..fa2152e 100644 --- a/Tests/TimeTests/RelationTests.swift +++ b/Tests/TimeTests/RelationTests.swift @@ -1,248 +1,249 @@ import XCTest + @testable import Time class RelationTests: XCTestCase { - - static var allTests = [ - ("testBeforeAndAfter", testBeforeAndAfter), - ("testMeetsAndIsMetBy", testMeetsAndIsMetBy), - ("testOverlapsAndIsOverlappedBy", testOverlapsAndIsOverlappedBy), - ("testStartsAndIsStartedBy", testStartsAndIsStartedBy), - ("testDuringAndContains", testDuringAndContains), - ("testFinishesAndIsFinishedBy", testFinishesAndIsFinishedBy), - ("testEqual", testEqual), - - ("testRangeBeforeAndAfter", testRangeBeforeAndAfter), - ("testRangeMeetsAndIsMetBy", testRangeMeetsAndIsMetBy), - ("testRangeOverlapsAndIsOverlappedBy", testRangeOverlapsAndIsOverlappedBy), - ("testRangeStartsAndIsStartedBy", testRangeStartsAndIsStartedBy), - ("testRangeDuringAndContains", testRangeDuringAndContains), - ("testRangeFinishesAndIsFinishedBy", testRangeFinishesAndIsFinishedBy), - ("testRangeEqual", testRangeEqual), - ] - - func testBeforeAndAfter() { - let a = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) - let b = try! Fixed(region: .posix, year: 2020, month: 2, day: 1) - - let aToB = a.relation(to: b) - XCTAssertEqual(aToB, .before) - XCTAssertTrue(a.isBefore(b)) - - let bToA = b.relation(to: a) - XCTAssertEqual(bToA, .after) - XCTAssertTrue(b.isAfter(a)) - } - - func testMeetsAndIsMetBy() { - let a = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) - let b = try! Fixed(region: .posix, year: 2020, month: 1, day: 2) - - let aToB = a.relation(to: b) - XCTAssertEqual(aToB, .meets) - XCTAssertTrue(a.isBefore(b)) - - let bToA = b.relation(to: a) - XCTAssertEqual(bToA, .isMetBy) - XCTAssertTrue(b.isAfter(a)) - - } - - func testOverlapsAndIsOverlappedBy() { - // you can't have overlapping values of the 8 base units within a single calendaring system - // because the point of a calendaring system is to represent sequences of discrete values, - // all subdivided by the same boundaries. - - // therefore in order to test overlapping, we have to resort to comparing values - // BETWEEN calendaring systems - - // Jumada al-Awwal 1441 overlaps with January 2020 - let islamicRegion = Region.posix.setCalendar(Calendar(identifier: .islamic)) - let a = try! Fixed(region: islamicRegion, year: 1441, month: 5) - - let b = try! Fixed(region: .posix, year: 2020, month: 1) - - let aToB = a.relation(to: b) - XCTAssertEqual(aToB, .overlaps) - XCTAssertTrue(a.overlaps(b)) - - let bToA = b.relation(to: a) - XCTAssertEqual(bToA, .isOverlappedBy) - XCTAssertTrue(b.overlaps(a)) - } - - func testStartsAndIsStartedBy() { - let a = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) - let b = try! Fixed(region: .posix, year: 2020, month: 1) - - let aToB = a.relation(to: b) - XCTAssertEqual(aToB, .starts) - XCTAssertTrue(a.isDuring(b)) - XCTAssertFalse(b.isDuring(a)) - - let bToA = b.relation(to: a) - XCTAssertEqual(bToA, .isStartedBy) - XCTAssertTrue(b.contains(a)) - XCTAssertFalse(a.contains(b)) - } - - func testDuringAndContains() { - let a = try! Fixed(region: .posix, year: 2020, month: 1, day: 2) - let b = try! Fixed(region: .posix, year: 2020, month: 1) - - let aToB = a.relation(to: b) - XCTAssertEqual(aToB, .during) - XCTAssertTrue(a.isDuring(b)) - XCTAssertFalse(b.isDuring(a)) - - let bToA = b.relation(to: a) - XCTAssertEqual(bToA, .contains) - XCTAssertTrue(b.contains(a)) - XCTAssertFalse(a.contains(b)) - } - - func testFinishesAndIsFinishedBy() { - let a = try! Fixed(region: .posix, year: 2020, month: 1, day: 31) - let b = try! Fixed(region: .posix, year: 2020, month: 1) - - let aToB = a.relation(to: b) - XCTAssertEqual(aToB, .finishes) - XCTAssertTrue(a.isDuring(b)) - XCTAssertFalse(b.isDuring(a)) - - let bToA = b.relation(to: a) - XCTAssertEqual(bToA, .isFinishedBy) - XCTAssertTrue(b.contains(a)) - XCTAssertFalse(a.contains(b)) - } - - func testEqual() { - let a = try! Fixed(region: .posix, year: 2020, month: 1, day: 31) - let b = try! Fixed(region: .posix, year: 2020, month: 1, day: 31) - - let aToB = a.relation(to: b) - XCTAssertEqual(aToB, .equal) - - let bToA = b.relation(to: a) - XCTAssertEqual(bToA, .equal) - } - - func testRangeBeforeAndAfter() { - let a1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) - let a2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 2) - - let b1 = try! Fixed(region: .posix, year: 2020, month: 2, day: 1) - let b2 = try! Fixed(region: .posix, year: 2020, month: 2, day: 2) - - let a = a1 ..< a2 - let b = b1 ..< b2 - - let aToB = a.relation(to: b) - XCTAssertEqual(aToB, .before) - - let bToA = b.relation(to: a) - XCTAssertEqual(bToA, .after) - } - - func testRangeMeetsAndIsMetBy() { - let a1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) - let a2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 2) - - let b1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 2) - let b2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 3) - - let a = a1 ..< a2 - let b = b1 ..< b2 - - let aToB = a.relation(to: b) - XCTAssertEqual(aToB, .meets) - - let bToA = b.relation(to: a) - XCTAssertEqual(bToA, .isMetBy) - } - - func testRangeOverlapsAndIsOverlappedBy() { - let a1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) - let a2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 10) - - let b1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 5) - let b2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 15) - - let a = a1 ..< a2 - let b = b1 ..< b2 - - let aToB = a.relation(to: b) - XCTAssertEqual(aToB, .overlaps) - - let bToA = b.relation(to: a) - XCTAssertEqual(bToA, .isOverlappedBy) - } - - func testRangeStartsAndIsStartedBy() { - let a1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) - let a2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 2) - - let b1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) - let b2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 5) - - let a = a1 ..< a2 - let b = b1 ..< b2 - - let aToB = a.relation(to: b) - XCTAssertEqual(aToB, .starts) - - let bToA = b.relation(to: a) - XCTAssertEqual(bToA, .isStartedBy) - } - - func testRangeDuringAndContains() { - let a1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 2) - let a2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 3) - - let b1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) - let b2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 5) - - let a = a1 ..< a2 - let b = b1 ..< b2 - - let aToB = a.relation(to: b) - XCTAssertEqual(aToB, .during) - - let bToA = b.relation(to: a) - XCTAssertEqual(bToA, .contains) - } - - func testRangeFinishesAndIsFinishedBy() { - let a1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 4) - let a2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 5) - - let b1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) - let b2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 5) - - let a = a1 ..< a2 - let b = b1 ..< b2 - - let aToB = a.relation(to: b) - XCTAssertEqual(aToB, .finishes) - - let bToA = b.relation(to: a) - XCTAssertEqual(bToA, .isFinishedBy) - } - - func testRangeEqual() { - let a1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) - let a2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 10) - - let b1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) - let b2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 10) - - let a = a1 ..< a2 - let b = b1 ..< b2 - - let aToB = a.relation(to: b) - XCTAssertEqual(aToB, .equal) - - let bToA = b.relation(to: a) - XCTAssertEqual(bToA, .equal) - } + + static var allTests = [ + ("testBeforeAndAfter", testBeforeAndAfter), + ("testMeetsAndIsMetBy", testMeetsAndIsMetBy), + ("testOverlapsAndIsOverlappedBy", testOverlapsAndIsOverlappedBy), + ("testStartsAndIsStartedBy", testStartsAndIsStartedBy), + ("testDuringAndContains", testDuringAndContains), + ("testFinishesAndIsFinishedBy", testFinishesAndIsFinishedBy), + ("testEqual", testEqual), + + ("testRangeBeforeAndAfter", testRangeBeforeAndAfter), + ("testRangeMeetsAndIsMetBy", testRangeMeetsAndIsMetBy), + ("testRangeOverlapsAndIsOverlappedBy", testRangeOverlapsAndIsOverlappedBy), + ("testRangeStartsAndIsStartedBy", testRangeStartsAndIsStartedBy), + ("testRangeDuringAndContains", testRangeDuringAndContains), + ("testRangeFinishesAndIsFinishedBy", testRangeFinishesAndIsFinishedBy), + ("testRangeEqual", testRangeEqual), + ] + + func testBeforeAndAfter() { + let a = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) + let b = try! Fixed(region: .posix, year: 2020, month: 2, day: 1) + + let aToB = a.relation(to: b) + XCTAssertEqual(aToB, .before) + XCTAssertTrue(a.isBefore(b)) + + let bToA = b.relation(to: a) + XCTAssertEqual(bToA, .after) + XCTAssertTrue(b.isAfter(a)) + } + + func testMeetsAndIsMetBy() { + let a = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) + let b = try! Fixed(region: .posix, year: 2020, month: 1, day: 2) + + let aToB = a.relation(to: b) + XCTAssertEqual(aToB, .meets) + XCTAssertTrue(a.isBefore(b)) + + let bToA = b.relation(to: a) + XCTAssertEqual(bToA, .isMetBy) + XCTAssertTrue(b.isAfter(a)) + + } + + func testOverlapsAndIsOverlappedBy() { + // you can't have overlapping values of the 8 base units within a single calendaring system + // because the point of a calendaring system is to represent sequences of discrete values, + // all subdivided by the same boundaries. + + // therefore in order to test overlapping, we have to resort to comparing values + // BETWEEN calendaring systems + + // Jumada al-Awwal 1441 overlaps with January 2020 + let islamicRegion = Region.posix.setCalendar(Calendar(identifier: .islamic)) + let a = try! Fixed(region: islamicRegion, year: 1441, month: 5) + + let b = try! Fixed(region: .posix, year: 2020, month: 1) + + let aToB = a.relation(to: b) + XCTAssertEqual(aToB, .overlaps) + XCTAssertTrue(a.overlaps(b)) + + let bToA = b.relation(to: a) + XCTAssertEqual(bToA, .isOverlappedBy) + XCTAssertTrue(b.overlaps(a)) + } + + func testStartsAndIsStartedBy() { + let a = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) + let b = try! Fixed(region: .posix, year: 2020, month: 1) + + let aToB = a.relation(to: b) + XCTAssertEqual(aToB, .starts) + XCTAssertTrue(a.isDuring(b)) + XCTAssertFalse(b.isDuring(a)) + + let bToA = b.relation(to: a) + XCTAssertEqual(bToA, .isStartedBy) + XCTAssertTrue(b.contains(a)) + XCTAssertFalse(a.contains(b)) + } + + func testDuringAndContains() { + let a = try! Fixed(region: .posix, year: 2020, month: 1, day: 2) + let b = try! Fixed(region: .posix, year: 2020, month: 1) + + let aToB = a.relation(to: b) + XCTAssertEqual(aToB, .during) + XCTAssertTrue(a.isDuring(b)) + XCTAssertFalse(b.isDuring(a)) + + let bToA = b.relation(to: a) + XCTAssertEqual(bToA, .contains) + XCTAssertTrue(b.contains(a)) + XCTAssertFalse(a.contains(b)) + } + + func testFinishesAndIsFinishedBy() { + let a = try! Fixed(region: .posix, year: 2020, month: 1, day: 31) + let b = try! Fixed(region: .posix, year: 2020, month: 1) + + let aToB = a.relation(to: b) + XCTAssertEqual(aToB, .finishes) + XCTAssertTrue(a.isDuring(b)) + XCTAssertFalse(b.isDuring(a)) + + let bToA = b.relation(to: a) + XCTAssertEqual(bToA, .isFinishedBy) + XCTAssertTrue(b.contains(a)) + XCTAssertFalse(a.contains(b)) + } + + func testEqual() { + let a = try! Fixed(region: .posix, year: 2020, month: 1, day: 31) + let b = try! Fixed(region: .posix, year: 2020, month: 1, day: 31) + + let aToB = a.relation(to: b) + XCTAssertEqual(aToB, .equal) + + let bToA = b.relation(to: a) + XCTAssertEqual(bToA, .equal) + } + + func testRangeBeforeAndAfter() { + let a1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) + let a2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 2) + + let b1 = try! Fixed(region: .posix, year: 2020, month: 2, day: 1) + let b2 = try! Fixed(region: .posix, year: 2020, month: 2, day: 2) + + let a = a1..(region: .posix, year: 2020, month: 1, day: 1) + let a2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 2) + + let b1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 2) + let b2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 3) + + let a = a1..(region: .posix, year: 2020, month: 1, day: 1) + let a2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 10) + + let b1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 5) + let b2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 15) + + let a = a1..(region: .posix, year: 2020, month: 1, day: 1) + let a2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 2) + + let b1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) + let b2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 5) + + let a = a1..(region: .posix, year: 2020, month: 1, day: 2) + let a2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 3) + + let b1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) + let b2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 5) + + let a = a1..(region: .posix, year: 2020, month: 1, day: 4) + let a2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 5) + + let b1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) + let b2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 5) + + let a = a1..(region: .posix, year: 2020, month: 1, day: 1) + let a2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 10) + + let b1 = try! Fixed(region: .posix, year: 2020, month: 1, day: 1) + let b2 = try! Fixed(region: .posix, year: 2020, month: 1, day: 10) + + let a = a1..(region: sweden, year: 2023, month: 10, day: 29, hour: 02, minute: 30, second: 00) - let end = start.adding(minutes: 30) - let range = start ..< end - - XCTAssertTrue(range.upperBound > range.lowerBound) - } - - func testSwedenRepeatedOffsettingAroundDST() throws { - let sweden = Region(calendar: Calendar(identifier: .gregorian), - timeZone: TimeZone(identifier: "Europe/Stockholm")!, - locale: Locale(identifier: "en_US")) - - let start = try Fixed(region: sweden, year: 2023, month: 10, day: 29, hour: 02, minute: 30, second: 00) - let target = start.adding(hours: 10) - var step = start - - while step.isBefore(target) { - step = step.adding(minutes: 30) - } - - XCTAssertTrue(step >= target) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +class ReportedBugs: SkipUnavailableTestCase { + + func testSwedenRangeCrashAroundDST() throws { + let sweden = Region( + calendar: Calendar(identifier: .gregorian), + timeZone: TimeZone(identifier: "Europe/Stockholm")!, + locale: Locale(identifier: "en_US")) + + let start = try Fixed( + region: sweden, year: 2023, month: 10, day: 29, hour: 02, minute: 30, second: 00) + let end = start.adding(minutes: 30) + let range = start.. range.lowerBound) + } + + func testSwedenRepeatedOffsettingAroundDST() throws { + let sweden = Region( + calendar: Calendar(identifier: .gregorian), + timeZone: TimeZone(identifier: "Europe/Stockholm")!, + locale: Locale(identifier: "en_US")) + + let start = try Fixed( + region: sweden, year: 2023, month: 10, day: 29, hour: 02, minute: 30, second: 00) + let target = start.adding(hours: 10) + var step = start + + while step.isBefore(target) { + step = step.adding(minutes: 30) } - - func testISO8601WeekdayNumber_GH75() throws { - /* + + XCTAssertTrue(step >= target) + } + + func testISO8601WeekdayNumber_GH75() throws { + /* The ISO8601 calendar uses different weekday numberings than the gregorian calendar. On the gregorian calendar, 1 = Sunday. On the ISO8601 calendar, 1 = Monday. - + This test makes sure that the numeric value from `.dayOfWeek` and the Locale.Weekday value from `.weekday` is consistent between calendars, even if they *format* differently. */ - - let iso8601 = Calendar(identifier: .iso8601) - let iso8601Region = Region(calendar: iso8601, - timeZone: TimeZone(secondsFromGMT: 0)!, - locale: .current) - - let mar3Gregorian = try Fixed(region: .posix, year: 2024, month: 3, day: 3) - let mar3ISO8601 = try Fixed(region: iso8601Region, year: 2024, month: 3, day: 3) - - XCTAssertEqual(mar3Gregorian.dayOfWeek, 1) - XCTAssertEqual(mar3ISO8601.dayOfWeek, 1) - - #if !os(Linux) - XCTAssertEqual(mar3Gregorian.weekday, .sunday) - XCTAssertEqual(mar3ISO8601.weekday, .sunday) - #endif - - let formattedGregorian = mar3Gregorian.format(weekday: .naturalDigits) - let formattedISO8601 = mar3ISO8601.format(weekday: .naturalDigits) - - XCTAssertEqual(formattedGregorian, "1") - XCTAssertEqual(formattedISO8601, "7") - } - - func testValuesWithoutErasStillHaveThem_GH82() throws { - let day = try Fixed(region: .posix, year: 2024, month: 4, day: 7) - XCTAssertEqual(day.era, 1) - } + + let iso8601 = Calendar(identifier: .iso8601) + let iso8601Region = Region( + calendar: iso8601, + timeZone: TimeZone(secondsFromGMT: 0)!, + locale: .current) + + let mar3Gregorian = try Fixed(region: .posix, year: 2024, month: 3, day: 3) + let mar3ISO8601 = try Fixed(region: iso8601Region, year: 2024, month: 3, day: 3) + + XCTAssertEqual(mar3Gregorian.dayOfWeek, 1) + XCTAssertEqual(mar3ISO8601.dayOfWeek, 1) + + #if !os(Linux) + XCTAssertEqual(mar3Gregorian.weekday, .sunday) + XCTAssertEqual(mar3ISO8601.weekday, .sunday) + #endif + + let formattedGregorian = mar3Gregorian.format(weekday: .naturalDigits) + let formattedISO8601 = mar3ISO8601.format(weekday: .naturalDigits) + + XCTAssertEqual(formattedGregorian, "1") + XCTAssertEqual(formattedISO8601, "7") + } + + func testValuesWithoutErasStillHaveThem_GH82() throws { + let day = try Fixed(region: .posix, year: 2024, month: 4, day: 7) + XCTAssertEqual(day.era, 1) + } } diff --git a/Tests/TimeTests/SerializationTests.swift b/Tests/TimeTests/SerializationTests.swift index 6f28314..2b6d86d 100644 --- a/Tests/TimeTests/SerializationTests.swift +++ b/Tests/TimeTests/SerializationTests.swift @@ -1,541 +1,560 @@ import XCTest -@testable import Time + import protocol Time.Unit -class SerializationTests: XCTestCase { +@testable import Time + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +class SerializationTests: SkipUnavailableTestCase { - static var allTests = [ - ("testCodableRegionRoundTrip", testCodableRegionRoundTrip), - ("testCodableTimePeriodRoundTrip", testCodableTimePeriodRoundTrip), - ("testMaliciousPayload", testMaliciousPayload), - ("testNonEraTimePeriod", testMaliciousPayload), - ] + static var allTests = [ + ("testCodableRegionRoundTrip", testCodableRegionRoundTrip), + ("testCodableTimePeriodRoundTrip", testCodableTimePeriodRoundTrip), + ("testMaliciousPayload", testMaliciousPayload), + ("testNonEraTimePeriod", testMaliciousPayload), + ] - func testCodableRegionRoundTrip() throws { - let thisRegion = Region.current - let encodedThis = try JSONEncoder().encode(thisRegion) - let decodedThis = try JSONDecoder().decode(Region.self, from: encodedThis) - XCTAssertEqual(thisRegion, decodedThis) + func testCodableRegionRoundTrip() throws { + let thisRegion = Region.current + let encodedThis = try JSONEncoder().encode(thisRegion) + let decodedThis = try JSONDecoder().decode(Region.self, from: encodedThis) + XCTAssertEqual(thisRegion, decodedThis) - let explicitRegion = Region(calendar: Calendar(identifier: .gregorian), - timeZone: TimeZone(identifier: "Europe/Paris")!, - locale: Locale(identifier: "fr_FR")) + let explicitRegion = Region( + calendar: Calendar(identifier: .gregorian), + timeZone: TimeZone(identifier: "Europe/Paris")!, + locale: Locale(identifier: "fr_FR")) - let encodedExplicit = try JSONEncoder().encode(explicitRegion) - let decodedExplicit = try JSONDecoder().decode(Region.self, from: encodedExplicit) - XCTAssertEqual(explicitRegion, decodedExplicit) - } + let encodedExplicit = try JSONEncoder().encode(explicitRegion) + let decodedExplicit = try JSONDecoder().decode(Region.self, from: encodedExplicit) + XCTAssertEqual(explicitRegion, decodedExplicit) + } - func testCodableTimePeriodRoundTrip() throws { + func testCodableTimePeriodRoundTrip() throws { - let clock = Clocks.system - - func testRoundTrip(of timePeriod: Fixed, file: StaticString = #file, line: UInt = #line) throws { - let encoded = try JSONEncoder().encode(timePeriod) - let decoded = try JSONDecoder().decode(Fixed.self, from: encoded) - XCTAssertEqual(timePeriod, decoded, file: file, line: line) - } + let clock = Clocks.system - try testRoundTrip(of: clock.currentYear) - try testRoundTrip(of: clock.currentMonth) - try testRoundTrip(of: clock.currentDay) - try testRoundTrip(of: clock.currentHour) - try testRoundTrip(of: clock.currentMinute) - try testRoundTrip(of: clock.currentSecond) - try testRoundTrip(of: clock.currentNanosecond) + func testRoundTrip( + of timePeriod: Fixed, file: StaticString = #file, line: UInt = #line + ) throws { + let encoded = try JSONEncoder().encode(timePeriod) + let decoded = try JSONDecoder().decode(Fixed.self, from: encoded) + XCTAssertEqual(timePeriod, decoded, file: file, line: line) } -// func testNonEraTimePeriod() throws { -// try testRoundTripOfNonEraPeriod(of: TimePeriod(region: .current, instant: Clocks.system.now)) -// try testRoundTripOfNonEraPeriod(of: TimePeriod(region: .current, instant: Clocks.system.now)) -// try testRoundTripOfNonEraPeriod(of: TimePeriod(region: .current, instant: Clocks.system.now)) -// try testRoundTripOfNonEraPeriod(of: TimePeriod(region: .current, instant: Clocks.system.now)) -// try testRoundTripOfNonEraPeriod(of: TimePeriod(region: .current, instant: Clocks.system.now)) -// try testRoundTripOfNonEraPeriod(of: TimePeriod(region: .current, instant: Clocks.system.now)) -// try testRoundTripOfNonEraPeriod(of: TimePeriod(region: .current, instant: Clocks.system.now)) -// -// } -// -// private func testRoundTripOfNonEraPeriod(of timePeriod: TimePeriod) throws { -// let encodedValue = try JSONEncoder().encode(timePeriod) -// let decodedValue = try JSONDecoder().decode(TimePeriod.self, from: encodedValue) -// XCTAssertEqual(timePeriod, decodedValue) -// } + try testRoundTrip(of: clock.currentYear) + try testRoundTrip(of: clock.currentMonth) + try testRoundTrip(of: clock.currentDay) + try testRoundTrip(of: clock.currentHour) + try testRoundTrip(of: clock.currentMinute) + try testRoundTrip(of: clock.currentSecond) + try testRoundTrip(of: clock.currentNanosecond) + } - // Test resources were added in a Swift version later than 5.0, which this library supports. + // func testNonEraTimePeriod() throws { + // try testRoundTripOfNonEraPeriod(of: TimePeriod(region: .current, instant: Clocks.system.now)) + // try testRoundTripOfNonEraPeriod(of: TimePeriod(region: .current, instant: Clocks.system.now)) + // try testRoundTripOfNonEraPeriod(of: TimePeriod(region: .current, instant: Clocks.system.now)) + // try testRoundTripOfNonEraPeriod(of: TimePeriod(region: .current, instant: Clocks.system.now)) + // try testRoundTripOfNonEraPeriod(of: TimePeriod(region: .current, instant: Clocks.system.now)) + // try testRoundTripOfNonEraPeriod(of: TimePeriod(region: .current, instant: Clocks.system.now)) + // try testRoundTripOfNonEraPeriod(of: TimePeriod(region: .current, instant: Clocks.system.now)) + // + // } + // + // private func testRoundTripOfNonEraPeriod(of timePeriod: TimePeriod) throws { + // let encodedValue = try JSONEncoder().encode(timePeriod) + // let decodedValue = try JSONDecoder().decode(TimePeriod.self, from: encodedValue) + // XCTAssertEqual(timePeriod, decodedValue) + // } - func testMaliciousPayload() throws { - let decoder = JSONDecoder() + // Test resources were added in a Swift version later than 5.0, which this library supports. - let validPayload = generatePayloadFor(year: 2023, month: 01, day: 01, hour: 11, minute: 00, second: 00) - let _ = try decoder.decode(Fixed.self, from: validPayload) + func testMaliciousPayload() throws { + let decoder = JSONDecoder() - let invalidMonthPayload = generatePayloadFor(year: 2023, month: -4, day: 01, hour: 11, minute: 00, second: 00) - XCTAssertThrowsError(try decoder.decode(Fixed.self, from: invalidMonthPayload), "-4 is an invalid calendar month") + let validPayload = generatePayloadFor( + year: 2023, month: 01, day: 01, hour: 11, minute: 00, second: 00) + let _ = try decoder.decode(Fixed.self, from: validPayload) - let invalidHourPayload = generatePayloadFor(year: 2023, month: 01, day: 01, hour: 27, minute: 00, second: 00) - XCTAssertThrowsError(try decoder.decode(Fixed.self, from: invalidHourPayload), "27 is an invalid hour") + let invalidMonthPayload = generatePayloadFor( + year: 2023, month: -4, day: 01, hour: 11, minute: 00, second: 00) + XCTAssertThrowsError( + try decoder.decode(Fixed.self, from: invalidMonthPayload), + "-4 is an invalid calendar month") - let invalidDayPayload = generatePayloadFor(year: 2023, month: 01, day: 00, hour: 01, minute: 00, second: 00) - XCTAssertThrowsError(try decoder.decode(Fixed.self, from: invalidDayPayload), "0 is an invalid day") + let invalidHourPayload = generatePayloadFor( + year: 2023, month: 01, day: 01, hour: 27, minute: 00, second: 00) + XCTAssertThrowsError( + try decoder.decode(Fixed.self, from: invalidHourPayload), "27 is an invalid hour") - let payloadMissingPayload = generatePayloadFor(year: 2023, month: 01, day: 01, hour: 01, minute: 00, second: 00) - XCTAssertThrowsError(try decoder.decode(Fixed.self, from: payloadMissingPayload), "Nanoseconds are missing") - } + let invalidDayPayload = generatePayloadFor( + year: 2023, month: 01, day: 00, hour: 01, minute: 00, second: 00) + XCTAssertThrowsError( + try decoder.decode(Fixed.self, from: invalidDayPayload), "0 is an invalid day") - private func generatePayloadFor(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int) -> Data { - // This is very crude. - - let string = #""" - { - "region": { - "locale": { - "current": 2, - "identifier": "en_SE" - }, - "timeZone": { - "autoupdating": true, - "identifier": "Europe\/Stockholm" - }, - "calendar": { - "locale": { - "current": 2, - "identifier": "en_SE" - }, - "timeZone": { - "autoupdating": true, - "identifier": "Europe\/Stockholm" - }, - "current": 0, - "identifier": "gregorian", - "minimumDaysInFirstWeek": 4, - "firstWeekday": 2 - } - }, - "components": { - "era": 1, - "year": \#(year), - "month": \#(month), - "day": \#(day), - "hour": \#(hour), - "minute": \#(minute), - "second": \#(second) - } - } - """# - - return Data(string.utf8) - } - - func testOldSerializationFormat() throws { - let jsonString = """ - [ - { - "value": 728405072, - "region": { - "calendar": { - "firstWeekday": 1, - "minimumDaysInFirstWeek": 1, - "identifier": "gregorian", - "locale": { - "identifier": "en_SE" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - } - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "locale": { - "identifier": "en_SE" - } - } - }, - { - "value": 728406872, - "region": { - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "locale": { - "identifier": "en_SE" - }, - "calendar": { - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "minimumDaysInFirstWeek": 1, - "locale": { - "identifier": "en_SE" - }, - "firstWeekday": 1, - "identifier": "gregorian" - } - } - }, - { - "region": { - "locale": { - "identifier": "en_SE" - }, - "calendar": { - "identifier": "gregorian", - "locale": { - "identifier": "en_SE" - }, - "firstWeekday": 1, - "minimumDaysInFirstWeek": 1, - "timeZone": { - "identifier": "Europe\\/Stockholm" - } - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - } - }, - "value": 728405072 - }, - { - "region": { - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "calendar": { - "locale": { - "identifier": "en_SE" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "minimumDaysInFirstWeek": 1, - "firstWeekday": 1, - "identifier": "gregorian" - }, - "locale": { - "identifier": "en_SE" - } - }, - "value": 728406872 - }, - { - "value": 728175606.030619, - "region": { - "calendar": { - "firstWeekday": 2, - "minimumDaysInFirstWeek": 4, - "identifier": "gregorian", - "locale": { - "identifier": "en_SE" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - } - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "locale": { - "identifier": "en_SE" - } - } - }, - { - "region": { - "locale": { - "identifier": "en_SE" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "calendar": { - "firstWeekday": 2, - "identifier": "gregorian", - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "locale": { - "identifier": "en_SE" - }, - "minimumDaysInFirstWeek": 4 - } - }, - "value": 728262004.728645 - }, - { - "value": 728406871, - "region": { - "locale": { - "identifier": "en_SE" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "calendar": { - "firstWeekday": 1, - "minimumDaysInFirstWeek": 1, - "identifier": "gregorian", - "locale": { - "identifier": "en_SE" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - } - } - } - }, - { - "value": 728319007.785119, - "region": { - "calendar": { - "firstWeekday": 2, - "identifier": "gregorian", - "minimumDaysInFirstWeek": 4, - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "locale": { - "identifier": "en_SE" - } - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "locale": { - "identifier": "en_SE" - } - } - }, - { - "value": 728491625, - "region": { - "calendar": { - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "minimumDaysInFirstWeek": 1, - "locale": { - "identifier": "en_SE" - }, - "identifier": "gregorian", - "firstWeekday": 1 - }, - "locale": { - "identifier": "en_SE" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - } - } - }, - { - "value": 728493425, - "region": { - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "calendar": { - "identifier": "gregorian", - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "firstWeekday": 1, - "locale": { - "identifier": "en_SE" - }, - "minimumDaysInFirstWeek": 1 - }, - "locale": { - "identifier": "en_SE" - } - } - }, - { - "value": 728491625, - "region": { - "calendar": { - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "firstWeekday": 1, - "locale": { - "identifier": "en_SE" - }, - "identifier": "gregorian", - "minimumDaysInFirstWeek": 1 - }, - "locale": { - "identifier": "en_SE" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - } - } - }, - { - "value": 728493425, - "region": { - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "calendar": { - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "firstWeekday": 1, - "locale": { - "identifier": "en_SE" - }, - "identifier": "gregorian", - "minimumDaysInFirstWeek": 1 - }, - "locale": { - "identifier": "en_SE" - } - } - }, - { - "value": 728370608.047602, - "region": { - "calendar": { - "firstWeekday": 2, - "minimumDaysInFirstWeek": 4, - "locale": { - "identifier": "en_SE" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "identifier": "gregorian" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "locale": { - "identifier": "en_SE" - } - } - }, - { - "value": 728578177, - "region": { - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "locale": { - "identifier": "en_SE" - }, - "calendar": { - "firstWeekday": 1, - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "minimumDaysInFirstWeek": 1, - "identifier": "gregorian", - "locale": { - "identifier": "en_SE" - } - } - } - }, - { - "value": 728579977, - "region": { - "calendar": { - "firstWeekday": 1, - "minimumDaysInFirstWeek": 1, - "identifier": "gregorian", - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "locale": { - "identifier": "en_SE" - } - }, - "locale": { - "identifier": "en_SE" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - } - } - }, - { - "region": { - "locale": { - "identifier": "en_SE" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "calendar": { - "identifier": "gregorian", - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "minimumDaysInFirstWeek": 1, - "locale": { - "identifier": "en_SE" - }, - "firstWeekday": 1 - } - }, - "value": 728578177 - }, - { - "region": { - "calendar": { - "identifier": "gregorian", - "timeZone": { - "identifier": "Europe\\/Stockholm" - }, - "minimumDaysInFirstWeek": 1, - "locale": { - "identifier": "en_SE" - }, - "firstWeekday": 1 - }, - "locale": { - "identifier": "en_SE" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - } - }, - "value": 728579977 - }, - { - "region": { - "calendar": { - "firstWeekday": 2, - "minimumDaysInFirstWeek": 4, - "identifier": "gregorian", - "locale": { - "identifier": "en_SE" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - } - }, - "locale": { - "identifier": "en_SE" - }, - "timeZone": { - "identifier": "Europe\\/Stockholm" - } - }, - "value": 728392809.920091 - } - ] -""" - let jsonData = Data(jsonString.utf8) - - do { - let timestamps = try JSONDecoder().decode(Array>.self, from: jsonData) - XCTAssertEqual(timestamps.count, 18) - } catch { - XCTFail("Cannot decode json: \(error)") - } + let payloadMissingPayload = generatePayloadFor( + year: 2023, month: 01, day: 01, hour: 01, minute: 00, second: 00) + XCTAssertThrowsError( + try decoder.decode(Fixed.self, from: payloadMissingPayload), + "Nanoseconds are missing") + } + + private func generatePayloadFor( + year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int + ) -> Data { + // This is very crude. + + let string = #""" + { + "region": { + "locale": { + "current": 2, + "identifier": "en_SE" + }, + "timeZone": { + "autoupdating": true, + "identifier": "Europe\/Stockholm" + }, + "calendar": { + "locale": { + "current": 2, + "identifier": "en_SE" + }, + "timeZone": { + "autoupdating": true, + "identifier": "Europe\/Stockholm" + }, + "current": 0, + "identifier": "gregorian", + "minimumDaysInFirstWeek": 4, + "firstWeekday": 2 + } + }, + "components": { + "era": 1, + "year": \#(year), + "month": \#(month), + "day": \#(day), + "hour": \#(hour), + "minute": \#(minute), + "second": \#(second) + } + } + """# + + return Data(string.utf8) + } + + func testOldSerializationFormat() throws { + let jsonString = """ + [ + { + "value": 728405072, + "region": { + "calendar": { + "firstWeekday": 1, + "minimumDaysInFirstWeek": 1, + "identifier": "gregorian", + "locale": { + "identifier": "en_SE" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + } + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "locale": { + "identifier": "en_SE" + } + } + }, + { + "value": 728406872, + "region": { + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "locale": { + "identifier": "en_SE" + }, + "calendar": { + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "minimumDaysInFirstWeek": 1, + "locale": { + "identifier": "en_SE" + }, + "firstWeekday": 1, + "identifier": "gregorian" + } + } + }, + { + "region": { + "locale": { + "identifier": "en_SE" + }, + "calendar": { + "identifier": "gregorian", + "locale": { + "identifier": "en_SE" + }, + "firstWeekday": 1, + "minimumDaysInFirstWeek": 1, + "timeZone": { + "identifier": "Europe\\/Stockholm" + } + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + } + }, + "value": 728405072 + }, + { + "region": { + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "calendar": { + "locale": { + "identifier": "en_SE" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "minimumDaysInFirstWeek": 1, + "firstWeekday": 1, + "identifier": "gregorian" + }, + "locale": { + "identifier": "en_SE" + } + }, + "value": 728406872 + }, + { + "value": 728175606.030619, + "region": { + "calendar": { + "firstWeekday": 2, + "minimumDaysInFirstWeek": 4, + "identifier": "gregorian", + "locale": { + "identifier": "en_SE" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + } + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "locale": { + "identifier": "en_SE" + } + } + }, + { + "region": { + "locale": { + "identifier": "en_SE" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "calendar": { + "firstWeekday": 2, + "identifier": "gregorian", + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "locale": { + "identifier": "en_SE" + }, + "minimumDaysInFirstWeek": 4 + } + }, + "value": 728262004.728645 + }, + { + "value": 728406871, + "region": { + "locale": { + "identifier": "en_SE" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "calendar": { + "firstWeekday": 1, + "minimumDaysInFirstWeek": 1, + "identifier": "gregorian", + "locale": { + "identifier": "en_SE" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + } + } + } + }, + { + "value": 728319007.785119, + "region": { + "calendar": { + "firstWeekday": 2, + "identifier": "gregorian", + "minimumDaysInFirstWeek": 4, + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "locale": { + "identifier": "en_SE" + } + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "locale": { + "identifier": "en_SE" + } + } + }, + { + "value": 728491625, + "region": { + "calendar": { + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "minimumDaysInFirstWeek": 1, + "locale": { + "identifier": "en_SE" + }, + "identifier": "gregorian", + "firstWeekday": 1 + }, + "locale": { + "identifier": "en_SE" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + } + } + }, + { + "value": 728493425, + "region": { + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "calendar": { + "identifier": "gregorian", + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "firstWeekday": 1, + "locale": { + "identifier": "en_SE" + }, + "minimumDaysInFirstWeek": 1 + }, + "locale": { + "identifier": "en_SE" + } + } + }, + { + "value": 728491625, + "region": { + "calendar": { + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "firstWeekday": 1, + "locale": { + "identifier": "en_SE" + }, + "identifier": "gregorian", + "minimumDaysInFirstWeek": 1 + }, + "locale": { + "identifier": "en_SE" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + } + } + }, + { + "value": 728493425, + "region": { + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "calendar": { + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "firstWeekday": 1, + "locale": { + "identifier": "en_SE" + }, + "identifier": "gregorian", + "minimumDaysInFirstWeek": 1 + }, + "locale": { + "identifier": "en_SE" + } + } + }, + { + "value": 728370608.047602, + "region": { + "calendar": { + "firstWeekday": 2, + "minimumDaysInFirstWeek": 4, + "locale": { + "identifier": "en_SE" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "identifier": "gregorian" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "locale": { + "identifier": "en_SE" + } + } + }, + { + "value": 728578177, + "region": { + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "locale": { + "identifier": "en_SE" + }, + "calendar": { + "firstWeekday": 1, + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "minimumDaysInFirstWeek": 1, + "identifier": "gregorian", + "locale": { + "identifier": "en_SE" + } + } + } + }, + { + "value": 728579977, + "region": { + "calendar": { + "firstWeekday": 1, + "minimumDaysInFirstWeek": 1, + "identifier": "gregorian", + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "locale": { + "identifier": "en_SE" + } + }, + "locale": { + "identifier": "en_SE" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + } + } + }, + { + "region": { + "locale": { + "identifier": "en_SE" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "calendar": { + "identifier": "gregorian", + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "minimumDaysInFirstWeek": 1, + "locale": { + "identifier": "en_SE" + }, + "firstWeekday": 1 + } + }, + "value": 728578177 + }, + { + "region": { + "calendar": { + "identifier": "gregorian", + "timeZone": { + "identifier": "Europe\\/Stockholm" + }, + "minimumDaysInFirstWeek": 1, + "locale": { + "identifier": "en_SE" + }, + "firstWeekday": 1 + }, + "locale": { + "identifier": "en_SE" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + } + }, + "value": 728579977 + }, + { + "region": { + "calendar": { + "firstWeekday": 2, + "minimumDaysInFirstWeek": 4, + "identifier": "gregorian", + "locale": { + "identifier": "en_SE" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + } + }, + "locale": { + "identifier": "en_SE" + }, + "timeZone": { + "identifier": "Europe\\/Stockholm" + } + }, + "value": 728392809.920091 + } + ] + """ + let jsonData = Data(jsonString.utf8) + + do { + let timestamps = try JSONDecoder().decode(Array>.self, from: jsonData) + XCTAssertEqual(timestamps.count, 18) + } catch { + XCTFail("Cannot decode json: \(error)") } + } } diff --git a/Tests/TimeTests/ThreadingTests.swift b/Tests/TimeTests/ThreadingTests.swift index b00939d..e1201b4 100644 --- a/Tests/TimeTests/ThreadingTests.swift +++ b/Tests/TimeTests/ThreadingTests.swift @@ -1,47 +1,52 @@ import Foundation import XCTest + @testable import Time class ThreadingTests: XCTestCase { - static var allTests = [ - ("testMultithreadingWithCopies", testMultithreadingWithCopies), - ] - - func testMultithreadingWithCopies() async throws { - // `Calendar`/`NSCalendar` aren't thread-safe on Linux, and many `TimePeriod` operations that don't - // appear to be mutating do end up calling calendar methods that perform temporary mutations internally. - // A `forceCopy()` method was added to `Region` and `TimePeriod` to allow users of this library to create - // thread-local copies. - - let region = Region(calendar: Calendar(identifier: .gregorian), - timeZone: TimeZone(identifier: "Europe/Paris")!, - locale: Locale(identifier: "en_US")) - - let rangeStart = try Fixed(region: region, year: 2023, month: 06, day: 26, hour: 14, minute: 00) - - let results = await withTaskGroup(of: Range>.self, body: { group in - for _ in 0..<1000 { - let taskLocalStart = rangeStart._forcedCopy() - // ^ Without this copy, this test is likely to crash on Linux. - group.addTask { - let fourHoursLater = taskLocalStart.adding(hours: 4) - if fourHoursLater <= taskLocalStart { - print(taskLocalStart.debugDescription, fourHoursLater.debugDescription) - } - let range = taskLocalStart ..< fourHoursLater - XCTAssert(range.lowerBound <= range.upperBound) - // ^ This assert is technially redundant since `Range` will crash if that assertion would fail. - return range - } + static var allTests = [ + ("testMultithreadingWithCopies", testMultithreadingWithCopies) + ] + + func testMultithreadingWithCopies() async throws { + // `Calendar`/`NSCalendar` aren't thread-safe on Linux, and many `TimePeriod` operations that don't + // appear to be mutating do end up calling calendar methods that perform temporary mutations internally. + // A `forceCopy()` method was added to `Region` and `TimePeriod` to allow users of this library to create + // thread-local copies. + + let region = Region( + calendar: Calendar(identifier: .gregorian), + timeZone: TimeZone(identifier: "Europe/Paris")!, + locale: Locale(identifier: "en_US")) + + let rangeStart = try Fixed( + region: region, year: 2023, month: 06, day: 26, hour: 14, minute: 00) + + let results = await withTaskGroup( + of: Range>.self, + body: { group in + for _ in 0..<1000 { + let taskLocalStart = rangeStart._forcedCopy() + // ^ Without this copy, this test is likely to crash on Linux. + group.addTask { + let fourHoursLater = taskLocalStart.adding(hours: 4) + if fourHoursLater <= taskLocalStart { + print(taskLocalStart.debugDescription, fourHoursLater.debugDescription) } - - var ranges = [Range>]() - for await result in group { ranges.append(result) } - return ranges - }) - - XCTAssertEqual(results.count, 1000) - } + let range = taskLocalStart..>]() + for await result in group { ranges.append(result) } + return ranges + }) + + XCTAssertEqual(results.count, 1000) + } } diff --git a/Tests/TimeTests/XCTAssert.swift b/Tests/TimeTests/XCTAssert.swift index eab80f2..5455f69 100644 --- a/Tests/TimeTests/XCTAssert.swift +++ b/Tests/TimeTests/XCTAssert.swift @@ -1,74 +1,129 @@ import Foundation import XCTest + @testable import Time @discardableResult -func XCTAssertEqualWithAccuracyWorkaround(_ value1: FP, _ value2: FP, accuracy: FP, file: StaticString = #file, line: UInt = #line) -> Bool { - let halfRange = accuracy / 2.0 - let value1Range = (value1 - halfRange) ..< (value1 + halfRange) - let value2Range = (value2 - halfRange) ..< (value2 + halfRange) - - let relation = value1Range.determineRelationship(to: value2Range) - if relation.isOverlapping == false { - XCTFail("\(value1) ≠ \(value2) with accuracy \(accuracy)", file: file, line: line) - return false - } else { - return true - } +func XCTAssertEqualWithAccuracyWorkaround( + _ value1: FP, _ value2: FP, accuracy: FP, file: StaticString = #file, line: UInt = #line +) -> Bool { + let halfRange = accuracy / 2.0 + let value1Range = (value1 - halfRange)..<(value1 + halfRange) + let value2Range = (value2 - halfRange)..<(value2 + halfRange) + + let relation = value1Range.determineRelationship(to: value2Range) + if relation.isOverlapping == false { + XCTFail("\(value1) ≠ \(value2) with accuracy \(accuracy)", file: file, line: line) + return false + } else { + return true + } } @discardableResult -func XCTAssertTime(_ time: Fixed, era: Int, file: StaticString = #file, line: UInt = #line) -> Bool { - if time.era == era { return true } - XCTFail("Unexpected time components: \(time), expecting era=\(era)", file: file, line: line) - return false +func XCTAssertTime( + _ time: Fixed, era: Int, file: StaticString = #file, line: UInt = #line +) -> Bool { + if time.era == era { return true } + XCTFail("Unexpected time components: \(time), expecting era=\(era)", file: file, line: line) + return false } @discardableResult -func XCTAssertTime(_ time: Fixed, era: Int, year: Int, file: StaticString = #file, line: UInt = #line) -> Bool { - if time.era == era && time.year == year { return true } - XCTFail("Unexpected time components: \(time), expecting era=\(era), year=\(year)", file: file, line: line) - return false +func XCTAssertTime( + _ time: Fixed, era: Int, year: Int, file: StaticString = #file, line: UInt = #line +) -> Bool { + if time.era == era && time.year == year { return true } + XCTFail( + "Unexpected time components: \(time), expecting era=\(era), year=\(year)", file: file, + line: line) + return false } @discardableResult -func XCTAssertTime(_ time: Fixed, era: Int, year: Int, month: Int, file: StaticString = #file, line: UInt = #line) -> Bool { - if time.era == era && time.year == year && time.month == month { return true } - XCTFail("Unexpected time components: \(time), expecting era=\(era), year=\(year), month=\(month)", file: file, line: line) - return false +func XCTAssertTime( + _ time: Fixed, era: Int, year: Int, month: Int, file: StaticString = #file, line: UInt = #line +) -> Bool { + if time.era == era && time.year == year && time.month == month { return true } + XCTFail( + "Unexpected time components: \(time), expecting era=\(era), year=\(year), month=\(month)", + file: file, line: line) + return false } @discardableResult -func XCTAssertTime(_ time: Fixed, era: Int, year: Int, month: Int, day: Int, file: StaticString = #file, line: UInt = #line) -> Bool { - if time.era == era && time.year == year && time.month == month && time.day == day { return true } - XCTFail("Unexpected time components: \(time), expecting era=\(era), year=\(year), month=\(month), day=\(day)", file: file, line: line) - return false +func XCTAssertTime( + _ time: Fixed, era: Int, year: Int, month: Int, day: Int, file: StaticString = #file, + line: UInt = #line +) -> Bool { + if time.era == era && time.year == year && time.month == month && time.day == day { return true } + XCTFail( + "Unexpected time components: \(time), expecting era=\(era), year=\(year), month=\(month), day=\(day)", + file: file, line: line) + return false } @discardableResult -func XCTAssertTime(_ time: Fixed, era: Int, year: Int, month: Int, day: Int, hour: Int, file: StaticString = #file, line: UInt = #line) -> Bool { - if time.era == era && time.year == year && time.month == month && time.day == day && time.hour == hour { return true } - XCTFail("Unexpected time components: \(time), expecting era=\(era), year=\(year), month=\(month), day=\(day), hour=\(hour)", file: file, line: line) - return false +func XCTAssertTime( + _ time: Fixed, era: Int, year: Int, month: Int, day: Int, hour: Int, + file: StaticString = #file, line: UInt = #line +) -> Bool { + if time.era == era && time.year == year && time.month == month && time.day == day + && time.hour == hour + { + return true + } + XCTFail( + "Unexpected time components: \(time), expecting era=\(era), year=\(year), month=\(month), day=\(day), hour=\(hour)", + file: file, line: line) + return false } @discardableResult -func XCTAssertTime(_ time: Fixed, era: Int, year: Int, month: Int, day: Int, hour: Int, minute: Int, file: StaticString = #file, line: UInt = #line) -> Bool { - if time.era == era && time.year == year && time.month == month && time.day == day && time.hour == hour && time.minute == minute { return true } - XCTFail("Unexpected time components: \(time), expecting era=\(era), year=\(year), month=\(month), day=\(day), hour=\(hour), minute=\(minute)", file: file, line: line) - return false +func XCTAssertTime( + _ time: Fixed, era: Int, year: Int, month: Int, day: Int, hour: Int, minute: Int, + file: StaticString = #file, line: UInt = #line +) -> Bool { + if time.era == era && time.year == year && time.month == month && time.day == day + && time.hour == hour && time.minute == minute + { + return true + } + XCTFail( + "Unexpected time components: \(time), expecting era=\(era), year=\(year), month=\(month), day=\(day), hour=\(hour), minute=\(minute)", + file: file, line: line) + return false } @discardableResult -func XCTAssertTime(_ time: Fixed, era: Int, year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, file: StaticString = #file, line: UInt = #line) -> Bool { - if time.era == era && time.year == year && time.month == month && time.day == day && time.hour == hour && time.minute == minute && time.second == second { return true } - XCTFail("Unexpected time components: \(time), expecting era=\(era), year=\(year), month=\(month), day=\(day), hour=\(hour), minute=\(minute), second=\(second)", file: file, line: line) - return false +func XCTAssertTime( + _ time: Fixed, era: Int, year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, + file: StaticString = #file, line: UInt = #line +) -> Bool { + if time.era == era && time.year == year && time.month == month && time.day == day + && time.hour == hour && time.minute == minute && time.second == second + { + return true + } + XCTFail( + "Unexpected time components: \(time), expecting era=\(era), year=\(year), month=\(month), day=\(day), hour=\(hour), minute=\(minute), second=\(second)", + file: file, line: line) + return false } @discardableResult -func XCTAssertTime(_ time: Fixed, era: Int, year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int, file: StaticString = #file, line: UInt = #line) -> Bool { - if time.era == era && time.year == year && time.month == month && time.day == day && time.hour == hour && time.minute == minute && time.second == second && time.nanosecond == nanosecond { return true } - XCTFail("Unexpected time components: \(time), expecting era=\(era), year=\(year), month=\(month), day=\(day), hour=\(hour), minute=\(minute), second=\(second), nanosecond=\(nanosecond)", file: file, line: line) - return false +func XCTAssertTime( + _ time: Fixed, era: Int, year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, + nanosecond: Int, file: StaticString = #file, line: UInt = #line +) -> Bool { + if time.era == era && time.year == year && time.month == month && time.day == day + && time.hour == hour && time.minute == minute && time.second == second + && time.nanosecond == nanosecond + { + return true + } + XCTFail( + "Unexpected time components: \(time), expecting era=\(era), year=\(year), month=\(month), day=\(day), hour=\(hour), minute=\(minute), second=\(second), nanosecond=\(nanosecond)", + file: file, line: line) + return false } diff --git a/Tests/TimeTests/XCTestCase+Convenience.swift b/Tests/TimeTests/XCTestCase+Convenience.swift index 6146c43..a757e78 100644 --- a/Tests/TimeTests/XCTestCase+Convenience.swift +++ b/Tests/TimeTests/XCTestCase+Convenience.swift @@ -1,11 +1,11 @@ import XCTest extension XCTestCase { - - func wait(_ delay: TimeInterval) { - let e = self.expectation(description: "Wait for \(delay)s") - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { e.fulfill() } - self.wait(for: [e], timeout: delay + 0.1) - } - + + func wait(_ delay: TimeInterval) { + let e = self.expectation(description: "Wait for \(delay)s") + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { e.fulfill() } + self.wait(for: [e], timeout: delay + 0.1) + } + } diff --git a/time.podspec b/time.podspec new file mode 100644 index 0000000..2c7c206 --- /dev/null +++ b/time.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |s| + + s.name = "Time" + s.version = "1.0.2" + s.summary = "Time is a Swift package that makes it easy to perform robust and type-safe date and time calculations." + + s.homepage = "https://github.com/davedelong/time" + s.license = 'MIT' + s.author = { "Dave Delong" => "davedelong.com" } + s.source = { :git => "https://github.com/davedelong/time.git", :tag => s.version.to_s } + + s.platform = :ios + s.ios.deployment_target = "15.0" + s.swift_versions = "5.7" + s.framework = "Foundation" + s.pod_target_xcconfig = { + "ENABLE_TESTING_SEARCH_PATHS" => "YES", + } + s.source_files = "Sources/**/*.swift" + +end \ No newline at end of file