Skip to content

Commit 3f10b66

Browse files
authored
Fix Numeric String Serialization (#78)
* fix numeric serialization where many zeroes come after integer or before fraction * remove debug print * cleanup code comments * test 1_000_000 numerics on linux * comment out slow test
1 parent 5807407 commit 3f10b66

File tree

2 files changed

+93
-23
lines changed

2 files changed

+93
-23
lines changed

Sources/PostgresNIO/Data/PostgresData+Numeric.swift

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@ public struct PostgresNumeric: CustomStringConvertible, CustomDebugStringConvert
1818
}
1919

2020
public var debugDescription: String {
21+
var copy = self.value
22+
var values: [Int16] = []
23+
while let value = copy.readInteger(endianness: .big, as: Int16.self) {
24+
values.append(value)
25+
}
2126
return """
2227
ndigits: \(self.ndigits)
2328
weight: \(self.weight)
2429
sign: \(self.sign)
2530
dscale: \(self.dscale)
26-
value: \(self.value.debugDescription)
31+
value: \(values)
2732
"""
2833
}
2934

@@ -122,56 +127,73 @@ public struct PostgresNumeric: CustomStringConvertible, CustomDebugStringConvert
122127
guard self.ndigits > 0 else {
123128
return "0"
124129
}
130+
// print(self.debugDescription)
125131

132+
// Digits before the decimal point.
126133
var integer = ""
134+
135+
// Digits after the decimal point.
127136
var fractional = ""
128137

138+
// Consume digits from the value buffer.
129139
var value = self.value
130140
for offset in 0..<self.ndigits {
131-
/// extract current char and advance memory
132141
let char = value.readInteger(endianness: .big, as: Int16.self) ?? 0
133142

134-
/// convert the current char to its string form
135-
let string: String
136-
if char == 0 {
137-
/// 0 means 4 zeros
138-
string = "0000"
143+
// Depending on offset, append value before or after the decimal point.
144+
if self.weight - offset >= 0 {
145+
if offset == 0 {
146+
// First integer offset doesn't have trailing zeroes.
147+
integer += char.description
148+
} else {
149+
integer += String(repeating: "0", count: 4 - char.description.count) + char.description
150+
}
139151
} else {
140-
string = char.description
152+
fractional += String(repeating: "0", count: 4 - char.description.count)
153+
+ char.description
141154
}
155+
}
142156

143-
/// depending on our offset, append the string to before or after the decimal point
144-
if offset < self.weight + 1 {
145-
// insert zeros (skip leading)
146-
if offset > 0 {
147-
integer += String(repeating: "0", count: 4 - string.count)
157+
// Check for any remaining zeroes required before or after decimal point.
158+
let offset: Int16
159+
if self.weight > 0 {
160+
offset = (self.weight + 1) - self.ndigits
161+
} else {
162+
offset = abs(self.weight) - self.ndigits
163+
}
164+
if offset > 0 {
165+
for _ in 0..<offset {
166+
if self.weight > 0 {
167+
integer = integer + "0000"
168+
} else {
169+
fractional = "0000" + fractional
148170
}
149-
integer += string
150-
} else {
151-
// leading zeros matter with fractional
152-
fractional += String(repeating: "0", count: 4 - string.count) + string
153171
}
154172
}
155173

174+
// Prevent fraction without leading "0"
156175
if integer.count == 0 {
157176
integer = "0"
158177
}
159178

179+
// Remove extraneous zeroes at the end of the fraction.
160180
if fractional.count > self.dscale {
161-
/// use the dscale to remove extraneous zeroes at the end of the fractional part
162-
let lastSignificantIndex = fractional.index(fractional.startIndex, offsetBy: Int(self.dscale))
163-
fractional = String(fractional[..<lastSignificantIndex])
181+
let lastSignificant = fractional.index(
182+
fractional.startIndex,
183+
offsetBy: Int(self.dscale)
184+
)
185+
fractional = String(fractional[..<lastSignificant])
164186
}
165187

166-
/// determine whether fraction is empty and dynamically add `.`
188+
// Determine whether fraction is empty to add decimal point.
167189
let numeric: String
168190
if fractional != "" {
169191
numeric = integer + "." + fractional
170192
} else {
171193
numeric = integer
172194
}
173195

174-
/// use sign to determine adding a leading `-`
196+
// Indicate whether or not the value is negative.
175197
if (self.sign & 0x4000) != 0 {
176198
return "-" + numeric
177199
} else {

Tests/PostgresNIOTests/PostgresNIOTests.swift

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,12 +378,60 @@ final class PostgresNIOTests: XCTestCase {
378378
'1234.5678'::numeric as a,
379379
'-123.456'::numeric as b,
380380
'123456.789123'::numeric as c,
381-
'3.14159265358979'::numeric as d
381+
'3.14159265358979'::numeric as d,
382+
'10000'::numeric as e,
383+
'0.00001'::numeric as f,
384+
'100000000'::numeric as g,
385+
'0.000000001'::numeric as h,
386+
'100000000000'::numeric as i,
387+
'0.000000000001'::numeric as j,
388+
'123000000000'::numeric as k,
389+
'0.000000000123'::numeric as l,
390+
'0.5'::numeric as m
382391
""").wait()
383392
XCTAssertEqual(rows[0].column("a")?.string, "1234.5678")
384393
XCTAssertEqual(rows[0].column("b")?.string, "-123.456")
385394
XCTAssertEqual(rows[0].column("c")?.string, "123456.789123")
386395
XCTAssertEqual(rows[0].column("d")?.string, "3.14159265358979")
396+
XCTAssertEqual(rows[0].column("e")?.string, "10000")
397+
XCTAssertEqual(rows[0].column("f")?.string, "0.00001")
398+
XCTAssertEqual(rows[0].column("g")?.string, "100000000")
399+
XCTAssertEqual(rows[0].column("h")?.string, "0.000000001")
400+
XCTAssertEqual(rows[0].column("k")?.string, "123000000000")
401+
XCTAssertEqual(rows[0].column("l")?.string, "0.000000000123")
402+
XCTAssertEqual(rows[0].column("m")?.string, "0.5")
403+
}
404+
405+
func testSingleNumericParsing() throws {
406+
// this seemingly duped test is useful for debugging numeric parsing
407+
let conn = try PostgresConnection.test(on: eventLoop).wait()
408+
defer { try! conn.close().wait() }
409+
let numeric = "790226039477542363.6032384900176272473"
410+
let rows = try conn.query("""
411+
select
412+
'\(numeric)'::numeric as n
413+
""").wait()
414+
XCTAssertEqual(rows[0].column("n")?.string, numeric)
415+
}
416+
417+
func testRandomlyGeneratedNumericParsing() throws {
418+
// this test takes a long time to run
419+
return
420+
421+
let conn = try PostgresConnection.test(on: eventLoop).wait()
422+
defer { try! conn.close().wait() }
423+
424+
for _ in 0..<1_000_000 {
425+
let integer = UInt.random(in: UInt.min..<UInt.max)
426+
let fraction = UInt.random(in: UInt.min..<UInt.max)
427+
let number = "\(integer).\(fraction)"
428+
.trimmingCharacters(in: CharacterSet(["0"]))
429+
let rows = try conn.query("""
430+
select
431+
'\(number)'::numeric as n
432+
""").wait()
433+
XCTAssertEqual(rows[0].column("n")?.string, number)
434+
}
387435
}
388436

389437
func testNumericSerialization() throws {

0 commit comments

Comments
 (0)