Skip to content

Commit bd34ab5

Browse files
committed
feat: add support for time format
1 parent 3caa2ef commit bd34ab5

File tree

5 files changed

+149
-3
lines changed

5 files changed

+149
-3
lines changed

Sources/Draft201909Validator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public class Draft201909Validator: Validator {
6262
"regex": validateRegex,
6363
"json-pointer": validateJSONPointer,
6464
"duration": validateDuration,
65+
"time": validateTime,
6566
]
6667

6768
public required init(schema: Bool) {

Sources/Draft202012Validator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public class Draft202012Validator: Validator {
6363
"regex": validateRegex,
6464
"json-pointer": validateJSONPointer,
6565
"duration": validateDuration,
66+
"time": validateTime,
6667
]
6768

6869
public required init(schema: Bool) {

Sources/Draft7Validator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public class Draft7Validator: Validator {
5050
"uri": validateURI,
5151
"json-pointer": validateJSONPointer,
5252
"regex": validateRegex,
53+
"time": validateTime,
5354
]
5455

5556
public required init(schema: Bool) {

Sources/Format/time.swift

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import Foundation
2+
3+
4+
/*
5+
partial-time https://tools.ietf.org/html/rfc3339
6+
7+
time-hour = 2DIGIT ; 00-23
8+
time-minute = 2DIGIT ; 00-59
9+
time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second
10+
; rules
11+
time-secfrac = "." 1*DIGIT
12+
time-numoffset = ("+" / "-") time-hour ":" time-minute
13+
time-offset = "Z" / time-numoffset
14+
15+
partial-time = time-hour ":" time-minute ":" time-second
16+
[time-secfrac]
17+
*/
18+
19+
20+
var timeExpression: NSRegularExpression = {
21+
let time = #"""
22+
(?x)
23+
^
24+
(?<hour>(([01]\d)|(2[0-3])))
25+
:
26+
(?<minute>([0-5]\d))
27+
:
28+
(?<second>(([0-5]\d)|(60)))
29+
(?<secfrac>\.\d+)?
30+
(?<offset>
31+
(
32+
Z
33+
|
34+
(?<numoffset>
35+
(?<numoffsetdirection>[+-])
36+
(?<numoffsethour>([01]\d)|2[0-3])
37+
:
38+
(?<numoffsetminute>[0-5]\d)
39+
)
40+
)
41+
)
42+
$
43+
"""#
44+
return try! NSRegularExpression(pattern: time, options: [.caseInsensitive])
45+
}()
46+
47+
48+
typealias TimeOffset = (hour: Int, minute: Int)
49+
50+
51+
enum Offset {
52+
case zulu
53+
case positive(TimeOffset)
54+
case negative(TimeOffset)
55+
}
56+
57+
58+
struct Time {
59+
var hour: Int
60+
var minute: Int
61+
var second: Int
62+
var offset: Offset
63+
64+
init(hour: Int, minute: Int, second: Int, offset: Offset) {
65+
self.hour = hour
66+
self.minute = minute
67+
self.second = second
68+
self.offset = offset
69+
}
70+
71+
init?(value: String) {
72+
guard let match = timeExpression.firstMatch(in: value, range: NSMakeRange(0, value.utf16.count)) else {
73+
return nil
74+
}
75+
76+
let hourRange = Range(match.range(withName: "hour"), in: value)!
77+
hour = Int(value[hourRange])!
78+
79+
let minuteRange = Range(match.range(withName: "minute"), in: value)!
80+
minute = Int(value[minuteRange])!
81+
82+
let secondRange = Range(match.range(withName: "second"), in: value)!
83+
second = Int(value[secondRange])!
84+
85+
if Range(match.range(withName: "numoffset"), in: value) != nil {
86+
let direction = value[Range(match.range(withName: "numoffsetdirection"), in: value)!]
87+
88+
let offsetHourRange = Range(match.range(withName: "numoffsethour"), in: value)!
89+
let offsetHour = Int(value[offsetHourRange])!
90+
91+
let offsetMinuteRange = Range(match.range(withName: "numoffsetminute"), in: value)!
92+
let offsetMinute = Int(value[offsetMinuteRange])!
93+
94+
if direction == "+" {
95+
offset = .positive((hour: offsetHour, minute: offsetMinute))
96+
} else if direction == "-" {
97+
offset = .negative((hour: offsetHour, minute: offsetMinute))
98+
} else {
99+
fatalError("ProgramaticError: Incorrect regular expression for time parsing invalid direction")
100+
}
101+
} else {
102+
offset = .zulu
103+
}
104+
}
105+
106+
// returns the time converted to UTC (without num offset)
107+
var zulu: Time {
108+
switch offset {
109+
case .zulu:
110+
return self
111+
case let .positive(offset):
112+
return Time(hour: hour - offset.hour, minute: minute - offset.minute, second: second, offset: .zulu)
113+
case let .negative(offset):
114+
return Time(hour: hour + offset.hour, minute: minute + offset.minute, second: second, offset: .zulu)
115+
}
116+
}
117+
}
118+
119+
120+
func isValidTime(_ value: String) -> Bool {
121+
guard let time = Time(value: value) else {
122+
return false
123+
}
124+
125+
let zuluTime = time.zulu
126+
if zuluTime.second == 60 && (zuluTime.hour != 23 || zuluTime.minute != 59) {
127+
return false
128+
}
129+
130+
return true
131+
}
132+
133+
134+
func validateTime(_ context: Context, _ value: String) -> AnySequence<ValidationError> {
135+
if isValidTime(value) {
136+
return AnySequence(EmptyCollection())
137+
}
138+
139+
return AnySequence([
140+
ValidationError(
141+
"'\(value)' is not a valid RFC 3339 formatted time.",
142+
instanceLocation: context.instanceLocation,
143+
keywordLocation: context.keywordLocation
144+
)
145+
])
146+
}

Tests/JSONSchemaTests/JSONSchemaCases.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ class JSONSchemaCases: XCTestCase {
146146
"iri-reference.json",
147147
"iri.json",
148148
"relative-json-pointer.json",
149-
"time.json",
150149
"uri-reference.json",
151150
"uri-template.json",
152151
] + additionalExclusions)
@@ -182,7 +181,6 @@ class JSONSchemaCases: XCTestCase {
182181
"iri-reference.json",
183182
"iri.json",
184183
"relative-json-pointer.json",
185-
"time.json",
186184
"uri-reference.json",
187185
"uri-template.json",
188186
] + additionalExclusions)
@@ -217,7 +215,6 @@ class JSONSchemaCases: XCTestCase {
217215
"iri-reference.json",
218216
"iri.json",
219217
"relative-json-pointer.json",
220-
"time.json",
221218
"uri-reference.json",
222219
"uri-template.json",
223220
] + additionalExclusions)

0 commit comments

Comments
 (0)