@@ -66,7 +66,7 @@ enum JSONValue: Codable, Hashable, Sendable, Comparable {
66 case .null: return "null"
66 case .null: return "null"
67 case .bool(let v): return v ? "true" : "false"
67 case .bool(let v): return v ? "true" : "false"
68 case .int(let v): return String(v)
68 case .int(let v): return String(v)
69− case .double(let v): return String(v)
69+ case .double(let v): return Self.jsonNumber(v)
70 case .string(let v): return "\"\(Self.escapeJSONString(v))\""
70 case .string(let v): return "\"\(Self.escapeJSONString(v))\""
71 case .array(let items):
71 case .array(let items):
72 return "[" + items.map { $0.compactJSON }.joined(separator: ",") + "]"
72 return "[" + items.map { $0.compactJSON }.joined(separator: ",") + "]"
@@ -96,7 +96,7 @@ enum JSONValue: Codable, Hashable, Sendable, Comparable {
96 case .null: return "NULL"
96 case .null: return "NULL"
97 case .bool(let v): return v ? "true" : "false"
97 case .bool(let v): return v ? "true" : "false"
98 case .int(let v): return String(v)
98 case .int(let v): return String(v)
99− case .double(let v): return String(v)
99+ case .double(let v): return v.isFinite ? String(v) : "NULL"
100 case .string(let v): return "'\(v.replacingOccurrences(of: "'", with: "''"))'"
100 case .string(let v): return "'\(v.replacingOccurrences(of: "'", with: "''"))'"
101 case .array, .object:
101 case .array, .object:
102 return "'\(exportString.replacingOccurrences(of: "'", with: "''"))'"
102 return "'\(exportString.replacingOccurrences(of: "'", with: "''"))'"
@@ -137,7 +137,7 @@ enum JSONValue: Codable, Hashable, Sendable, Comparable {
137 case .null: return "null"
137 case .null: return "null"
138 case .bool(let v): return v ? "true" : "false"
138 case .bool(let v): return v ? "true" : "false"
139 case .int(let v): return String(v)
139 case .int(let v): return String(v)
140− case .double(let v): return String(v)
140+ case .double(let v): return Self.jsonNumber(v)
141 case .string(let v): return "\"\(Self.escapeJSONString(v))\""
141 case .string(let v): return "\"\(Self.escapeJSONString(v))\""
142 case .array(let items):
142 case .array(let items):
143 if items.isEmpty { return "[]" }
143 if items.isEmpty { return "[]" }
@@ -182,23 +182,43 @@ enum JSONValue: Codable, Hashable, Sendable, Comparable {
182 return result
182 return result
185+ /// Stable ordering bucket per case so cross-type comparisons form a total
186+ /// order. Integers and doubles share a bucket so they compare numerically.
187+ private var typeRank: Int {
189+ case .null: return 0
190+ case .bool: return 1
191+ case .int, .double: return 2
192+ case .string: return 3
193+ case .array: return 4
194+ case .object: return 5
185 /// Type-aware comparison: numbers compare numerically, strings
198 /// Type-aware comparison: numbers compare numerically, strings
186 /// lexicographically, nulls sort first, bools sort false < true.
199 /// lexicographically, nulls sort first, bools sort false < true.
187− /// Cross-type comparisons fall back to displayString.
200+ /// Different types are ordered by a stable type rank so that `<` is a strict
201+ /// weak ordering consistent with `==` (the `Comparable` contract requires
202+ /// that order-equivalent values are equal — a `displayString` fallback
203+ /// violated this, e.g. `.int(5)` vs `.string("5")` both render "5").
188 static func < (lhs: JSONValue, rhs: JSONValue) -> Bool {
204 static func < (lhs: JSONValue, rhs: JSONValue) -> Bool {
205+ if lhs.typeRank != rhs.typeRank { return lhs.typeRank < rhs.typeRank }
189 switch (lhs, rhs) {
206 switch (lhs, rhs) {
190 case (.null, .null): return false
207 case (.null, .null): return false
191− case (.null, _): return true
192− case (_, .null): return false
193 case (.bool(let a), .bool(let b)): return !a && b
208 case (.bool(let a), .bool(let b)): return !a && b
194 case (.int(let a), .int(let b)): return a < b
209 case (.int(let a), .int(let b)): return a < b
195 case (.double(let a), .double(let b)): return doubleLessThan(a, b)
210 case (.double(let a), .double(let b)): return doubleLessThan(a, b)
196− case (.int(let a), .double(let b)): return doubleLessThan(Double(a), b)
211+ case (.int(let a), .double(let b)): return intLessThanDouble(a, b)
197− case (.double(let a), .int(let b)): return doubleLessThan(a, Double(b))
212+ case (.double(let a), .int(let b)): return doubleLessThanInt(a, b)
198 case (.string(let a), .string(let b)):
213 case (.string(let a), .string(let b)):
199 return a.localizedStandardCompare(b) == .orderedAscending
214 return a.localizedStandardCompare(b) == .orderedAscending
215+ case (.array, .array), (.object, .object):
216+ // Same rank, structured: equal values must not be ordered; otherwise
217+ // use a deterministic serialization tiebreaker.
218+ if lhs == rhs { return false }
219+ return lhs.compactJSON < rhs.compactJSON
201− return lhs.displayString < rhs.displayString
221+ return false
@@ -211,6 +231,36 @@ enum JSONValue: Codable, Hashable, Sendable, Comparable {
211 return a < b
231 return a < b
234+ /// Exact `Int < Double` comparison that avoids the precision loss of
235+ /// `Double(Int)` for magnitudes beyond 2^53. NaN sorts after all numbers.
236+ private static func intLessThanDouble(_ a: Int, _ b: Double) -> Bool {
237+ if b.isNaN { return true }
238+ if let bi = Int(exactly: b) { return a < bi } // b is a whole, in-range number
239+ return Double(a) < b // b is fractional / infinite / out of range
242+ /// Exact `Double < Int` comparison, the mirror of `intLessThanDouble`.
243+ /// NaN sorts after all numbers, so `NaN < anyInt` is false.
244+ private static func doubleLessThanInt(_ a: Double, _ b: Int) -> Bool {
245+ if a.isNaN { return false }
246+ if let ai = Int(exactly: a) { return ai < b } // a is a whole, in-range number
247+ return a < Double(b) // a is fractional / infinite / out of range
250+ /// Exact `Int == Double` test (true only when the double represents exactly
251+ /// the same integer — `Int(exactly:)` is nil for fractional/out-of-range b).
252+ private static func intEqualsDouble(_ a: Int, _ b: Double) -> Bool {
253+ if let bi = Int(exactly: b) { return a == bi }
257+ /// JSON-safe rendering of a double: JSON has no NaN/Infinity literals, so
258+ /// non-finite values serialize as `null` rather than the invalid `nan`/`inf`
259+ /// that `String(_:)` would emit.
260+ static func jsonNumber(_ v: Double) -> String {
261+ v.isFinite ? String(v) : "null"
214 // MARK: - Equatable / Hashable
264 // MARK: - Equatable / Hashable
216 /// Equality treats numerically-equal integers and doubles as equal
266 /// Equality treats numerically-equal integers and doubles as equal
@@ -223,8 +273,8 @@ enum JSONValue: Codable, Hashable, Sendable, Comparable {
223 case (.bool(let a), .bool(let b)): return a == b
273 case (.bool(let a), .bool(let b)): return a == b
224 case (.int(let a), .int(let b)): return a == b
274 case (.int(let a), .int(let b)): return a == b
225 case (.double(let a), .double(let b)): return a == b
275 case (.double(let a), .double(let b)): return a == b
226− case (.int(let a), .double(let b)): return Double(a) == b
276+ case (.int(let a), .double(let b)): return Self.intEqualsDouble(a, b)
227− case (.double(let a), .int(let b)): return a == Double(b)
277+ case (.double(let a), .int(let b)): return Self.intEqualsDouble(b, a)
228 case (.string(let a), .string(let b)): return a == b
278 case (.string(let a), .string(let b)): return a == b
229 case (.array(let a), .array(let b)): return a == b
279 case (.array(let a), .array(let b)): return a == b
230 case (.object(let a), .object(let b)): return a == b
280 case (.object(let a), .object(let b)): return a == b