From 3845d3c00c18f63fcdbe80f9c520087f38a3504e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20B=C3=BCnz?= Date: Tue, 5 Mar 2024 17:53:42 +0100 Subject: [PATCH] Add timezone to dateformatter (#500) --- CriticalMaps.xcodeproj/project.pbxproj | 4 + .../xcschemes/NextRideFeatureTests.xcscheme | 58 ++++++++++++ .../Helpers/DateFormatter+Additions.swift | 18 ++-- .../Sources/Helpers/Timezone+Extras.swift | 26 ++++++ .../NextRideFeature/NextRideCore.swift | 5 +- .../NextRideCoreTests.swift | 26 +++++- .../NextRideFeatureTests/RideTests.swift | 90 +++++++++++++++++++ LisbonPortugal.gpx | 8 ++ Testplans/NextRideFeatureTests.xctestplan | 42 +++++++++ Testplans/SnapshotTests.xctestplan | 11 --- 10 files changed, 266 insertions(+), 22 deletions(-) create mode 100644 CriticalMapsKit/.swiftpm/xcode/xcshareddata/xcschemes/NextRideFeatureTests.xcscheme create mode 100644 CriticalMapsKit/Sources/Helpers/Timezone+Extras.swift create mode 100644 CriticalMapsKit/Tests/NextRideFeatureTests/RideTests.swift create mode 100644 LisbonPortugal.gpx create mode 100644 Testplans/NextRideFeatureTests.xctestplan diff --git a/CriticalMaps.xcodeproj/project.pbxproj b/CriticalMaps.xcodeproj/project.pbxproj index 55edfe80..539e09e2 100644 --- a/CriticalMaps.xcodeproj/project.pbxproj +++ b/CriticalMaps.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 7327344D277118670007579E /* TwitterFeaturePreviewApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterFeaturePreviewApp.swift; sourceTree = ""; }; 7347AB262A27BE2A00BCA949 /* ChatFeature.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = ChatFeature.xctestplan; sourceTree = ""; }; 7347AB272A27C0A900BCA949 /* MapFeature.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MapFeature.xctestplan; sourceTree = ""; }; + 7355D2E42B91C4350097DD42 /* NextRideFeatureTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = NextRideFeatureTests.xctestplan; sourceTree = ""; }; 73654C6B26C85FCF004BE38B /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; 7369207C28D0D92100EA0584 /* SnapshotTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SnapshotTests.xctestplan; sourceTree = ""; }; 738214EF271F49AB009847C7 /* appIcon-5.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "appIcon-5.png"; sourceTree = ""; }; @@ -51,6 +52,7 @@ 73CF5FF1263EB0A6001925A3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 73CF5FF6263EB0A6001925A3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 73EB0310263F23A100941D57 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; + 73F010D82B94A19700C2B613 /* LisbonPortugal.gpx */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = LisbonPortugal.gpx; sourceTree = ""; }; 73FD0B302779F5500061539D /* GuideFeaturePreview.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GuideFeaturePreview.app; sourceTree = BUILT_PRODUCTS_DIR; }; 73FD0B322779F5500061539D /* GuideFeaturePreviewApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuideFeaturePreviewApp.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -126,6 +128,7 @@ 7347AB282A27C0CA00BCA949 /* Testplans */ = { isa = PBXGroup; children = ( + 7355D2E42B91C4350097DD42 /* NextRideFeatureTests.xctestplan */, 7347AB272A27C0A900BCA949 /* MapFeature.xctestplan */, 7347AB262A27BE2A00BCA949 /* ChatFeature.xctestplan */, 7369207C28D0D92100EA0584 /* SnapshotTests.xctestplan */, @@ -150,6 +153,7 @@ 73CF5FB9263EAF5B001925A3 = { isa = PBXGroup; children = ( + 73F010D82B94A19700C2B613 /* LisbonPortugal.gpx */, 73BABF82277B0A2900D9790F /* CHANGELOG.md */, 73BABF83277B0A2900D9790F /* README.md */, 732734462771172C0007579E /* CriticalMapsKit */, diff --git a/CriticalMapsKit/.swiftpm/xcode/xcshareddata/xcschemes/NextRideFeatureTests.xcscheme b/CriticalMapsKit/.swiftpm/xcode/xcshareddata/xcschemes/NextRideFeatureTests.xcscheme new file mode 100644 index 00000000..fcabccfc --- /dev/null +++ b/CriticalMapsKit/.swiftpm/xcode/xcshareddata/xcschemes/NextRideFeatureTests.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CriticalMapsKit/Sources/Helpers/DateFormatter+Additions.swift b/CriticalMapsKit/Sources/Helpers/DateFormatter+Additions.swift index 7261bdca..6a4a83b7 100644 --- a/CriticalMapsKit/Sources/Helpers/DateFormatter+Additions.swift +++ b/CriticalMapsKit/Sources/Helpers/DateFormatter+Additions.swift @@ -1,3 +1,4 @@ +import ComposableArchitecture import Foundation public extension DateFormatter { @@ -34,16 +35,21 @@ extension Date.FormatStyle { locale: .autoupdatingCurrent ) - public static let localeAwareShortDate = Self( - date: .numeric, - time: .omitted, - locale: .autoupdatingCurrent - ) + public static let localeAwareShortDate: Self = { + @Dependency(\.timeZone) var timezone + return Self( + date: .numeric, + time: .omitted, + locale: .autoupdatingCurrent, + timeZone: timezone + ) + }() public static let localeAwareShortTime = Self( date: .omitted, time: .shortened, - locale: .autoupdatingCurrent + locale: .autoupdatingCurrent, + timeZone: .germany ) public static func chatTime(_ cal: Calendar = .autoupdatingCurrent) -> Self { diff --git a/CriticalMapsKit/Sources/Helpers/Timezone+Extras.swift b/CriticalMapsKit/Sources/Helpers/Timezone+Extras.swift new file mode 100644 index 00000000..cb06108a --- /dev/null +++ b/CriticalMapsKit/Sources/Helpers/Timezone+Extras.swift @@ -0,0 +1,26 @@ +import Foundation + +public extension TimeZone { + // Europe + static let germany = TimeZone(identifier: "Europe/Berlin")! + static let gmt = TimeZone(identifier: "GMT")! + static let spain = TimeZone(identifier: "Europe/Madrid")! + static let france = TimeZone(identifier: "Europe/Paris")! + static let greece = TimeZone(identifier: "Europe/Athens")! + + // America + static let ecuador = TimeZone(identifier: "America/Guayaquil")! + static let cuba = TimeZone(identifier: "America/Havana")! + + // Africa + static let egypt = TimeZone(identifier: "Africa/Cairo")! + static let southAfrica = TimeZone(identifier: "Africa/Johannesburg")! + + // Asia + static let india = TimeZone(identifier: "Asia/Kolkata")! + static let japan = TimeZone(identifier: "Asia/Tokyo")! + + // USA + static let newYork = TimeZone(identifier: "America/New_York")! + static let losAngeles = TimeZone(identifier: "America/Los_Angeles")! +} diff --git a/CriticalMapsKit/Sources/NextRideFeature/NextRideCore.swift b/CriticalMapsKit/Sources/NextRideFeature/NextRideCore.swift index 8193f4c9..07aabbca 100644 --- a/CriticalMapsKit/Sources/NextRideFeature/NextRideCore.swift +++ b/CriticalMapsKit/Sources/NextRideFeature/NextRideCore.swift @@ -16,6 +16,7 @@ public struct NextRideFeature: ReducerProtocol { @Dependency(\.mainQueue) public var mainQueue @Dependency(\.coordinateObfuscator) public var coordinateObfuscator @Dependency(\.isNetworkAvailable) public var isNetworkAvailable + @Dependency(\.calendar) public var calendar public struct State: Equatable { public init(nextRide: Ride? = nil) { @@ -70,6 +71,7 @@ public struct NextRideFeature: ReducerProtocol { case let .nextRideResponse(.failure(error)): logger.error("Get next ride failed 🛑 with error: \(error)") return .none + case let .nextRideResponse(.success(rides)): guard !rides.isEmpty else { logger.info("Rides array is empty") @@ -101,7 +103,7 @@ public struct NextRideFeature: ReducerProtocol { return byDate } - if Calendar.current.isDate(lhs.dateTime, inSameDayAs: rhs.dateTime) { + if calendar.isDate(lhs.dateTime, inSameDayAs: rhs.dateTime) { return lhsCoordinate.distance(from: userLocation) < rhsCoordinate.distance(from: userLocation) } else { return byDate @@ -113,7 +115,6 @@ public struct NextRideFeature: ReducerProtocol { logger.info("No upcoming events after filter") return .none } - return EffectTask(value: .setNextRide(filteredRide)) case let .setNextRide(ride): diff --git a/CriticalMapsKit/Tests/NextRideFeatureTests/NextRideCoreTests.swift b/CriticalMapsKit/Tests/NextRideFeatureTests/NextRideCoreTests.swift index 3f9fccb7..15afdda3 100644 --- a/CriticalMapsKit/Tests/NextRideFeatureTests/NextRideCoreTests.swift +++ b/CriticalMapsKit/Tests/NextRideFeatureTests/NextRideCoreTests.swift @@ -12,7 +12,7 @@ final class NextRideCoreTests: XCTestCase { let now = { Calendar.current.date( from: .init( - timeZone: .init(secondsFromGMT: 0), + timeZone: .germany, year: 2022, month: 3, day: 25, @@ -43,6 +43,26 @@ final class NextRideCoreTests: XCTestCase { ) } + var berlinNow: Ride { + Ride( + id: 0, + slug: nil, + title: "CriticalMaps Berlin", + description: nil, + dateTime: now(), + location: nil, + latitude: 53.1235, + longitude: 13.4234, + estimatedParticipants: nil, + estimatedDistance: nil, + estimatedDuration: nil, + enabled: true, + disabledReason: nil, + disabledReasonMessage: nil, + rideType: .criticalMass + ) + } + var falkensee: Ride { Ride( id: 0, @@ -64,7 +84,6 @@ final class NextRideCoreTests: XCTestCase { } var rides: [Ride] { [berlin, falkensee] } - let coordinate = Coordinate(latitude: 53.1234, longitude: 13.4233) func test_disabledNextRideFeature_shouldNotRequestRides() async { @@ -87,7 +106,7 @@ final class NextRideCoreTests: XCTestCase { await store.receive(.nextRideResponse(.success([]))) } - func test_getNextRide_shouldReturnMockRide() async { + func test_getRides_shouldUpdateRidesInUserTimezone() async { let store = TestStore( initialState: .init(), reducer: NextRideFeature() @@ -343,6 +362,7 @@ final class NextRideCoreTests: XCTestCase { .encoded() } store.dependencies.date = .constant(now()) + store.dependencies.calendar = .autoupdatingCurrent store.dependencies.isNetworkAvailable = true // then diff --git a/CriticalMapsKit/Tests/NextRideFeatureTests/RideTests.swift b/CriticalMapsKit/Tests/NextRideFeatureTests/RideTests.swift new file mode 100644 index 00000000..0efe1067 --- /dev/null +++ b/CriticalMapsKit/Tests/NextRideFeatureTests/RideTests.swift @@ -0,0 +1,90 @@ +import ComposableArchitecture +import Helpers +import SharedModels +import XCTest + +final class RideTests: XCTestCase { + func test_rideInGMTTimezone() { + // arrange + let ride = Ride.mock() + + // act + withDependencies { + $0.timeZone = .gmt + } operation: { + XCTAssertEqual( + "20:00", + ride.dateTime.humanReadableTime + ) + } + } + + func test_rideInGermanTimezone() { + // arrange + let ride = Ride.mock() + + // act + withDependencies { + $0.timeZone = .germany + } operation: { + XCTAssertEqual( + "20:00", + ride.dateTime.humanReadableTime + ) + } + } + + func test_rideInGreeceTimezone() { + // arrange + let ride = Ride.mock() + + // act + withDependencies { + $0.timeZone = .greece + $0.calendar = .init(identifier: .gregorian) + } operation: { + XCTAssertEqual( + "20:00", + ride.dateTime.humanReadableTime + ) + } + } + + func test_rideInEcuadorTimezone() { + // arrange + let ride = Ride.mock() + + // act + withDependencies { + $0.timeZone = .ecuador + $0.calendar = .init(identifier: .gregorian) + } operation: { + XCTAssertEqual( + "20:00", + ride.dateTime.humanReadableTime + ) + } + } +} + +extension Ride { + static func mock() -> Self { + Self( + id: 0, + slug: nil, + title: "CriticalMaps Berlin", + description: nil, + dateTime: Date(timeIntervalSince1970: 1711738800), + location: nil, + latitude: 53.1235, + longitude: 13.4234, + estimatedParticipants: nil, + estimatedDistance: nil, + estimatedDuration: nil, + enabled: true, + disabledReason: nil, + disabledReasonMessage: nil, + rideType: .criticalMass + ) + } +} diff --git a/LisbonPortugal.gpx b/LisbonPortugal.gpx new file mode 100644 index 00000000..d1ad48d2 --- /dev/null +++ b/LisbonPortugal.gpx @@ -0,0 +1,8 @@ + + + + Cupertino + + + + diff --git a/Testplans/NextRideFeatureTests.xctestplan b/Testplans/NextRideFeatureTests.xctestplan new file mode 100644 index 00000000..887d162d --- /dev/null +++ b/Testplans/NextRideFeatureTests.xctestplan @@ -0,0 +1,42 @@ +{ + "configurations" : [ + { + "id" : "EC875105-37D8-413B-989B-09862018C3C9", + "name" : "Test Scheme Action", + "options" : { + "language" : "en", + "region" : "GB" + } + }, + { + "id" : "1858BF7A-D057-468A-B31F-AD3EB6637E6E", + "name" : "Configuration 2", + "options" : { + "environmentVariableEntries" : [ + { + "key" : "setenv(\"TZ\", identifier, 1)", + "value" : "" + } + ] + } + } + ], + "defaultOptions" : { + "language" : "en", + "locationScenario" : { + "identifier" : "London, England", + "referenceType" : "built-in" + }, + "region" : "GB" + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "NextRideFeatureTests", + "name" : "NextRideFeatureTests" + } + } + ], + "version" : 1 +} diff --git a/Testplans/SnapshotTests.xctestplan b/Testplans/SnapshotTests.xctestplan index 9ccea0b4..e7b836d3 100644 --- a/Testplans/SnapshotTests.xctestplan +++ b/Testplans/SnapshotTests.xctestplan @@ -70,17 +70,6 @@ "identifier" : "StyleguideTests", "name" : "StyleguideTests" } - }, - { - "skippedTests" : [ - "TweetTests", - "TwitterFeedCoreTests" - ], - "target" : { - "containerPath" : "container:CriticalMapsKit", - "identifier" : "TwitterFeedFeatureTests", - "name" : "TwitterFeedFeatureTests" - } } ], "version" : 1