Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ struct MeetingFormView: View {
Text(label)
Spacer()
pill(
text: date.wrappedValue.formatted(.dateTime.day().month(.abbreviated).year()),
text: DateFormatter.meetingDate.string(from: date.wrappedValue),
isSelected: expandedField == dateField
) {
toggleExpansion(dateField)
Expand All @@ -220,7 +220,7 @@ struct MeetingFormView: View {
.disabled(!isDateFieldEnabled)
.accessibilityHidden(!isDateFieldEnabled)
pill(
text: date.wrappedValue.formatted(date: .omitted, time: .shortened),
text: DateFormatter.meetingTime.string(from: date.wrappedValue),
isSelected: expandedField == timeField
) {
toggleExpansion(timeField)
Expand Down Expand Up @@ -291,6 +291,8 @@ private struct MinuteIntervalTimePicker: UIViewRepresentable {
let datePicker = UIDatePicker()
datePicker.datePickerMode = .time
datePicker.preferredDatePickerStyle = .wheels
// Keep 24-hour wheels even when the device uses a 12-hour clock.
datePicker.locale = Locale(identifier: "en_US_POSIX@hours=h23")
datePicker.minuteInterval = minuteInterval
datePicker.minimumDate = range.lowerBound
datePicker.maximumDate = maximumDate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,14 @@ package struct MeetingsFormatter: MeetingsFormatterProtocol {
let calendar = Calendar.current

if calendar.isDate(date, inSameDayAs: now) {
return Strings.Header.today + " (\(DateFormatter.dayHeader.string(from: date)))"
return Strings.Header.today + " (\(DateFormatter.meetingDate.string(from: date)))"
} else {
return DateFormatter.dayHeader.string(from: date)
return DateFormatter.meetingDate.string(from: date)
}
}

package func timeRange(from start: Date, to end: Date) -> String {
let calendar = Calendar.current
let startPeriod = calendar.component(.hour, from: start) / 12
let endPeriod = calendar.component(.hour, from: end) / 12
let isSamePeriod = startPeriod == endPeriod
let startFormatter = isSamePeriod ? DateFormatter.meetingTimeWithoutPeriod : DateFormatter.meetingTime
let startString = startFormatter.string(from: start)
let startString = DateFormatter.meetingTime.string(from: start)
let endString = DateFormatter.meetingTime.string(from: end)
return "\(startString) - \(endString)"
}
Expand All @@ -54,30 +49,20 @@ package struct MeetingsFormatter: MeetingsFormatterProtocol {

// MARK: - Helpers

private extension DateFormatter {

static let dayHeader: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = .current
formatter.dateFormat = "EEEE, MMMM d"
return formatter
}()

}

private extension DateFormatter {
extension DateFormatter {

static let meetingTimeWithoutPeriod: DateFormatter = {
static let meetingDate: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = .current
formatter.dateFormat = "hh:mm"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.calendar = Calendar(identifier: .gregorian)
formatter.dateFormat = "dd.MM.yyyy"
return formatter
}()

static let meetingTime: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = .current
formatter.dateFormat = "hh:mm a"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm"
return formatter
}()

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -24,60 +24,65 @@ import WireCallingUI
struct MeetingsFormatterTests {

let formatter = MeetingsFormatter()
let calendar = Calendar.current
let calendar = Calendar(identifier: .gregorian)

// MARK: - Day Header Tests

@Test("dayHeader returns 'Today' for current date")
func testDayHeaderForToday() {
let now = Date()
func testDayHeaderForToday() throws {
let now = try makeDate(hour: 9, minute: 0)
let result = formatter.dayHeader(for: now, now: now)

#expect(result.contains("Today"))
#expect(result == "Today (08.09.2026)")
}

@Test("dayHeader returns formatted date for other days")
func testDayHeaderForOtherDays() {
let now = Date()
guard let futureDate = calendar.date(byAdding: .day, value: 5, to: now) else {
Issue.record("Failed to create future date")
return
}
@Test("dayHeader uses a numeric calendar date", arguments: [
(2026, 9, 13, "13.09.2026"),
(2026, 12, 31, "31.12.2026")
])
func testDayHeaderForOtherDays(year: Int, month: Int, day: Int, expected: String) throws {
let now = try makeDate(hour: 9, minute: 0)
let date = try #require(calendar.date(from: DateComponents(year: year, month: month, day: day)))

let result = formatter.dayHeader(for: futureDate, now: now)

#expect(!result.contains("Today"))
#expect(!result.isEmpty)
#expect(formatter.dayHeader(for: date, now: now) == expected)
}

// MARK: - Time Range Tests

@Test("timeRange places the period once for a morning range")
@Test("timeRange zero-pads morning hours without a period")
func timeRange_sameMorningPeriod() throws {
let start = try makeDate(hour: 7, minute: 30)
let end = try makeDate(hour: 7, minute: 40)

#expect(formatter.timeRange(from: start, to: end) == "07:30 - 07:40 AM")
#expect(formatter.timeRange(from: start, to: end) == "07:30 - 07:40")
}

@Test("timeRange places the period once for an afternoon range")
@Test("timeRange uses 24-hour afternoon hours")
func timeRange_sameAfternoonPeriod() throws {
let start = try makeDate(hour: 14, minute: 0)
let end = try makeDate(hour: 15, minute: 15)

#expect(formatter.timeRange(from: start, to: end) == "02:00 - 03:15 PM")
#expect(formatter.timeRange(from: start, to: end) == "14:00 - 15:15")
}

@Test("timeRange places the period on both times when crossing from morning to afternoon")
@Test("timeRange uses the same format across noon")
func timeRange_crossesPeriod() throws {
let start = try makeDate(hour: 11, minute: 30)
let end = try makeDate(hour: 13, minute: 15)

#expect(formatter.timeRange(from: start, to: end) == "11:30 AM - 01:15 PM")
#expect(formatter.timeRange(from: start, to: end) == "11:30 - 13:15")
}

@Test("timeRange formats midnight as zero and noon as twelve")
func timeRange_midnightAndNoon() throws {
let start = try makeDate(hour: 0, minute: 0)
let end = try makeDate(hour: 12, minute: 0)

#expect(formatter.timeRange(from: start, to: end) == "00:00 - 12:00")
}

private func makeDate(hour: Int, minute: Int) throws -> Date {
try #require(calendar.date(from: DateComponents(year: 2026, month: 6, day: 15, hour: hour, minute: minute)))
try #require(calendar.date(from: DateComponents(year: 2026, month: 9, day: 8, hour: hour, minute: minute)))
}

}
Loading