Skip to content
This repository has been archived by the owner on Mar 30, 2024. It is now read-only.

Commit

Permalink
Merge pull request #48 from vapor-community/numeric-parsing
Browse files Browse the repository at this point in the history
Numeric value parsing fixes
  • Loading branch information
vzsg authored Jun 15, 2017
2 parents e8b5c62 + 408600c commit 5efa27f
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 50 deletions.
204 changes: 154 additions & 50 deletions Sources/PostgreSQL/Bind/BinaryUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,73 +145,177 @@ struct BinaryUtils {
return Float64(bigEndian: convert(value))
}

// MARK: - Numberic

struct NumericConstants {
static let signNaN: Int16 = -16384
static let signNegative: Int16 = 16384
static let decDigits = 4
}

static func parseNumeric(value: UnsafeMutablePointer<Int8>) -> String {
let sign = parseInt16(value: value.advanced(by: 4))
// MARK: - Numeric

struct Numeric {
private static let signNaN: Int16 = -16384
private static let signNegative: Int16 = 16384
private static let decDigits = 4
private static let NBASE: Int16 = 10000
private static let halfNBASE: Int16 = 5000
private static let roundPowers: [Int16] = [0, 1000, 100, 10]

// Check for NaN
guard sign != NumericConstants.signNaN else {
return "NaN"
}
var sign: Int16
var weight: Int
var dscale: Int
var numberOfDigits: Int
var digits: [Int16]

// Check that we actually have some digits
let numberOfDigits = Int(parseInt16(value: value))
guard numberOfDigits > 0 else {
return "0"
init(value: UnsafeMutablePointer<Int8>) {
sign = BinaryUtils.parseInt16(value: value.advanced(by: 4))
weight = Int(BinaryUtils.parseInt16(value: value.advanced(by: 2)))

var dscale = Int(BinaryUtils.parseInt16(value: value.advanced(by: 6)))
if dscale < 0 {
dscale = 0
}
self.dscale = dscale

numberOfDigits = Int(BinaryUtils.parseInt16(value: value))
digits = (0..<numberOfDigits).map { BinaryUtils.parseInt16(value: value.advanced(by: 8 + $0 * 2)) }
}

let dscale = Int(parseInt16(value: value.advanced(by: 6)))

// Add all digits to a string
var number: String = ""
for i in 0..<numberOfDigits {
let int16 = parseInt16(value: value.advanced(by: 8 + i * 2))
private func getDigit(atIndex index: Int) -> String {
let int16: Int16
if index >= 0 && index < numberOfDigits {
int16 = digits[index]
} else {
int16 = 0
}
let stringDigits = String(int16)

if i == 0 {
number += stringDigits
}
else {
// The number of digits should be 4 (DEC_DIGITS),
// so pad if necessary.
number += String(repeating: "0", count: 4 - stringDigits.characters.count) + stringDigits
guard index != 0 else {
return stringDigits
}

// The number of digits should be 4 (DEC_DIGITS),
// so pad if necessary.
return String(repeating: "0", count: Numeric.decDigits - stringDigits.characters.count) + stringDigits
}

if dscale > 0 {
// Make sure we have enough decimal digits by pre-padding with zeros
if number.characters.count < dscale {
number = String(repeating: "0", count: dscale - number.characters.count) + number
/// Function for rounding numeric values.
/// The code is based on https://github.com/postgres/postgres/blob/3a0d473192b2045cbaf997df8437e7762d34f3ba/src/backend/utils/adt/numeric.c#L8594
mutating func roundIfNeeded() {
// Decimal digits wanted
var totalDigits = (weight + 1) * Numeric.decDigits + dscale

// If less than 0, result should be 0
guard totalDigits >= 0 else {
digits = []
weight = 0
sign = 0
return
}
else {
// Remove any trailing zeros
while number.hasSuffix("0") {
number.remove(at: number.index(number.endIndex, offsetBy: -1))
}

// NBASE digits wanted
var nbaseDigits = (totalDigits + Numeric.decDigits - 1) / Numeric.decDigits

// 0, or number of decimal digits to keep in last NBASE digit
totalDigits = totalDigits % Numeric.decDigits

guard nbaseDigits < numberOfDigits || (nbaseDigits == numberOfDigits && totalDigits > 0) else {
return
}

// Insert decimal point
number.insert(".", at: number.index(number.endIndex, offsetBy: -dscale))
numberOfDigits = nbaseDigits

// If we have at least a zero before the decimal point
if dscale == number.characters.count - 1 {
number = "0"+number
var carry: Int16
if totalDigits == 0 {
carry = digits[0] >= Numeric.halfNBASE ? 1 : 0
} else {
nbaseDigits -= 1

// Must round within last NBASE digit
var pow10 = Numeric.roundPowers[totalDigits]
let extra = digits[nbaseDigits] % pow10
digits[nbaseDigits] = digits[nbaseDigits] - extra

carry = 0
if extra >= pow10 / 2 {
pow10 += digits[nbaseDigits]
if pow10 >= Numeric.NBASE {
pow10 -= Numeric.NBASE
carry = 1
}
digits[nbaseDigits] = pow10
}
}

// Propagate carry if needed
while carry > 0 {
nbaseDigits -= 1
if nbaseDigits < 0 {
digits.insert(0, at: 0)
nbaseDigits = 0

numberOfDigits += 1
weight += 1
}

carry += digits[nbaseDigits]

if carry >= Numeric.NBASE {
digits[nbaseDigits] = carry - Numeric.NBASE
carry = 1
} else {
digits[nbaseDigits] = carry
carry = 0
}
}
}

// Make number negative if necessary
if sign == NumericConstants.signNegative {
number = "-"+number
var string: String {
// Check for NaN
guard sign != Numeric.signNaN else {
return "NaN"
}

guard !digits.isEmpty else {
return "0"
}

var digitIndex = 0
var string: String = ""

// Make number negative if necessary
if sign == Numeric.signNegative {
string += "-"
}

// Add all digits before decimal point
if weight < 0 {
digitIndex = weight + 1
string += "0"
} else {
while digitIndex <= weight {
string += getDigit(atIndex: digitIndex)
digitIndex += 1
}
}

guard dscale > 0 else {
return string
}

// Add digits after decimal point
string += "."
let decimalIndex = string.endIndex

for _ in stride(from: 0, to: dscale, by: Numeric.decDigits) {
string += getDigit(atIndex: digitIndex)
digitIndex += 1
}

let endIndex = string.index(decimalIndex, offsetBy: dscale + 1)
string = string.substring(to: endIndex)
return string
}

return number
}

static func parseNumeric(value: UnsafeMutablePointer<Int8>) -> String {
var numeric = Numeric(value: value)
numeric.roundIfNeeded()
return numeric.string
}

// MARK: - Date / Time
Expand Down
6 changes: 6 additions & 0 deletions Tests/PostgreSQLTests/BinaryUtilsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,18 @@ class BinaryUtilsTests: XCTestCase {
("0000000000000000", "0"),
("00010000000000000001", "1"),
("00010000400000000001", "-1"),
("00010000000000020001", "1.00"),
("00010000400000050001", "-1.00000"),
("00000000c0000000", "NaN"),
("0006000200000009000109291a8504d2162e2328", "123456789.123456789"),
("0006000340000006270f270f270f270f270f26ac", "-9999999999999999.999999"),
("000600020000000a000109291a8504d2162e2328", "123456789.1234567890"),
("0006000340000009270f270f270f270f270f26ac", "-9999999999999999.999999000"),
("0001000000000000007b", "123"),
("0002ffff0000000526941388", "0.98765"),
("0002ffff4000000526941388", "-0.98765"),
("0002ffff0000000426941388", "0.9877"),
("0002ffff0000000026941388", "1"),
]

for (hexString, numericString) in numericTests {
Expand Down
49 changes: 49 additions & 0 deletions Tests/PostgreSQLTests/PostgreSQLTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class PostgreSQLTests: XCTestCase {
("testInts", testInts),
("testFloats", testFloats),
("testNumeric", testNumeric),
("testNumericAverage", testNumericAverage),
("testJSON", testJSON),
("testIntervals", testIntervals),
("testPoints", testPoints),
Expand Down Expand Up @@ -304,6 +305,54 @@ class PostgreSQLTests: XCTestCase {
}
}

func testNumericAverage() throws {
let conn = try postgreSQL.makeConnection()

try conn.execute("DROP TABLE IF EXISTS jobs")
try conn.execute("CREATE TABLE jobs (id SERIAL PRIMARY KEY NOT NULL, title VARCHAR(255) NOT NULL, pay INT8 NOT NULL)")

try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["A", 100])
try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["A", 200])
try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["A", 300])
try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["A", 400])
try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["A", 500])
try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["B", 100])
try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["B", 200])
try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["B", 900])
try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["C", 100])
try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["C", 1100])
try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["D", 100])
try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["E", 500])

