Skip to content

Commit 8b25725

Browse files
fix: DateOnlyCoder uses regex to strip time + timezone (#287)
For DateOnlyCoding, we assume the device local timezone when parsing. For this, we now strip out everything except for the date from the input string, and only then run the formatter. This produces a naive Date object in the local timezone. When we send it back to the backend, we only format the date component, so roundtripping should be safe in all cases. 🤞 See #286
1 parent eac6d5c commit 8b25725

File tree

5 files changed

+38
-27
lines changed

5 files changed

+38
-27
lines changed

Common/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ let package = Package(
88
name: "Common",
99
platforms: [
1010
.iOS(.v17),
11-
.macOS(.v12),
11+
.macOS(.v13),
1212
],
1313
products: [
1414
.library(

Common/Sources/Common/DateOnlyCoder.swift

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import Foundation
99
import MetaCodable
1010

11+
import os
12+
1113
public struct DateOnlyCoder: HelperCoder {
1214
public typealias Coded = Date
1315

@@ -17,36 +19,26 @@ public struct DateOnlyCoder: HelperCoder {
1719
let container = try decoder.singleValueContainer()
1820
let dateStr = try container.decode(String.self)
1921

20-
let df = DateFormatter()
21-
df.timeZone = TimeZone(secondsFromGMT: 0)
22-
df.dateFormat = "yyyy-MM-dd"
22+
let ex = /(\d{4}-\d{2}-\d{2}).*/
2323

24-
if let res = df.date(from: dateStr) {
25-
return res
24+
guard let match = try? ex.wholeMatch(in: dateStr) else {
25+
throw DateDecodingError.invalidDate(string: dateStr)
2626
}
2727

28-
let iso = ISO8601DateFormatter()
29-
iso.formatOptions = [
30-
.withInternetDateTime,
31-
.withFractionalSeconds,
32-
]
33-
if let res = iso.date(from: dateStr) {
34-
return res
35-
}
28+
let df = DateFormatter()
29+
df.dateFormat = "yyyy-MM-dd"
3630

37-
iso.formatOptions = [.withInternetDateTime]
38-
if let res = iso.date(from: dateStr) {
39-
return res
31+
guard let res = df.date(from: String(match.1)) else {
32+
throw DateDecodingError.invalidDate(string: dateStr)
4033
}
4134

42-
throw DateDecodingError.invalidDate(string: dateStr)
35+
return res
4336
}
4437

4538
public func encode(_ value: Coded, to encoder: Encoder) throws {
4639
var container = encoder.singleValueContainer()
4740

4841
let formatter = DateFormatter()
49-
formatter.timeZone = TimeZone(secondsFromGMT: 0)
5042
formatter.dateFormat = "yyyy-MM-dd"
5143

5244
try container.encode(formatter.string(from: value))

Common/Tests/CommonTests/DateOnlyCoderTest.swift

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,41 @@ func testDateOnlyCoderDecode() throws {
3636
let decoder = JSONDecoder()
3737

3838
var decoded = try decoder.decode(TestStruct.self, from: dateOnly)
39+
print(decoded)
3940

4041
let exp = try #require(Calendar.current.date(from: DateComponents(year: 2023, month: 12, day: 4, hour: 0, minute: 0, second: 0)))
4142
#expect(decoded.created == exp)
4243

43-
let tz = TimeZone(secondsFromGMT: 60 * 60)!
4444
let dateTime = try #require(#"{"created":"2023-12-04T09:10:24+01:00"}"#.data(using: .utf8))
4545

4646
decoded = try decoder.decode(TestStruct.self, from: dateTime)
4747

48-
var cal = Calendar.current
49-
cal.timeZone = tz
48+
let cal = Calendar.current
5049
let components = cal.dateComponents([.year, .month, .day, .hour, .minute, .second], from: decoded.created)
5150

5251
#expect(components.year == 2023)
5352
#expect(components.month == 12)
5453
#expect(components.day == 4)
55-
#expect(components.hour == 9)
56-
#expect(components.minute == 10)
57-
#expect(components.second == 24)
54+
#expect(components.hour == 0)
55+
#expect(components.minute == 0)
56+
#expect(components.second == 0)
57+
}
58+
59+
@Test("Arbitrary timezones get stripped out", arguments: 1 ... 10)
60+
func testArbitraryTimezonesGetStrippedOut(offset: Int) throws {
61+
let dateOnly = try #require("{\"created\":\"2028-11-02T00:00:00+0\(offset):00\"}".data(using: .utf8))
62+
63+
let decoder = JSONDecoder()
64+
65+
let decoded = try decoder.decode(TestStruct.self, from: dateOnly)
66+
67+
let cal = Calendar.current
68+
let components = cal.dateComponents([.year, .month, .day, .hour, .minute, .second], from: decoded.created)
69+
70+
#expect(components.year == 2028)
71+
#expect(components.month == 11)
72+
#expect(components.day == 2)
73+
#expect(components.hour == 0)
74+
#expect(components.minute == 0)
75+
#expect(components.second == 0)
5876
}

DataModel/Tests/DataModelTests/DocumentModelTest.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ struct DocumentModelTest {
2626
#expect(document.title == "Quittung")
2727
#expect(document.tags == [1, 2, 3])
2828

29-
#expect(dateApprox(document.created, datetime(year: 2024, month: 12, day: 21, hour: 0, minute: 0, second: 0, tz: TimeZone(secondsFromGMT: 0)!)))
29+
#expect(dateApprox(document.created, datetime(year: 2024, month: 12, day: 21, hour: 0, minute: 0, second: 0)))
3030
#expect(try dateApprox(#require(document.modified), datetime(year: 2024, month: 12, day: 21, hour: 21, minute: 41, second: 49, tz: tz)))
3131
#expect(try dateApprox(#require(document.added), datetime(year: 2024, month: 12, day: 21, hour: 21, minute: 26, second: 36, tz: tz)))
3232

@@ -53,7 +53,7 @@ struct DocumentModelTest {
5353
#expect(document.title == "Quittung")
5454
#expect(document.tags == [1, 2, 3])
5555

56-
#expect(dateApprox(document.created, datetime(year: 2024, month: 12, day: 21, hour: 0, minute: 0, second: 0, tz: tz)))
56+
#expect(dateApprox(document.created, datetime(year: 2024, month: 12, day: 21, hour: 0, minute: 0, second: 0)))
5757
#expect(try dateApprox(#require(document.modified), datetime(year: 2024, month: 12, day: 21, hour: 21, minute: 41, second: 49, tz: tz)))
5858
#expect(try dateApprox(#require(document.added), datetime(year: 2024, month: 12, day: 21, hour: 21, minute: 26, second: 36, tz: tz)))
5959

current_changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Fix timezone conversion in document created date: on versions below 2.16.0 of Paperless-ngx, the timezone was incorrectly set and resulted in the date shifting when saving.

0 commit comments

Comments
 (0)