defer {
_ = try? conn.execute("DROP TABLE IF EXISTS jobs")
}

let result = try conn.execute("select title, avg(pay) as average, count(*) as cnt from jobs group by title order by average desc")

guard let array = result.array else {
XCTFail("Result was not an array")
return
}

XCTAssertEqual(5, array.count)
XCTAssertEqual("C", array[0]["title"]?.string)
XCTAssertEqual("E", array[1]["title"]?.string)
XCTAssertEqual("B", array[2]["title"]?.string)
XCTAssertEqual("A", array[3]["title"]?.string)
XCTAssertEqual("D", array[4]["title"]?.string)
XCTAssertEqual(2, array[0]["cnt"]?.int)
XCTAssertEqual(1, array[1]["cnt"]?.int)
XCTAssertEqual(3, array[2]["cnt"]?.int)
XCTAssertEqual(5, array[3]["cnt"]?.int)
XCTAssertEqual(1, array[4]["cnt"]?.int)
XCTAssertEqual(600, array[0]["average"]?.double)
XCTAssertEqual(500, array[1]["average"]?.double)
XCTAssertEqual(400, array[2]["average"]?.double)
XCTAssertEqual(300, array[3]["average"]?.double)
XCTAssertEqual(100, array[4]["average"]?.double)
}

func testJSON() throws {
let conn = try postgreSQL.makeConnection()

Expand Down

0 comments on commit 5efa27f

Please sign in to comment.