From bbe0de56e9ffa15c929d6638d71a5fe077c523ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20Bystr=C3=B6m=20Ericsson?= Date: Wed, 20 Nov 2024 07:36:10 +0100 Subject: [PATCH 1/2] Rework binary integer description error-handling using (#95). --- Sources/CoreKit/BinaryInteger+Text.swift | 43 +- Sources/CoreKit/Models/TextInt+Decoding.swift | 215 +++--- Sources/CoreKit/Models/TextInt+Encoding.swift | 97 ++- Sources/CoreKit/Models/TextInt+Error.swift | 22 - .../Models/TextInt+Exponentiation.swift | 4 +- Sources/CoreKit/Models/TextInt+Numerals.swift | 45 +- Sources/CoreKit/Models/TextInt.swift | 42 +- Sources/StdlibIntKit/StdlibInt+Text.swift | 33 +- Sources/TestKit/Utilities+Optional.swift | 35 + Sources/TestKit/Utilities+Text.swift | 29 +- Tests/Benchmarks/Fibonacci.swift | 4 +- Tests/Benchmarks/TextInt+Base10.swift | 20 +- Tests/Benchmarks/TextInt+Base16.swift | 20 +- .../StdlibInt+Integers.swift | 8 +- Tests/StdlibIntKitTests/StdlibInt+Text.swift | 12 +- .../BinaryInteger+Division.swift.swift | 40 +- .../UltimathnumTests/BinaryInteger+Text.swift | 665 ++---------------- Tests/UltimathnumTests/TextInt+Decoding.swift | 358 ++++++++++ Tests/UltimathnumTests/TextInt+Encoding.swift | 190 +++++ .../TextInt+Exponentiation.swift | 6 +- Tests/UltimathnumTests/TextInt+Letters.swift | 6 +- Tests/UltimathnumTests/TextInt+Numerals.swift | 42 +- Tests/UltimathnumTests/TextInt+Pyramids.swift | 149 ++++ Tests/UltimathnumTests/TextInt+Samples.swift | 282 ++++++++ Tests/UltimathnumTests/TextInt.swift | 28 +- 25 files changed, 1406 insertions(+), 989 deletions(-) delete mode 100644 Sources/CoreKit/Models/TextInt+Error.swift create mode 100644 Sources/TestKit/Utilities+Optional.swift create mode 100644 Tests/UltimathnumTests/TextInt+Decoding.swift create mode 100644 Tests/UltimathnumTests/TextInt+Encoding.swift create mode 100644 Tests/UltimathnumTests/TextInt+Pyramids.swift create mode 100644 Tests/UltimathnumTests/TextInt+Samples.swift diff --git a/Sources/CoreKit/BinaryInteger+Text.swift b/Sources/CoreKit/BinaryInteger+Text.swift index 7d756921..0850f377 100644 --- a/Sources/CoreKit/BinaryInteger+Text.swift +++ b/Sources/CoreKit/BinaryInteger+Text.swift @@ -20,32 +20,41 @@ // MARK: Initializers //=------------------------------------------------------------------------= - /// Decodes the `description`, if possible. + /// Returns the `value` of `description`, or `nil`. + /// + /// ```swift + /// format.decode(description)?.optional() + /// ``` /// /// ### Binary Integer Description /// - /// - Note: The default format is `TextInt.decimal`. + /// - Note: The `error` is set if the operation is `lossy`. /// - /// - Note: Decoding failures throw `TextInt.Error`. + /// - Note: It produces `nil` if the `description` is `invalid`. /// - @inlinable public init?(_ description: String) { - always: do { - self = try TextInt.decimal.decode(description) - } catch { - return nil - } + /// - Note: The default `format` is `TextInt.decimal`. + /// + @inlinable public init?(_ description: consuming String) { + self.init(description, using: TextInt.decimal) } - /// Decodes the `description` using the given `format`, if possible. + /// Returns the `value` of `description`, or `nil`. + /// + /// ```swift + /// format.decode(description)?.optional() + /// ``` /// /// ### Binary Integer Description /// - /// - Note: The default format is `TextInt.decimal`. + /// - Note: The `error` is set if the operation is `lossy`. + /// + /// - Note: It produces `nil` if the `description` is `invalid`. /// - /// - Note: Decoding failures throw `TextInt.Error`. + /// - Note: The default `format` is `TextInt.decimal`. /// - @inlinable public init(_ description: some StringProtocol, using format: TextInt) throws { - self = try format.decode(description) + @inlinable public init?(_ description: consuming String, using format: borrowing TextInt) { + guard let instance: Self = format.decode(description)?.optional() else { return nil } + self = ((instance)) } //=------------------------------------------------------------------------= @@ -58,8 +67,6 @@ /// /// - Note: The default format is `TextInt.decimal`. /// - /// - Note: Decoding failures throw `TextInt.Error`. - /// @inlinable public var description: String { self.description(using: TextInt.decimal) } @@ -70,9 +77,7 @@ /// /// - Note: The default format is `TextInt.decimal`. /// - /// - Note: Decoding failures throw `TextInt.Error`. - /// - @inlinable public func description(using format: TextInt) -> String { + @inlinable public func description(using format: borrowing TextInt) -> String { format.encode(self) } } diff --git a/Sources/CoreKit/Models/TextInt+Decoding.swift b/Sources/CoreKit/Models/TextInt+Decoding.swift index 99c8cdb1..6fc7b783 100644 --- a/Sources/CoreKit/Models/TextInt+Decoding.swift +++ b/Sources/CoreKit/Models/TextInt+Decoding.swift @@ -17,51 +17,62 @@ extension TextInt { // MARK: Utilities //=------------------------------------------------------------------------= - @inlinable public func decode(_ description: StaticString) -> T { - description.withUTF8Buffer { - try! self.decode($0) - } - } - - @inlinable public func decode(_ description: some StringProtocol) throws -> T { - var description = String(description) - return try description.withUTF8 { - try self.decode($0) + /// Returns the `value` of `description` and an `error` indicator, or `nil`. + /// + /// ### Binary Integer Description + /// + /// - Note: The `error` is set if the operation is `lossy`. + /// + /// - Note: It produces `nil` if the `description` is `invalid`. + /// + /// - Note: The default `format` is `TextInt.decimal`. + /// + @inlinable public func decode( + _ description: consuming String, + as type: Integer.Type = Integer.self + ) -> Optional> where Integer: BinaryInteger { + + description.withUTF8 { + self.decode($0, as: Integer.self) } } - @inlinable public func decode(_ description: UnsafeBufferPointer) throws -> T { - var magnitude = Fallible(T.Magnitude.zero, error: true) + /// Returns the `value` of `description` and an `error` indicator, or `nil`. + /// + /// ### Binary Integer Description + /// + /// - Note: The `error` is set if the operation is `lossy`. + /// + /// - Note: It produces `nil` if the `description` is `invalid`. + /// + /// - Note: The default `format` is `TextInt.decimal`. + /// + @inlinable public func decode( + _ description: UnsafeBufferPointer, + as type: Integer.Type = Integer.self + ) -> Optional> where Integer: BinaryInteger { + var body = description[...] let sign = TextInt.remove(from: &body, prefix: Self.sign) ?? Sign.plus let mask = TextInt.remove(from: &body, prefix: Self.mask) != nil - // TODO: consider simpler error handling, only checking mask if infinite + var magnitude: Optional> = nil - if self.power.div == 1 { - try self.words16(numerals: UnsafeBufferPointer(rebasing: body)) { - magnitude = T.Magnitude.exactly($0, mode: .unsigned) - } - - } else { - try self.words10(numerals: UnsafeBufferPointer(rebasing: body)) { - magnitude = T.Magnitude.exactly($0, mode: .unsigned) - } + self.decode(body: UnsafeBufferPointer(rebasing: body)) { + magnitude = Integer.Magnitude.exactly($0, mode: Signedness.unsigned) } - if magnitude.error { - throw Error.lossy - } - - if mask, T.isFinite { - throw Error.lossy - } + guard var magnitude = consume magnitude else { return nil } if mask { magnitude.value.toggle() } - return try T.exactly(sign: sign, magnitude: magnitude.value).prune(Error.lossy) + if mask, Integer.isFinite { + magnitude.error = true + } + + return Integer.exactly(sign: sign, magnitude: magnitude.value).veto(magnitude.error) } } @@ -75,118 +86,110 @@ extension TextInt { // MARK: Utilities //=------------------------------------------------------------------------= - @usableFromInline package func words10( - numerals: consuming UnsafeBufferPointer, success: (DataInt) -> Void - ) throws { - //=--------------------------------------= - Swift.assert(self.power.div != 1) - //=--------------------------------------= - // text must contain at least one numeral - //=--------------------------------------= - if numerals.isEmpty { - throw Error.invalid + @inlinable package func decode( + body: consuming UnsafeBufferPointer, + callback: (DataInt) -> Void + ) -> Void { + + if self.power.div == 1 { + self.decode16(body: body, callback: callback) + } else { - numerals = UnsafeBufferPointer(rebasing: numerals.drop(while:{ $0 == 48 })) + self.decode10(body: body, callback: callback) } + } + + //=------------------------------------------------------------------------= + // MARK: Utilities x Non-generic & Non-inlinable + //=------------------------------------------------------------------------= + + @usableFromInline package func decode10( + body: consuming UnsafeBufferPointer, + callback: (DataInt) -> Void + ) -> Void { + + Swift.assert(self.power.div != 1) //=--------------------------------------= - // capacity is measured in radix powers + if body.isEmpty { return } + //=--------------------------------------= + body = UnsafeBufferPointer(rebasing: body.drop(while:{ $0 == 48 })) //=--------------------------------------= let divisor = Nonzero(unchecked: self.exponent) - var (stride): IX = IX(numerals.count).remainder(divisor) - var capacity: IX = IX(numerals.count).quotient (divisor).unchecked() + var steps: IX = IX(body.count).remainder(divisor) + var capacity: IX = IX(body.count).quotient (divisor).unchecked() - if (stride).isZero { - (stride) = self.exponent + if steps.isZero { + steps = self.exponent } else { capacity = capacity.incremented().unchecked() } //=--------------------------------------= - return try Swift.withUnsafeTemporaryAllocation(of: UX.self, capacity: Int(capacity)) { + Swift.withUnsafeTemporaryAllocation(of: UX.self, capacity: Int(capacity)) { let words = MutableDataInt.Body($0)![unchecked: .., success: (DataInt) -> Void - ) throws { - //=--------------------------------------= + @usableFromInline package func decode16( + body: consuming UnsafeBufferPointer, + callback: (DataInt) -> Void + ) -> Void { + Swift.assert(self.power.div == 1) Swift.assert(self.exponent.count(Bit.one) == Count(1)) //=--------------------------------------= - // text must contain at least one numeral - //=--------------------------------------= - if numerals.isEmpty { - throw Error.invalid - } else { - numerals = UnsafeBufferPointer(rebasing: numerals.drop(while:{ $0 == 48 })) - } + if body.isEmpty { return } //=--------------------------------------= - // capacity is measured in radix powers + body = UnsafeBufferPointer(rebasing: body.drop(while:{ $0 == 48 })) //=--------------------------------------= let divisor = Nonzero(unchecked: self.exponent) - var (stride): IX = IX(numerals.count).remainder(divisor) - var capacity: IX = IX(numerals.count).quotient (divisor).unchecked() + var steps: IX = IX(body.count).remainder(divisor) + var capacity: IX = IX(body.count).quotient (divisor).unchecked() - if (stride).isZero { - (stride) = self.exponent + if steps.isZero { + steps = self.exponent } else { capacity = capacity.incremented().unchecked() } //=--------------------------------------= - return try Swift.withUnsafeTemporaryAllocation(of: UX.self, capacity: Int(capacity)) { - let words = MutableDataInt.Body($0)![unchecked: ...Body($0)![unchecked: ..( info: consuming U32, @@ -106,21 +106,26 @@ extension TextInt { ) -> String where Integer: UnsignedInteger { let body = (consume body).value - + if Integer.size <= UX.size { var small = UX(load: body) - let range = PartialRangeUpTo(IX(Bit(!small.isZero))) - return small.withUnsafeMutableBinaryIntegerBody { - self.encode(info: info, normalized: $0[unchecked: range]) + let count = IX(Bit(!small.isZero)) + return Swift.withUnsafeMutablePointer(to: &small) { + let normalized = MutableDataInt.Body($0, count: count) + return self.encode(info: info, normalized: normalized) } } else { return body.withUnsafeBinaryIntegerBody(as: U8.self) { - self.encode(info: info, body: $0) + return self.encode(info: info, body: $0) } } } + //=------------------------------------------------------------------------= + // MARK: Utilities x Non-generic & Non-inlinable + //=------------------------------------------------------------------------= + @usableFromInline package func encode( info: consuming UnsafeBufferPointer, body: consuming DataInt.Body @@ -129,61 +134,58 @@ extension TextInt { let count = body.count(as: UX.self) return Swift.withUnsafeTemporaryAllocation(of: UX.self, capacity: Swift.Int(count)) { - let words = MutableDataInt.Body($0.baseAddress!, count: count) - //=----------------------------------= - // pointee: initialization - //=----------------------------------= + let words = MutableDataInt.Body($0.baseAddress.unchecked(), count: count) + for index: IX in words.indices { let start: IX = index.times(IX(MemoryLayout.stride)).unchecked() words[unchecked: index] = DataInt(body[unchecked: start...]).load(as: UX.self) } - //=----------------------------------= - // pointee: deferred deinitialization - //=----------------------------------= + defer { words.deinitialize() } - //=----------------------------------= + return self.encode(info: info, normalized: words.normalized()) } } + /// ### Development + /// + /// - TODO: Better size approximation. + /// @usableFromInline package func encode( info: consuming UnsafeBufferPointer, normalized body: consuming MutableDataInt.Body ) -> String { Swift.assert(body.isNormal) - //=--------------------------------------= - // text: capacity upper bound - //=--------------------------------------= + var capacity = IX(raw: body.nondescending(Bit.zero)) - let speed = if self.power.div == 1 { - IX(raw: self.power.div.size()) - } else { - IX(raw: self.power.div.nondescending(Bit.zero)).decremented().unchecked() + let speed: Nonzero + + if self.power.div == 1 { + speed = Nonzero(unchecked: IX(raw: self.power.div.size())) + } else { + let x = IX(raw: self.power.div.nondescending(Bit.zero)) + speed = Nonzero(unchecked: x.decremented().unchecked()) } - capacity /= speed - capacity += 1 + Swift.assert(speed.value > 1) + capacity = capacity.quotient(speed).unchecked("speed > 1") + capacity = capacity.incremented( ).unchecked("speed > 1") capacity *= self.exponent capacity += IX(info.count) - //=--------------------------------------= + return Swift.withUnsafeTemporaryAllocation(of: UInt8.self, capacity: Int(capacity)) { ascii in - //=----------------------------------= - // pointee: initialization - //=----------------------------------= - ascii.initialize(repeating: UInt8(ascii: "0")) - //=----------------------------------= - var chunk: UX = 000 // must be zero var asciiIndex: Int = ascii.endIndex var chunkIndex: Int = ascii.endIndex + + ascii.initialize(repeating: UInt8(ascii: "0")) + + var chunk: UX = 0 let radix = Nonzero(unchecked: UX(load: self.radix as U8)) - //=----------------------------------= - // text: set numerals - //=----------------------------------= - major: while true { - + + major: while true { if self.power.div != 1 { chunk = (body).divisionSetQuotientGetRemainder(self.power) body = (body).normalized() @@ -194,35 +196,26 @@ extension TextInt { Swift.assert(chunk.isZero) } - minor: repeat { - - let lowest: UX - (chunk, lowest) = chunk.division(radix).components() - let element = try! self.numerals.encode(U8(load: lowest)) + minor: repeat { + let value: UX + (chunk, value) = chunk.division(radix).components() + let element = self.numerals.encode(U8(load: value)).unchecked() precondition(asciiIndex > ascii .startIndex) asciiIndex = ascii.index(before: asciiIndex) ascii.initializeElement(at: asciiIndex, to: UInt8(element)) - } while !chunk.isZero - //=------------------------------= - if body.isEmpty { break } - //=------------------------------= - // note preinitialization to 48s - //=------------------------------= + + if body.isEmpty { break } chunkIndex = chunkIndex - Int(self.exponent) asciiIndex = chunkIndex } - //=----------------------------------= - // text: set sign and/or mask - //=----------------------------------= + for element in info.reversed() { precondition(asciiIndex > ascii.startIndex) asciiIndex = ascii.index(before: asciiIndex) ascii.initializeElement(at: asciiIndex, to: UInt8(element)) } - //=----------------------------------= - // pointee: move de/initialization - //=----------------------------------= + let prefix = UnsafeMutableBufferPointer(rebasing: ascii[..= 2` - @inlinable public init(_ radix: UX) throws { + @inlinable public init?(_ radix: UX) { if radix < 2 { - throw Error.invalid + return nil } var power = radix as UX diff --git a/Sources/CoreKit/Models/TextInt+Numerals.swift b/Sources/CoreKit/Models/TextInt+Numerals.swift index 508f378b..9dc7e821 100644 --- a/Sources/CoreKit/Models/TextInt+Numerals.swift +++ b/Sources/CoreKit/Models/TextInt+Numerals.swift @@ -52,36 +52,23 @@ extension TextInt { /// Creates a new instance with a radix of `10`. @inlinable public init() { - try! self.init(radix: 10) + self.init(radix: 10)! } /// Creates a new instance using the given `radix` and `letters`. /// /// - Requires: `0 ≤ radix ≤ 36` /// - /// - Throws: `TextInt.Error.invalid` if the `radix` is invalid. - /// - @inlinable public init( - radix: some BinaryInteger, - letters: Letters = .lowercase - ) throws { - try self.init( - radix: try UX.exactly(radix).prune(Error.invalid), - letters: letters - ) + @inlinable public init?(radix: some BinaryInteger, letters: Letters = .lowercase) { + guard let radix = UX.exactly(radix).optional() else { return nil } + self.init(radix: radix, letters: letters) } /// Creates a new instance using the given `radix` and `letters`. /// /// - Requires: `0 ≤ radix ≤ 36` /// - /// - Throws: `TextInt.Error.invalid` if the `radix` is invalid. - /// - @inlinable public init( - radix: UX, - letters: Letters = .lowercase - ) throws { - + @inlinable public init?(radix: UX,letters: Letters = .lowercase) { if radix <= 10 { self.i00x10 = U8(load: radix) self.i10x36 = U8.zero @@ -89,7 +76,7 @@ extension TextInt { self.i00x10 = 0000010 self.i10x36 = U8(load: radix).minus(10).unchecked() } else { - throw TextInt.Error.invalid + return nil } self.o00x10 = 48 @@ -144,7 +131,7 @@ extension TextInt { /// /// - Note: This conversion is case-insensitive. /// - @inlinable public func decode(_ data: U8) throws -> U8 { + @inlinable public func decode(_ data: U8) -> Optional { var next = data &- 48 if next < self.i00x10 { @@ -157,7 +144,7 @@ extension TextInt { return next &+ 10 } - throw TextInt.Error.invalid + return nil } /// An integer to ASCII numeral conversion. @@ -170,7 +157,7 @@ extension TextInt { /// /// - Note: This conversion is case-sensitive. /// - @inlinable public func encode(_ data: U8) throws -> U8 { + @inlinable public func encode(_ data: U8) -> Optional { if data < self.i00x10 { return data &+ self.o00x10 } @@ -181,24 +168,28 @@ extension TextInt { return next &+ self.o10x36 } - throw TextInt.Error.invalid + return nil } //=--------------------------------------------------------------------= // MARK: Utilities //=--------------------------------------------------------------------= - /// Decodes the given `numerals` and truncates the result if it is too big. - @inlinable internal func load(_ numerals: borrowing UnsafeBufferPointer, as type: UX.Type) throws -> UX { + /// Decodes the given `numerals` and returns the bit pattern that fits. + @inlinable package func load( + _ numerals: UnsafeBufferPointer, as type: UX.Type = UX.self + ) -> Optional { + var value = UX.zero let radix = UX(load: self.radix) for numeral: UInt8 in numerals { + guard let increment = self.decode(U8(numeral)) else { return nil } value &*= radix - value &+= UX(load: try self.decode(U8(numeral))) + value &+= UX(load: increment) } - return value as UX + return value } } } diff --git a/Sources/CoreKit/Models/TextInt.swift b/Sources/CoreKit/Models/TextInt.swift index 3952f0ee..b01612c3 100644 --- a/Sources/CoreKit/Models/TextInt.swift +++ b/Sources/CoreKit/Models/TextInt.swift @@ -44,19 +44,19 @@ /// /// - Note: This value caches the result of `radix(2)`. /// - public static let binary = Self.radix(2) + public static let binary = Self.radix(2)! /// A `TextInt` instance with a radix of `10`. /// /// - Note: This value caches the result of `radix(10)`. /// - public static let decimal = Self.radix(10) + public static let decimal = Self.radix(10)! /// A `TextInt` instance with a radix of `16`. /// /// - Note: This value caches the result of `radix(16)`. /// - public static let hexadecimal = Self.radix(16) + public static let hexadecimal = Self.radix(16)! //=------------------------------------------------------------------------= // MARK: State @@ -101,9 +101,7 @@ /// /// - Requires: `2 ≤ radix ≤ 36` /// - /// - Note: Use `init(radix:letters:)` to recover from invalid radices. - /// - @inlinable public static func radix(_ radix: some BinaryInteger) -> Self { + @inlinable public static func radix(_ radix: some BinaryInteger) -> Optional { Self.radix(UX(radix)) } @@ -111,40 +109,28 @@ /// /// - Requires: `2 ≤ radix ≤ 36` /// - /// - Note: Use `init(radix:letters:)` to recover from invalid radices. - /// - @inlinable public static func radix(_ radix: UX) -> Self { - try! Self(radix: radix) + @inlinable public static func radix(_ radix: UX) -> Optional { + Self(radix: radix) } /// Creates a new instance using the given `radix` and `letters`. /// /// - Requires: `2 ≤ radix ≤ 36` /// - /// - Throws: `TextInt.Error.invalid` if the `radix` is invalid. - /// - @inlinable public init( - radix: some BinaryInteger, - letters: Letters = .lowercase - ) throws { - try self.init( - radix: try UX.exactly(radix).prune(Error.invalid), - letters: letters - ) + @inlinable public init?(radix: some BinaryInteger, letters: Letters = .lowercase) { + guard let radix = UX.exactly(radix).optional() else { return nil } + self.init(radix: radix, letters: letters) } /// Creates a new instance using the given `radix` and `letters`. /// /// - Requires: `2 ≤ radix ≤ 36` /// - /// - Throws: `TextInt.Error.invalid` if the `radix` is invalid. - /// - @inlinable public init( - radix: UX, - letters: Letters = .lowercase - ) throws { - self.base = try Numerals(radix: radix, letters: letters) - let exponentiation = try Exponentiation(radix) + @inlinable public init?(radix: UX, letters: Letters = .lowercase) { + guard let base = Numerals(radix: radix, letters: letters) else { return nil } + guard let exponentiation = Exponentiation.init(((radix))) else { return nil } + + self.base = base self.exponent = exponentiation.exponent as IX self.power = Divider21(Swift.max(1, exponentiation.power)) } diff --git a/Sources/StdlibIntKit/StdlibInt+Text.swift b/Sources/StdlibIntKit/StdlibInt+Text.swift index d8cf64ce..e25c93a7 100644 --- a/Sources/StdlibIntKit/StdlibInt+Text.swift +++ b/Sources/StdlibIntKit/StdlibInt+Text.swift @@ -22,17 +22,23 @@ extension StdlibInt { /// Decodes the decimal `description`, if possible. /// + /// ```swift + /// format.decode(description)?.optional() + /// ``` + /// /// ### Binary Integer Description /// - /// - Note: The default format is `TextInt.decimal`. + /// - Note: The `error` is set if the operation is `lossy`. /// - /// - Note: Decoding failures throw `TextInt.Error`. + /// - Note: It produces `nil` if the `description` is `invalid`. + /// + /// - Note: The default format is `TextInt.decimal`. /// /// ### Binary Integer Description (StdlibInt) /// /// - Note: `String.init(_:radix:)` does not use `TextInt`. /// - @inlinable public init?(_ description: String) { + @inlinable public init?(_ description: consuming String) { if let base = Base(description) { self.init(base) } else { @@ -42,18 +48,25 @@ extension StdlibInt { /// Decodes the `description` using the given `format`, if possible. /// + /// ```swift + /// format.decode(description)?.optional() + /// ``` + /// /// ### Binary Integer Description /// - /// - Note: The default format is `TextInt.decimal`. + /// - Note: The `error` is set if the operation is `lossy`. + /// + /// - Note: It produces `nil` if the `description` is `invalid`. /// - /// - Note: Decoding failures throw `TextInt.Error`. + /// - Note: The default format is `TextInt.decimal`. /// /// ### Binary Integer Description (StdlibInt) /// /// - Note: `String.init(_:radix:)` does not use `TextInt`. /// - @inlinable public init(_ description: some StringProtocol, as format: TextInt) throws { - try self.init(Base(description, using: format)) + @inlinable public init?(_ description: consuming String, using format: borrowing TextInt) { + guard let base = Base(description, using: format) else { return nil } + self.init(base) } //=------------------------------------------------------------------------= @@ -66,8 +79,6 @@ extension StdlibInt { /// /// - Note: The default format is `TextInt.decimal`. /// - /// - Note: Decoding failures throw `TextInt.Error`. - /// /// ### Binary Integer Description (StdlibInt) /// /// - Note: `String.init(_:radix:)` does not use `TextInt`. @@ -82,13 +93,11 @@ extension StdlibInt { /// /// - Note: The default format is `TextInt.decimal`. /// - /// - Note: Decoding failures throw `TextInt.Error`. - /// /// ### Binary Integer Description (StdlibInt) /// /// - Note: `String.init(_:radix:)` does not use `TextInt`. /// - @inlinable public func description(using format: TextInt) -> String { + @inlinable public func description(using format: borrowing TextInt) -> String { self.base.description(using: format) } } diff --git a/Sources/TestKit/Utilities+Optional.swift b/Sources/TestKit/Utilities+Optional.swift new file mode 100644 index 00000000..6f2c90ff --- /dev/null +++ b/Sources/TestKit/Utilities+Optional.swift @@ -0,0 +1,35 @@ +//=----------------------------------------------------------------------------= +// This source file is part of the Ultimathnum open source project. +// +// Copyright (c) 2023 Oscar Byström Ericsson +// Licensed under Apache License, Version 2.0 +// +// See http://www.apache.org/licenses/LICENSE-2.0 for license information. +//=----------------------------------------------------------------------------= + +//*============================================================================* +// MARK: * Utilities x Optional +//*============================================================================* + +extension Optional { + + //=------------------------------------------------------------------------= + // MARK: Transformations + //=------------------------------------------------------------------------= + + @inlinable public consuming func prune(_ error: @autoclosure () -> Error) throws(Error) -> Wrapped { + if let self { + return self + } else { + throw error() + } + } + + @inlinable public consuming func result(_ error: @autoclosure () -> Error) -> Result { + if let self { + return Result.success(self) + } else { + return Result.failure(error()) + } + } +} diff --git a/Sources/TestKit/Utilities+Text.swift b/Sources/TestKit/Utilities+Text.swift index 8bfd3527..6f1c94d6 100644 --- a/Sources/TestKit/Utilities+Text.swift +++ b/Sources/TestKit/Utilities+Text.swift @@ -76,6 +76,33 @@ extension Bit { //*============================================================================* extension TextInt { + + //=------------------------------------------------------------------------= + // MARK: Metadata + //=------------------------------------------------------------------------= + + public static let all: [Self] = { + Self.lowercase + + Self.uppercase + }() + + public static let lowercase: [Self] = Self.radices.compactMap { + Self.radix($0)?.lowercased() + } + + public static let uppercase: [Self] = Self.radices.compactMap { + Self.radix($0)?.uppercased() + } + + public static let radices: ClosedRange = 2...36 + + //=------------------------------------------------------------------------= + // MARK: Metadata + //=------------------------------------------------------------------------= + + @inlinable public static var regex: Regex<(Substring, sign: Substring?, mask: Substring?, body: Substring)> { + #/^(?\+|-)?(?&)?(?[0-9A-Za-z]+)$/# + } //=------------------------------------------------------------------------= // MARK: Utilities @@ -84,6 +111,6 @@ extension TextInt { @inlinable public static func random(using randomness: inout some Randomness) -> Self { let radix = UX.random(in: 2...36, using: &randomness) let letters = Letters(uppercase: Bool(U8.random(using: &randomness).lsb)) - return Self.radix(radix).letters(letters) + return Self(radix: radix, letters: letters)! } } diff --git a/Tests/Benchmarks/Fibonacci.swift b/Tests/Benchmarks/Fibonacci.swift index 5e29e9e5..ca4e1d7f 100644 --- a/Tests/Benchmarks/Fibonacci.swift +++ b/Tests/Benchmarks/Fibonacci.swift @@ -92,14 +92,14 @@ final class FibonacciBenchmarks: XCTestCase { func testFibonacciIXL1e6FromTextAsDecimal() throws { let text = blackHoleIdentity(Self.fib1e6r10) let format = blackHoleIdentity(TextInt.decimal) - let data = try IXL.init(text, using: format) + let data = try IXL(text, using: format).prune(Bad.error) XCTAssertEqual(data, Self.fib1e6) } func testFibonacciIXL1e6FromTextAsHexadecimal() throws { let text = blackHoleIdentity(Self.fib1e6r16) let format = blackHoleIdentity(TextInt.hexadecimal) - let data = try IXL.init(text, using: format) + let data = try IXL(text, using: format).prune(Bad.error) XCTAssertEqual(data, Self.fib1e6) } } diff --git a/Tests/Benchmarks/TextInt+Base10.swift b/Tests/Benchmarks/TextInt+Base10.swift index 89429281..5e0d47a0 100644 --- a/Tests/Benchmarks/TextInt+Base10.swift +++ b/Tests/Benchmarks/TextInt+Base10.swift @@ -24,14 +24,14 @@ final class TextIntBenchmarksOnRadix10: XCTestCase { // MARK: Metadata //=------------------------------------------------------------------------= - static let formatter = blackHoleIdentity(TextInt.decimal) + static let coder = blackHoleIdentity(TextInt.decimal) //=------------------------------------------------------------------------= // MARK: Initializers //=------------------------------------------------------------------------= override static func setUp() { - blackHole(formatter) + blackHole(coder) } //=------------------------------------------------------------------------= @@ -39,18 +39,18 @@ final class TextIntBenchmarksOnRadix10: XCTestCase { //=------------------------------------------------------------------------= func testDecodingOneMillionTimesBinaryIntegerAsUX() throws { - let encoded = blackHoleIdentity(Self.formatter.encode(UX.max)) + let encoded = blackHoleIdentity(Self.coder.encode(UX.max)) for _ in 0 as UX ..< blackHoleIdentity(1_000_000) { - precondition((try? Self.formatter.decode(encoded) as UX) != nil) + precondition(Self.coder.decode(encoded, as: UX.self) != nil) } } func testDecodingOneMillionTimesBinaryIntegerAsUXL() throws { - let encoded = blackHoleIdentity(Self.formatter.encode(UX.max)) + let encoded = blackHoleIdentity(Self.coder.encode(UX.max)) for _ in 0 as UX ..< blackHoleIdentity(1_000_000) { - precondition((try? Self.formatter.decode(encoded) as UXL) != nil) + precondition(Self.coder.decode(encoded, as: UXL.self) != nil) } } @@ -62,7 +62,7 @@ final class TextIntBenchmarksOnRadix10: XCTestCase { var counter = UX.zero for value in 0 as UX ..< blackHoleIdentity(1_000_000) { - counter += UX(Bit(!Self.formatter.encode(value).isEmpty)) + counter += UX(Bit(!Self.coder.encode(value).isEmpty)) } XCTAssertEqual(counter, blackHoleIdentity(1_000_000)) @@ -73,7 +73,7 @@ final class TextIntBenchmarksOnRadix10: XCTestCase { for _ in 0 as UX ..< blackHoleIdentity(1_000_000) { value &+= one - counter += UX(Bit(!Self.formatter.encode(value).isEmpty)) + counter += UX(Bit(!Self.coder.encode(value).isEmpty)) } XCTAssertEqual(counter, blackHoleIdentity(1_000_000)) @@ -83,7 +83,7 @@ final class TextIntBenchmarksOnRadix10: XCTestCase { var counter = UX.zero for value in 0 as UX ..< blackHoleIdentity(1_000_000) { - counter += UX(Bit(!Self.formatter.encode(sign: .plus, magnitude: value).isEmpty)) + counter += UX(Bit(!Self.coder.encode(sign: .plus, magnitude: value).isEmpty)) } XCTAssertEqual(counter, blackHoleIdentity(1_000_000)) @@ -94,7 +94,7 @@ final class TextIntBenchmarksOnRadix10: XCTestCase { for _ in 0 as UX ..< blackHoleIdentity(1_000_000) { value &+= increment - counter += UX(Bit(!Self.formatter.encode(sign: .plus, magnitude: value).isEmpty)) + counter += UX(Bit(!Self.coder.encode(sign: .plus, magnitude: value).isEmpty)) } XCTAssertEqual(counter, blackHoleIdentity(1_000_000)) diff --git a/Tests/Benchmarks/TextInt+Base16.swift b/Tests/Benchmarks/TextInt+Base16.swift index 626f39c0..24c27dc5 100644 --- a/Tests/Benchmarks/TextInt+Base16.swift +++ b/Tests/Benchmarks/TextInt+Base16.swift @@ -24,14 +24,14 @@ final class TextIntBenchmarksOnRadix16: XCTestCase { // MARK: Metadata //=------------------------------------------------------------------------= - static let formatter = blackHoleIdentity(TextInt.hexadecimal) + static let coder = blackHoleIdentity(TextInt.hexadecimal) //=------------------------------------------------------------------------= // MARK: Initializers //=------------------------------------------------------------------------= override static func setUp() { - blackHole(formatter) + blackHole(coder) } //=------------------------------------------------------------------------= @@ -39,18 +39,18 @@ final class TextIntBenchmarksOnRadix16: XCTestCase { //=------------------------------------------------------------------------= func testDecodingOneMillionTimesBinaryIntegerAsUX() throws { - let encoded = blackHoleIdentity(Self.formatter.encode(UX.max)) + let encoded = blackHoleIdentity(Self.coder.encode(UX.max)) for _ in 0 as UX ..< blackHoleIdentity(1_000_000) { - precondition((try? Self.formatter.decode(encoded) as UX) != nil) + precondition(Self.coder.decode(encoded, as: UX.self) != nil) } } func testDecodingOneMillionTimesBinaryIntegerAsUXL() throws { - let encoded = blackHoleIdentity(Self.formatter.encode(UX.max)) + let encoded = blackHoleIdentity(Self.coder.encode(UX.max)) for _ in 0 as UX ..< blackHoleIdentity(1_000_000) { - precondition((try? Self.formatter.decode(encoded) as UXL) != nil) + precondition(Self.coder.decode(encoded, as: UXL.self) != nil) } } @@ -62,7 +62,7 @@ final class TextIntBenchmarksOnRadix16: XCTestCase { var counter = UX.zero for value in 0 as UX ..< blackHoleIdentity(1_000_000) { - counter += UX(Bit(!Self.formatter.encode(value).isEmpty)) + counter += UX(Bit(!Self.coder.encode(value).isEmpty)) } XCTAssertEqual(counter, blackHoleIdentity(1_000_000)) @@ -73,7 +73,7 @@ final class TextIntBenchmarksOnRadix16: XCTestCase { for _ in 0 as UX ..< blackHoleIdentity(1_000_000) { value &+= one - counter += UX(Bit(!Self.formatter.encode(value).isEmpty)) + counter += UX(Bit(!Self.coder.encode(value).isEmpty)) } XCTAssertEqual(counter, blackHoleIdentity(1_000_000)) @@ -83,7 +83,7 @@ final class TextIntBenchmarksOnRadix16: XCTestCase { var counter = UX.zero for value in 0 as UX ..< blackHoleIdentity(1_000_000) { - counter += UX(Bit(!Self.formatter.encode(sign: .plus, magnitude: value).isEmpty)) + counter += UX(Bit(!Self.coder.encode(sign: .plus, magnitude: value).isEmpty)) } XCTAssertEqual(counter, blackHoleIdentity(1_000_000)) @@ -94,7 +94,7 @@ final class TextIntBenchmarksOnRadix16: XCTestCase { for _ in 0 as UX ..< blackHoleIdentity(1_000_000) { value &+= one - counter += UX(Bit(!Self.formatter.encode(sign: .plus, magnitude: value).isEmpty)) + counter += UX(Bit(!Self.coder.encode(sign: .plus, magnitude: value).isEmpty)) } XCTAssertEqual(counter, blackHoleIdentity(1_000_000)) diff --git a/Tests/StdlibIntKitTests/StdlibInt+Integers.swift b/Tests/StdlibIntKitTests/StdlibInt+Integers.swift index c6862039..c23ec6c1 100644 --- a/Tests/StdlibIntKitTests/StdlibInt+Integers.swift +++ b/Tests/StdlibIntKitTests/StdlibInt+Integers.swift @@ -92,7 +92,7 @@ import TestKit try #require(StdlibInt(truncatingIfNeeded: source) == destination, sourceLocation: location) try #require(StdlibInt(InfiniInt(destination)) == destination, sourceLocation: location) - if T.isSigned == StdlibInt.isSigned, destination.bitWidth <= source.bitWidth { + if T.isSigned == StdlibInt.isSigned, destination.bitWidth <= source.bitWidth { try #require(T( destination) == source, sourceLocation: location) try #require(T(exactly: destination)! == source, sourceLocation: location) try #require(T(clamping: destination) == source, sourceLocation: location) @@ -108,9 +108,9 @@ import TestKit try #require(radix10 == String(destination, radix: 10), sourceLocation: location) try #require(radix16 == String(destination, radix: 16), sourceLocation: location) - try #require( StdlibInt(radix10) == destination, sourceLocation: location) - try #require(try StdlibInt(radix10, as: .decimal) == destination, sourceLocation: location) - try #require(try StdlibInt(radix16, as: .hexadecimal) == destination, sourceLocation: location) + try #require(StdlibInt(radix10) == destination, sourceLocation: location) + try #require(StdlibInt(radix10, using: .decimal) == destination, sourceLocation: location) + try #require(StdlibInt(radix16, using: .hexadecimal) == destination, sourceLocation: location) } } } diff --git a/Tests/StdlibIntKitTests/StdlibInt+Text.swift b/Tests/StdlibIntKitTests/StdlibInt+Text.swift index 90d6eacb..1634733f 100644 --- a/Tests/StdlibIntKitTests/StdlibInt+Text.swift +++ b/Tests/StdlibIntKitTests/StdlibInt+Text.swift @@ -34,25 +34,25 @@ import TestKit //=------------------------------------------------------------------------= @Test( - "BinaryInteger/text: vs StdlibInt.Base", + "BinaryInteger/text: description of Self vs Base", Tag.List.tags(.forwarding, .random), arguments: fuzzers ) func forwarding(randomness: consuming FuzzerInt) throws { for _ in 0 ..< 128 { let value = IXL.entropic(size: 256, using: &randomness) let radix = IX .random(in: 02...36, using: &randomness) - let coder = try TextInt(radix: radix) + let coder = try #require(TextInt(radix: radix)) let lowercase = value.description(using: coder.lowercased()) let uppercase = value.description(using: coder.uppercased()) - try #require(try StdlibInt(lowercase, as: coder) == StdlibInt(value)) - try #require(try StdlibInt(uppercase, as: coder) == StdlibInt(value)) + try #require(StdlibInt(lowercase, using: coder) == StdlibInt(value)) + try #require(StdlibInt(uppercase, using: coder) == StdlibInt(value)) try #require(StdlibInt(value).description(using: coder.lowercased()) == lowercase) try #require(StdlibInt(value).description(using: coder.uppercased()) == uppercase) - try #require(String(StdlibInt(value), radix: Swift.Int(radix), uppercase: false) == lowercase) - try #require(String(StdlibInt(value), radix: Swift.Int(radix), uppercase: true ) == uppercase) + try #require(String(StdlibInt(value), radix: Swift.Int(radix), uppercase: false) == lowercase) + try #require(String(StdlibInt(value), radix: Swift.Int(radix), uppercase: true ) == uppercase) if radix == 10 { try #require(StdlibInt(lowercase) == StdlibInt(value)) diff --git a/Tests/UltimathnumTests/BinaryInteger+Division.swift.swift b/Tests/UltimathnumTests/BinaryInteger+Division.swift.swift index 6ee085f3..3347effb 100644 --- a/Tests/UltimathnumTests/BinaryInteger+Division.swift.swift +++ b/Tests/UltimathnumTests/BinaryInteger+Division.swift.swift @@ -952,34 +952,34 @@ import TestKit remainder: IXL(1) << 96, source: "https://github.com/apple/swift-numerics/issues/272" ),( - dividend: IXL("311758830729407788314878278112166161571")!, - divisor: IXL("259735543268722398904715765931073125012")!, - quotient: IXL("000000000000000000000000000000000000001")!, - remainder: IXL("052023287460685389410162512181093036559")!, + dividend: IXL(311758830729407788314878278112166161571), + divisor: IXL(259735543268722398904715765931073125012), + quotient: IXL(000000000000000000000000000000000000001), + remainder: IXL(052023287460685389410162512181093036559), source: "https://github.com/apple/swift-numerics/issues/272" ),( - dividend: IXL("213714108890282186096522258117935109183")!, - divisor: IXL("205716886996038887182342392781884393270")!, - quotient: IXL("000000000000000000000000000000000000001")!, - remainder: IXL("007997221894243298914179865336050715913")!, + dividend: IXL(213714108890282186096522258117935109183), + divisor: IXL(205716886996038887182342392781884393270), + quotient: IXL(000000000000000000000000000000000000001), + remainder: IXL(007997221894243298914179865336050715913), source: "https://github.com/apple/swift-numerics/issues/272" ),( - dividend: IXL("000000000000000000002369676578372158364766242369061213561181961479062237766620")!, - divisor: IXL("000000000000000000000000000000000000000102797312405202436815976773795958969482")!, - quotient: IXL("000000000000000000000000000000000000000000000000000000000023051931251193218442")!, - remainder: IXL("000000000000000000000000000000000000000001953953567802622125048779101000179576")!, + dividend: IXL(000000000000000000002369676578372158364766242369061213561181961479062237766620), + divisor: IXL(000000000000000000000000000000000000000102797312405202436815976773795958969482), + quotient: IXL(000000000000000000000000000000000000000000000000000000000023051931251193218442), + remainder: IXL(000000000000000000000000000000000000000001953953567802622125048779101000179576), source: "https://github.com/apple/swift-numerics/issues/272" ),( - dividend: IXL("096467201117289166187766181030232879447148862859323917044548749804018359008044")!, - divisor: IXL("000000000000000000004646260627574879223760172113656436161581617773435991717024")!, - quotient: IXL("000000000000000000000000000000000000000000000000000000000020762331011904583253")!, - remainder: IXL("000000000000000000002933245778855346947389808606934720764144871598087733608972")!, + dividend: IXL(096467201117289166187766181030232879447148862859323917044548749804018359008044), + divisor: IXL(000000000000000000004646260627574879223760172113656436161581617773435991717024), + quotient: IXL(000000000000000000000000000000000000000000000000000000000020762331011904583253), + remainder: IXL(000000000000000000002933245778855346947389808606934720764144871598087733608972), source: "https://github.com/apple/swift-numerics/issues/272" ),( - dividend: IXL("000000000000000000003360506852691063560493141264855294697309369118818719524903")!, - divisor: IXL("000000000000000000000000000000000000000038792928317726192474768301090870907748")!, - quotient: IXL("000000000000000000000000000000000000000000000000000000000086626789943967710436")!, - remainder: IXL("000000000000000000000000000000000000000016136758413064865246015978698186666775")!, + dividend: IXL(000000000000000000003360506852691063560493141264855294697309369118818719524903), + divisor: IXL(000000000000000000000000000000000000000038792928317726192474768301090870907748), + quotient: IXL(000000000000000000000000000000000000000000000000000000000086626789943967710436), + remainder: IXL(000000000000000000000000000000000000000016136758413064865246015978698186666775), source: "https://github.com/oscbyspro/Numberick/issues/101" ), ])) func nonissues( diff --git a/Tests/UltimathnumTests/BinaryInteger+Text.swift b/Tests/UltimathnumTests/BinaryInteger+Text.swift index 13c1cc79..fa56dfd2 100644 --- a/Tests/UltimathnumTests/BinaryInteger+Text.swift +++ b/Tests/UltimathnumTests/BinaryInteger+Text.swift @@ -18,223 +18,57 @@ import TestKit @Suite struct BinaryIntegerTestsOnText { - //=------------------------------------------------------------------------= - // MARK: Tests x Metadata - //=------------------------------------------------------------------------= - - static let coders: [TextInt] = (U8(2)...36).reduce(into: []) { - let coder = TextInt.radix($1) - $0.append(coder.lowercased()) - $0.append(coder.uppercased()) - } - - static var regex: Regex<(Substring, sign: Substring?, mask: Substring?, body: Substring)> { - #/^(?\+|-)?(?&)?(?[0-9A-Za-z]+)$/# - } - //=------------------------------------------------------------------------= // MARK: Tests //=------------------------------------------------------------------------= @Test( - "BinaryInteger/text: description is decodable", - Tag.List.tags(.generic, .random), - arguments: typesAsBinaryInteger, fuzzers - ) func descriptionIsDecodable( - type: any BinaryInteger.Type, randomness: consuming FuzzerInt - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: BinaryInteger { - let size = IX(size: T.self) ?? conditional(debug: 256, release: 4096) - - for _ in 0 ..< conditional(debug: 64, release: 1024) { - let coder = TextInt.random(using: &randomness) - let value = T.entropic(size: size, using: &randomness) - try whereIs(value, using: coder) - } - - for _ in 0 ..< conditional(debug: 64, release: 1024) { - let value = T.entropic(size: size, using: &randomness) - try whereIs(value, using: TextInt.decimal) - try whereIs(value, using: TextInt.hexadecimal.lowercased()) - try whereIs(value, using: TextInt.hexadecimal.uppercased()) - } - - func whereIs(_ value: T, using coder: TextInt, at location: SourceLocation = #_sourceLocation) throws { - let encoded = value.description(using: coder) - let decoded = try T(((encoded)),using: coder) - try #require(decoded == value, sourceLocation: location) - } - } - } - - @Test( - "BinaryInteger/text: description letter case is stable", - Tag.List.tags(.generic, .random), + "BinaryInteger/text: description is TextInt.decimal", + Tag.List.tags(.forwarding, .generic, .random), arguments: typesAsBinaryInteger, fuzzers - ) func descriptionLetterCaseIsStable( + ) func descriptionIsTextIntDecimal( type: any BinaryInteger.Type, randomness: consuming FuzzerInt ) throws { try whereIs(type) func whereIs(_ type: T.Type) throws where T: BinaryInteger { - let size = IX(size: T.self) ?? conditional(debug: 256, release: 4096) + let size = IX(size: T.self) ?? 256 - for _ in 0 ..< conditional(debug: 64, release: 1024) { - let coder = TextInt.random(using: &randomness) + for _ in 0 ..< conditional(debug: 32, release: 64) { let value = T.entropic(size: size, using: &randomness) + let description = TextInt.decimal.encode(value) - let lowercased: String = value.description(using: coder.lowercased()) - let uppercased: String = value.description(using: coder.uppercased()) - - if coder.radix <= 10 { - try #require(lowercased == uppercased) - } + let decoded = T(description) + let encoded = value.description - try #require(lowercased == lowercased.lowercased()) - try #require(uppercased == uppercased.uppercased()) - } - } - } - - @Test( - "BinaryInteger/text: description matches known regex", - Tag.List.tags(.generic, .random), - arguments: typesAsBinaryInteger, fuzzers - ) func descriptionMatchesKnownRegex( - type: any BinaryInteger.Type, randomness: consuming FuzzerInt - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: BinaryInteger { - for _ in 0 ..< 64 { - let regex = BinaryIntegerTestsOnText.regex - let coder = TextInt.random(using: &randomness) - let value = T.entropic(through: Shift.max(or: 255), using: &randomness) - let description = value.description(using: coder) - let match = try #require(try regex.wholeMatch(in: description)) - try #require(match.output.0 == description) - try #require(match.output.sign == (value.isNegative ? "-" : nil)) - try #require(match.output.mask == (value.isInfinite ? "&" : nil)) + try #require(decoded == value) + try #require(encoded == description) } } } @Test( - "BinaryInteger/text: description alternatives are equivalent", - Tag.List.tags(.generic, .random), + "BinaryInteger/text: description using TexInt is TextInt", + Tag.List.tags(.forwarding, .generic, .random), arguments: typesAsBinaryInteger, fuzzers - ) func descriptionAlternativesAreEquivalent( + ) func descriptionUsingTextIntIsTextInt( type: any BinaryInteger.Type, randomness: consuming FuzzerInt ) throws { try whereIs(type) func whereIs(_ type: T.Type) throws where T: BinaryInteger { - for _ in 0 ..< 64 { - let coder = TextInt.random(using: &randomness) - let value = T.entropic(through: Shift.max(or: 255), using: &randomness) - let magnitude: T.Magnitude = value.magnitude() - let description: String = value.description(using: coder) - - always: do { - let result = coder.encode(value) - try #require(result == description) - } - - if !value.isNegative { - let result = coder.encode(sign: Sign.plus, magnitude: magnitude) - try #require(result == description) - } - - if !value.isPositive { - let result = coder.encode(sign: Sign.minus, magnitude: magnitude) - try #require(result == description) - } - } - } - } - - @Test( - "BinaryInteger/text: description of negative vs positive", - Tag.List.tags(.generic, .random), - arguments: typesAsBinaryIntegerAsSigned, fuzzers - ) func descriptionOfNegativeVersusPositive( - type: any SignedInteger.Type, randomness: consuming FuzzerInt - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: SignedInteger { - for _ in 0 ..< 32 { - let negative = T.entropic(through: Shift.max(or: 255), as: Domain.natural, using: &randomness).toggled() - let positive = negative.magnitude() - - try #require(negative.isNegative) - try #require(positive.isPositive) - - for _ in 0 ..< 8 { - let radix = UX.random(in: 02...36, using: &randomness) - let uppercase = Bool.random(using: &randomness.stdlib) - let letters = TextInt.Letters(uppercase: uppercase) - let coder = try TextInt(radix: radix,letters: letters) - try #require(negative.description(using: coder) == "-\(positive.description(using: coder))") - } - } - } - } - - @Test( - "BinaryInteger/text: description of infinite vs finite", - Tag.List.tags(.generic, .random), - arguments: typesAsArbitraryIntegerAsUnsigned, fuzzers - ) func descriptionOfInfiniteVersusFinite( - type: any ArbitraryIntegerAsUnsigned.Type, randomness: consuming FuzzerInt - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: ArbitraryIntegerAsUnsigned { - for _ in 0 ..< 32 { - let (finite) = T.entropic(size: 256, as: Domain.finite, using: &randomness) - let infinite = finite.toggled() - - try #require(!(finite).isInfinite) - try #require( infinite.isInfinite) - - for _ in 0 ..< 8 { - let radix = UX.random(in: 02...36, using: &randomness) - let uppercase = Bool.random(using: &randomness.stdlib) - let letters = TextInt.Letters(uppercase: uppercase) - let coder = try TextInt(radix: radix,letters: letters) - try #require(infinite.description(using: coder) == "&\(finite.description(using: coder))") - } - } - } - } - - @Test( - "BinaryInteger/text: description of negative and infinite vs finite", - Tag.List.tags(.generic, .random), - arguments: typesAsArbitraryIntegerAsUnsigned, fuzzers - ) func descriptionOfNegativeAndInfiniteVersusFinite( - type: any ArbitraryIntegerAsUnsigned.Type, randomness: consuming FuzzerInt - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: ArbitraryIntegerAsUnsigned { - for _ in 0 ..< 32 { - let (finite) = T.entropic(size: 256, as: Domain.finite, using: &randomness) - let infinite = finite.toggled() + let size = IX(size: T.self) ?? 256 + + for _ in 0 ..< conditional(debug: 32, release: 64) { + let value = T.entropic(size: size, using: &randomness) + let coder = TextInt.all.randomElement(using: &randomness.stdlib)! + let description = coder.encode(value) - try #require(!(finite).isInfinite) - try #require( infinite.isInfinite) + let decoded = T(description, using: coder) + let encoded = value.description(using: coder) - for _ in 0 ..< 8 { - let radix = UX.random(in: 02...36, using: &randomness) - let uppercase = Bool.random(using: &randomness.stdlib) - let letters = TextInt.Letters(uppercase: uppercase) - let coder = try TextInt(radix: radix,letters: letters) - try #require(coder.encode(sign: Sign.minus, magnitude: infinite) == "-&\(finite.description(using: coder))") - } + try #require(decoded == value) + try #require(encoded == description) } } } @@ -244,452 +78,43 @@ import TestKit // MARK: * Binary Integer x Text x Validation //*============================================================================* -@Suite(.tags(.important)) struct BinaryIntegerTestsOnTextValidation { +@Suite struct BinaryIntegerTestsOnTextValidation { //=------------------------------------------------------------------------= - // MARK: Tests x Validation + // MARK: Tests //=------------------------------------------------------------------------= - - @Test( - "BinaryInteger/text/validation: throws error if no numerals", - Tag.List.tags(.generic), - arguments: typesAsBinaryInteger - ) func throwsErrorIfNoNumerals( - type: any BinaryInteger.Type - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: BinaryInteger { - let signs: [String] = ["", "+", "-"] - let masks: [String] = ["", "&"] - - for coder in BinaryIntegerTestsOnText.coders { - for sign in signs { - for mask in masks { - try #require(throws: TextInt.Error.invalid) { - try T(sign + mask, using: coder) - } - } - } - } - } - } - - @Test( - "BinaryInteger/text/validation: throws error on invalid byte", - Tag.List.tags(.generic), - arguments: typesAsBinaryInteger - ) func throwsErrorOnInvalidByte( - type: any BinaryInteger.Type - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: BinaryInteger { - var banned = Set(U8.all) - let prefix = Set("+-&".utf8.lazy.map(U8.init(_:))) - - for element in U8(UInt8(ascii: "0"))...U8(UInt8(ascii: "1")) { - banned.remove(element) - } - - try whereIs(TextInt.binary) - - for element in U8(UInt8(ascii: "2"))...U8(UInt8(ascii: "9")) { - banned.remove(element) - } - - try whereIs(TextInt.decimal) - - for element in U8(UInt8(ascii: "A"))...U8(UInt8(ascii: "F")) { - banned.remove(element) - } - - for element in U8(UInt8(ascii: "a"))...U8(UInt8(ascii: "f")) { - banned.remove(element) - } - - try whereIs(TextInt.hexadecimal) - func whereIs(_ coder: TextInt) throws { - for element in banned { - let scalar = String(UnicodeScalar(UInt8(element))) - - always: do { - try #require(throws: TextInt.Error.invalid) { - try T("0" + scalar, using: coder) - } - } - - if !prefix.contains(element) { - try #require(throws: TextInt.Error.invalid) { - try T(scalar + "0", using: coder) - } - } - - always: do { - try #require(throws: TextInt.Error.invalid) { - try T("0" + scalar + "0", using: coder) - } - } - } - } - } - } @Test( - "BinaryInteger/text/validation: decoding redundant characters", - Tag.List.tags(.generic), + "BinaryInteger/text: lossy is nil", + Tag.List.tags(.generic, .important, .random), arguments: typesAsBinaryInteger, fuzzers - ) func decodingRedundantCharacters( + ) func lossyIsNil( type: any BinaryInteger.Type, randomness: consuming FuzzerInt ) throws { - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: BinaryInteger { - let regex = BinaryIntegerTestsOnText.regex - let coders = BinaryIntegerTestsOnText.coders + try whereIs(source: IXL.self, destination: type) + try whereIs(source: UXL.self, destination: type) + func whereIs(source: A.Type, destination: B.Type) + throws where A: ArbitraryInteger, B: BinaryInteger { - for _ in 0 ..< conditional(debug: 8, release: 32) { - let coder = coders.randomElement(using: &randomness.stdlib)! - let value = T.entropic(through: Shift.max(or: 255), using: &randomness) - let description = value.description(using: coder) - let match = try #require(try regex.firstMatch(in: description)).output + for _ in 0 ..< conditional(debug: 32, release: 64) { + let value = A.entropic(size: 256, using: &randomness) + let coder = TextInt.all.randomElement(using: &randomness.stdlib)! - for _ in 0 ..< 4 { - var modified = String() - - if let sign = match.sign { - modified.append(contentsOf: sign) - } else if Bool.random(using: &randomness.stdlib) { - modified.append("+") - } - - if let mask = match.mask { - modified.append(contentsOf: mask) - } - - let zeros = IX.random(in: 0...12, using: &randomness) - modified.append(contentsOf: repeatElement("0", count: Swift.Int(zeros))) - modified.append(contentsOf: try #require(match.body)) - try #require(try T(modified, using: coder) == value) - } - } - } - } -} - -//*============================================================================* -// MARK: * Binary Integer x Text x Edge Cases -//*============================================================================* - -@Suite struct BinaryIntegerTestsOnTextEdgeCases { - - //=------------------------------------------------------------------------= - // MARK: Tests - //=------------------------------------------------------------------------= - - @Test( - "BinaryInteger/text/validation: decoding edges works", - Tag.List.tags(.generic, .important), - arguments: typesAsEdgyInteger - ) func decodingEdgesWorks( - type: any EdgyInteger.Type - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: EdgyInteger { - for coder in BinaryIntegerTestsOnText.coders { - for value in [T.min, T.max] { - let description: String = value.description(using: coder) - try #require(try T(description, using: coder) == value) - } - } - } - } - - @Test( - "BinaryInteger/text/validation: decoding one past min is error", - Tag.List.tags(.generic, .important), - arguments: typesAsEdgyInteger - ) func decodingOnePastMinIsError( - type: any EdgyInteger.Type - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: EdgyInteger { - let value = IXL(T.min).decremented() - - for coder in BinaryIntegerTestsOnText.coders { - let description = value.description(using: coder) - try #require(throws: TextInt.Error.lossy) { - try T(description, using: coder) - } - } - } - } - - @Test( - "BinaryInteger/text/validation: decoding one past max is error", - Tag.List.tags(.generic, .important), - arguments: typesAsSystemsInteger - ) func decodingOnePastMaxIsError( - type: any SystemsInteger.Type - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: SystemsInteger { - let value = IXL(T.max).incremented() - - for coder in BinaryIntegerTestsOnText.coders { - let description = value.description(using: coder) - try #require(throws: TextInt.Error.lossy) { - try T(description, using: coder) - } - } - } - } - - @Test( - "BinaryInteger/text/validation: decoding random past min is error", - Tag.List.tags(.generic, .random, .important), - arguments: typesAsEdgyInteger, fuzzers - ) func decodingOnePastMinIsError( - type: any EdgyInteger.Type, randomness: consuming FuzzerInt - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: EdgyInteger { - let base = IXL(T.min).decremented() - let coders = BinaryIntegerTestsOnText.coders - - for _ in 0 ..< 64 { - let coder = coders.randomElement(using: &randomness.stdlib)! - let natural = IXL.entropic(size: 256, as: Domain.natural, using: &randomness) - let description: String = base.minus(natural).description(using: coder) - try #require(throws: TextInt.Error.lossy) { - try T(description, using: coder) - } - } - } - } - - @Test( - "BinaryInteger/text/validation: decoding random past max is error", - Tag.List.tags(.generic, .random, .important), - arguments: typesAsSystemsInteger, fuzzers - ) func decodingOnePastMaxIsError( - type: any SystemsInteger.Type, randomness: consuming FuzzerInt - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: SystemsInteger { - let base = IXL(T.max).incremented() - let coders = BinaryIntegerTestsOnText.coders - - for _ in 0 ..< 64 { - let coder = coders.randomElement(using: &randomness.stdlib)! - let natural = IXL.entropic(size: 256, as: Domain.natural, using: &randomness) - let description: String = base.plus(natural).description(using: coder) - try #require(throws: TextInt.Error.lossy) { - try T(description, using: coder) - } - } - } - } - - @Test( - "BinaryInteger/text/validation: decoding infinite as finite is error", - Tag.List.tags(.generic, .random, .important), - arguments: typesAsFiniteInteger, fuzzers - ) func decodingInfiniteAsFiniteIsError( - type: any FiniteInteger.Type, randomness: consuming FuzzerInt - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: FiniteInteger { - let coders = BinaryIntegerTestsOnText.coders - - for _ in 0 ..< 64 { - let coder = coders.randomElement(using: &randomness.stdlib)! - let value = UXL.entropic(size: 256, as: Domain.natural, using: &randomness).toggled() - let description: String = value.description(using: coder) - try #require(value.isInfinite) - try #require(throws: TextInt.Error.lossy) { - try T(description, using: coder) - } - } - } - } -} - -//*============================================================================* -// MARK: * Binary Integer x Text x Pyramids -//*============================================================================* - -@Suite struct BinaryIntegerTestsOnTextPyramids { - - //=------------------------------------------------------------------------= - // MARK: Metadata - //=------------------------------------------------------------------------= - - static let coders: [TextInt] = Self.radices.map(TextInt.radix) - - static let radices: [UX] = conditional(debug: [10, 16], release: [UX](2...36)) - - //=------------------------------------------------------------------------= - // MARK: Tests - //=------------------------------------------------------------------------= - - /// Here we check the following sequence: - /// - /// 1 - /// 10 - /// 100 - /// 1000 - /// ..... - /// - @Test( - "BinaryInteger/text/pyramids: one followed by zeros", - Tag.List.tags(.generic, .important), - arguments: typesAsBinaryInteger - ) func pyramidOfOneFollowedByZeros( - type: any BinaryInteger.Type - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: BinaryInteger { - for coder: TextInt in Self.coders { - var encoded = String("1") - var decoded = Fallible(T(1)) - let radix = T(coder.radix) - - for _ in 0 ..< 64 { - if decoded.error { break } - try #require(encoded == decoded.value.description(using: coder)) - try #require(try decoded.value == T.init(encoded, using: coder)) - - encoded.append("0") - decoded = decoded.value.times(radix) + always: do { + let description = value.description + let lhs = B(description) + let rhs = B.exactly(value).optional() + try #require(lhs == rhs) } - } - } - } - - /// Here we check the following sequence: - /// - /// 1 - /// 12 - /// 123 - /// 1234 - /// ..... - /// - @Test( - "BinaryInteger/text/pyramids: ascending numeral cycle", - Tag.List.tags(.generic, .important), - arguments: typesAsBinaryInteger - ) func pyramidOfAscendingNumeralCycle( - type: any BinaryInteger.Type - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: BinaryInteger { - for coder: TextInt in Self.coders { - var encoded = String() - var decoded = Fallible(T( )) - let radix = T(coder.radix) - for index: U8 in 1 ... 64 { - let element = index % coder.radix - let numeral = try coder.numerals.encode(element) - - decoded = decoded.map{$0.times(T((radix)))} - decoded = decoded.map{$0.plus (T(element))} - encoded.append(String(UnicodeScalar(UInt8(numeral)))) - - if decoded.error { break } - try #require(encoded == decoded.value.description(using: coder)) - try #require(try decoded.value == T.init(encoded, using: coder)) - } - } - } - } - - /// Here we check the following sequence: - /// - /// x - /// xx - /// xxx - /// xxxx - /// ..... - /// where x is radix - 1 - /// - @Test( - "BinaryInteger/text/pyramids: repeating highest numeral", - Tag.List.tags(.generic, .important), - arguments: typesAsBinaryInteger - ) func pyramidOfRepeatingHighestNumeral( - type: any BinaryInteger.Type - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: BinaryInteger { - for coder: TextInt in Self.coders { - var encoded = String() - var decoded = Fallible(T( )) - let radix = T(coder.radix) - let value = T(coder.radix - 1) - let numeral = try coder.numerals.encode(coder.radix - 1) - - for _ in 0 ..< 64 { - decoded = decoded.map{$0.times(radix)} - decoded = decoded.map{$0.plus (value)} - encoded.append(String(UnicodeScalar(UInt8(numeral)))) - - if decoded.error { break } - try #require(encoded == decoded.value.description(using: coder)) - try #require(try decoded.value == T.init(encoded, using: coder)) + always: do { + let description = value.description(using: coder) + let lhs = B(description, using:coder) + let rhs = B.exactly(value).optional() + try #require(lhs == rhs) } } } } } - -//*============================================================================* -// MARK: * Binary Integer x Text x Conveniences -//*============================================================================* - -@Suite struct BinaryIntegerTestsOnTextConveniences { - - //=------------------------------------------------------------------------= - // MARK: Tests - //=------------------------------------------------------------------------= - - @Test( - "BinaryInteger/text/conveniences: decimal", - Tag.List.tags(.forwarding, .generic, .random), - arguments: typesAsBinaryInteger, fuzzers - ) func decimal( - type: any BinaryInteger.Type, randomness: consuming FuzzerInt - ) throws { - - try whereIs(type) - func whereIs(_ type: T.Type) throws where T: BinaryInteger { - try #require(T(String()) == nil) - - for byte: U8 in U8.min ... 47 { - try #require(T(String(UnicodeScalar(UInt8(byte)))) == nil) - } - - for byte: U8 in 58 ... U8.max { - try #require(T(String(UnicodeScalar(UInt8(byte)))) == nil) - } - - for _ in 0 ..< 32 { - let decoded = T.entropic(through: Shift.max(or: 255), using: &randomness) - let encoded = decoded.description(using: TextInt.decimal) - - try #require(decoded == T(encoded)) - try #require(encoded == decoded.description) - } - } - } -} diff --git a/Tests/UltimathnumTests/TextInt+Decoding.swift b/Tests/UltimathnumTests/TextInt+Decoding.swift new file mode 100644 index 00000000..55141938 --- /dev/null +++ b/Tests/UltimathnumTests/TextInt+Decoding.swift @@ -0,0 +1,358 @@ +//=----------------------------------------------------------------------------= +// This source file is part of the Ultimathnum open source project. +// +// Copyright (c) 2023 Oscar Byström Ericsson +// Licensed under Apache License, Version 2.0 +// +// See http://www.apache.org/licenses/LICENSE-2.0 for license information. +//=----------------------------------------------------------------------------= + +import CoreKit +import InfiniIntKit +import RandomIntKit +import TestKit + +//*============================================================================* +// MARK: * Text Int x Decoding +//*============================================================================* + +@Suite struct TextIntTestsOnDecoding { + + //=------------------------------------------------------------------------= + // MARK: Tests + //=------------------------------------------------------------------------= + + @Test( + "TextInt/decoding: description", + Tag.List.tags(.generic, .random), + arguments: typesAsBinaryInteger, fuzzers + ) func description( + type: any BinaryInteger.Type, randomness: consuming FuzzerInt + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + let size = IX(size: T.self) ?? conditional(debug: 256, release: 4096) + + for _ in 0 ..< conditional(debug: 64, release: 1024) { + let coder = TextInt.random(using: &randomness) + let value = T.entropic(size: size, using: &randomness) + try whereIs(value, using: coder) + } + + for _ in 0 ..< conditional(debug: 64, release: 1024) { + let value = T.entropic(size: size, using: &randomness) + try whereIs(value, using: TextInt.decimal) + try whereIs(value, using: TextInt.hexadecimal.lowercased()) + try whereIs(value, using: TextInt.hexadecimal.uppercased()) + } + + func whereIs(_ value: T, using coder: TextInt, at location: SourceLocation = #_sourceLocation) throws { + let encoded = value.description(using: coder) + let decoded = coder.decode(encoded,as: T.self)?.optional() + try #require(decoded == value, sourceLocation: location) + } + } + } + + @Test( + "TextInt/decoding: lossy vs exact", + Tag.List.tags(.generic, .random), + arguments: typesAsBinaryInteger, fuzzers + ) func lossyVersusExact( + type: any BinaryInteger.Type, randomness: consuming FuzzerInt + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + for _ in 0 ..< conditional(debug: 32, release: 256) { + let coder = TextInt.all.randomElement(using: &randomness.stdlib)! + let value = IXL.entropic(size: 256, using: &randomness) + let description = value.description(using: coder) + let expectation = T.exactly(value) + try #require(coder.decode(description, as: T.self) == expectation) + } + } + } +} + +//*============================================================================* +// MARK: * Text Int x Decoding x Edge Cases +//*============================================================================* + +@Suite struct TextIntTestsOnDecodingEdgeCases { + + //=------------------------------------------------------------------------= + // MARK: Tests + //=------------------------------------------------------------------------= + + @Test( + "TextInt/decoding/edge-cases: edges", + Tag.List.tags(.generic, .important), + arguments: typesAsEdgyInteger + ) func edges( + type: any EdgyInteger.Type + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: EdgyInteger { + for coder in TextInt.all { + for value in [T.min, T.max] { + let description: String = value.description(using: coder) + try #require(T(description, using: coder) == value) + } + } + } + } + + @Test( + "TextInt/decoding/edge-cases: one past min is lossy", + Tag.List.tags(.generic, .important), + arguments: typesAsEdgyInteger + ) func onePastMinIsLossy( + type: any EdgyInteger.Type + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: EdgyInteger { + let value = IXL(T.min).decremented() + + for coder in TextInt.all { + let description = value.description(using: coder) + try #require(coder.decode(description) == T.max.veto()) + } + } + } + + @Test( + "TextInt/decoding/edge-cases: one past max is lossy", + Tag.List.tags(.generic, .important), + arguments: typesAsSystemsInteger + ) func onePastMaxIsLossy( + type: any SystemsInteger.Type + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: SystemsInteger { + let value = IXL(T.max).incremented() + + for coder in TextInt.all { + let description = value.description(using: coder) + try #require(coder.decode(description) == T.min.veto()) + } + } + } + + @Test( + "TextInt/decoding/edge-cases: random past min is lossy", + Tag.List.tags(.generic, .random, .important), + arguments: typesAsEdgyInteger, fuzzers + ) func randomPastMinIsLossy( + type: any EdgyInteger.Type, randomness: consuming FuzzerInt + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: EdgyInteger { + let base = IXL(T.min).decremented() + + for _ in 0 ..< 64 { + let coder = TextInt.all.randomElement(using: &randomness.stdlib)! + let natural = IXL.entropic(size: 256, as: Domain.natural, using: &randomness) + let value = base.minus(natural) as IXL + let description: String = value.description(using: coder) + try #require(coder.decode(description) == T.exactly(value)) + } + } + } + + @Test( + "TextInt/decoding/edge-cases: random past max is lossy", + Tag.List.tags(.generic, .random, .important), + arguments: typesAsSystemsInteger, fuzzers + ) func randomPastMaxIsLossy( + type: any SystemsInteger.Type, randomness: consuming FuzzerInt + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: SystemsInteger { + let base = IXL(T.max).incremented() + + for _ in 0 ..< 64 { + let coder = TextInt.all.randomElement(using: &randomness.stdlib)! + let natural = IXL.entropic(size: 256, as: Domain.natural, using: &randomness) + let value = base.plus(natural) as IXL + let description: String = value.description(using: coder) + try #require(coder.decode(description) == T.exactly(value)) + } + } + } + + @Test( + "TextInt/decoding/edge-cases: infinite as finite is lossy", + Tag.List.tags(.generic, .random, .important), + arguments: typesAsFiniteInteger, fuzzers + ) func infiniteAsFiniteIsLossy( + type: any FiniteInteger.Type, randomness: consuming FuzzerInt + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: FiniteInteger { + for _ in 0 ..< 64 { + let coder = TextInt.all.randomElement(using: &randomness.stdlib)! + let value = UXL.entropic(size: 256, as: Domain.natural, using: &randomness).toggled() + let description: String = value.description(using: coder) + try #require(value.isInfinite) + try #require(coder.decode(description) == T.exactly(value)) + } + } + } +} + +//*============================================================================* +// MARK: * Text Int x Decoding x Validation +//*============================================================================* + +@Suite struct TextIntTestsOnDecodingValidation { + + //=------------------------------------------------------------------------= + // MARK: Tests + //=------------------------------------------------------------------------= + + @Test( + "TextInt/decoding/validation: no numerals is nil", + Tag.List.tags(.generic, .important), + arguments: typesAsBinaryInteger + ) func noNumeralsIsNil( + type: any BinaryInteger.Type + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + let signs: [String] = ["", "+", "-"] + let masks: [String] = ["", "&"] + + for coder in TextInt.all { + for sign in signs { + for mask in masks { + try #require(coder.decode(sign + mask, as: T.self) == nil) + } + } + } + } + } + + @Test( + "TextInt/decoding/validation: byte is invalid is nil", + Tag.List.tags(.generic, .important), + arguments: typesAsBinaryInteger + ) func byteIsInvalidIsNil( + type: any BinaryInteger.Type + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + var banned = Set(U8.all) + let prefix = Set("+-&".utf8.lazy.map(U8.init(_:))) + + for element in U8(UInt8(ascii: "0"))...U8(UInt8(ascii: "1")) { + banned.remove(element) + } + + try whereIs(TextInt.binary) + + for element in U8(UInt8(ascii: "2"))...U8(UInt8(ascii: "9")) { + banned.remove(element) + } + + try whereIs(TextInt.decimal) + + for element in U8(UInt8(ascii: "A"))...U8(UInt8(ascii: "F")) { + banned.remove(element) + } + + for element in U8(UInt8(ascii: "a"))...U8(UInt8(ascii: "f")) { + banned.remove(element) + } + + try whereIs(TextInt.hexadecimal) + func whereIs(_ coder: TextInt) throws { + for element in banned { + let invalid = String(UnicodeScalar(UInt8(element))) + + try #require(coder.decode("0" + invalid, as: T.self) == nil) + try #require(coder.decode("0" + invalid + "0", as: T.self) == nil) + + if !prefix.contains(element) { + try #require((coder).decode(invalid + "0", as: T.self) == nil) + } + } + } + } + } + + @Test( + "TextInt/decoding/validation: redundance is allowed", + Tag.List.tags(.generic, .important), + arguments: typesAsBinaryInteger, fuzzers + ) func redundanceIsAllowed( + type: any BinaryInteger.Type, randomness: consuming FuzzerInt + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + let regex = TextInt.regex + + for _ in 0 ..< conditional(debug: 8, release: 32) { + let coder = TextInt.all.randomElement(using: &randomness.stdlib)! + let value = T.entropic(through: Shift.max(or: 255), using: &randomness) + let description = value.description(using: coder) + let match = try #require(try regex.firstMatch(in: description)).output + + for _ in 0 ..< 4 { + var modified = String() + + if let sign = match.sign { + modified.append(contentsOf: sign) + } else if Bool.random(using: &randomness.stdlib) { + modified.append("+") + } + + if let mask = match.mask { + modified.append(contentsOf: mask) + } + + let zeros = IX.random(in: 0...12, using: &randomness) + modified.append(contentsOf: repeatElement("0", count: Swift.Int(zeros))) + modified.append(contentsOf: try #require(match.body)) + try #require(T(modified, using: coder) == value) + } + } + } + } + + @Test( + "TextInt/decoding/validation: is case-insensitive", + Tag.List.tags(.generic, .important), + arguments: typesAsBinaryInteger, fuzzers + ) func isCaseInsensitive( + type: any BinaryInteger.Type, randomness: consuming FuzzerInt + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + let size = IX(size: T.self) ?? 256 + + for _ in 0 ..< conditional(debug: 8, release: 32) { + let radix = UX.random(in: 11...36, using: &randomness) + let coder = try #require(TextInt(radix: radix)) + let value = T.entropic(size: size, using: &randomness) + + let lowercase = coder.lowercased().encode(value) + let uppercase = coder.uppercased().encode(value) + + try #require(T(lowercase, using: coder.uppercased()) == value) + try #require(T(uppercase, using: coder.lowercased()) == value) + } + } + } +} diff --git a/Tests/UltimathnumTests/TextInt+Encoding.swift b/Tests/UltimathnumTests/TextInt+Encoding.swift new file mode 100644 index 00000000..830b1eb8 --- /dev/null +++ b/Tests/UltimathnumTests/TextInt+Encoding.swift @@ -0,0 +1,190 @@ +//=----------------------------------------------------------------------------= +// This source file is part of the Ultimathnum open source project. +// +// Copyright (c) 2023 Oscar Byström Ericsson +// Licensed under Apache License, Version 2.0 +// +// See http://www.apache.org/licenses/LICENSE-2.0 for license information. +//=----------------------------------------------------------------------------= + +import CoreKit +import RandomIntKit +import TestKit + +//*============================================================================* +// MARK: * Text Int x Encoding +//*============================================================================* + +@Suite struct TextIntTestsOnEncoding { + + //=------------------------------------------------------------------------= + // MARK: Tests + //=------------------------------------------------------------------------= + + @Test( + "TextInt/encoding: description matches known regex", + Tag.List.tags(.generic, .random), + arguments: typesAsBinaryInteger, fuzzers + ) func descriptionMatchesKnownRegex( + type: any BinaryInteger.Type, randomness: consuming FuzzerInt + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + let regex = TextInt.regex + + for _ in 0 ..< 64 { + let coder = TextInt.all.randomElement(using: &randomness.stdlib)! + let value = T.entropic(through: Shift.max(or: 255), using: &randomness) + let description = value.description(using: coder) + let match = try #require(try regex.wholeMatch(in: description)) + try #require(match.output.0 == (description)) + try #require(match.output.sign == (value.isNegative ? "-" : nil)) + try #require(match.output.mask == (value.isInfinite ? "&" : nil)) + } + } + } + + @Test( + "TextInt/encoding: (-) vs (+)", + Tag.List.tags(.generic, .random), + arguments: typesAsBinaryIntegerAsSigned, fuzzers + ) func negativeVersusPositive( + type: any SignedInteger.Type, randomness: consuming FuzzerInt + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: SignedInteger { + let size = IX(size: T.self) ?? 256 + + for _ in 0 ..< 32 { + let negative = T.entropic(size: size, as: Domain.natural, using: &randomness).toggled() + let positive = negative.magnitude() + + try #require(negative.isNegative) + try #require(positive.isPositive) + + for _ in 0 ..< 8 { + let coder = TextInt.all.randomElement(using: &randomness.stdlib)! + let lhs = negative.description(using: coder) + let rhs = positive.description(using: coder) + try #require(lhs == "-\(rhs)") + } + } + } + } + + @Test( + "TextInt/encoding: (∞) vs (ℤ)", + Tag.List.tags(.generic, .random), + arguments: typesAsArbitraryIntegerAsUnsigned, fuzzers + ) func infiniteVersusFinite( + type: any ArbitraryIntegerAsUnsigned.Type, randomness: consuming FuzzerInt + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: ArbitraryIntegerAsUnsigned { + for _ in 0 ..< 32 { + let (finite) = T.entropic(size: 256, as: Domain.finite, using: &randomness) + let infinite = finite.toggled() + + try #require(!(finite).isInfinite) + try #require( infinite.isInfinite) + + for _ in 0 ..< 8 { + let coder = TextInt.all.randomElement(using: &randomness.stdlib)! + let lhs = infinite.description(using: coder) + let rhs = (finite).description(using: coder) + try #require(lhs == "&\(rhs)") + } + } + } + } + + @Test( + "TextInt/encoding: (-) & (∞) vs (ℤ)", + Tag.List.tags(.generic, .random), + arguments: typesAsArbitraryIntegerAsUnsigned, fuzzers + ) func negativeAndInfiniteVersusFinite( + type: any ArbitraryIntegerAsUnsigned.Type, randomness: consuming FuzzerInt + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: ArbitraryIntegerAsUnsigned { + for _ in 0 ..< 32 { + let (finite) = T.entropic(size: 256, as: Domain.finite, using: &randomness) + let infinite = finite.toggled() + + try #require(!(finite).isInfinite) + try #require( infinite.isInfinite) + + for _ in 0 ..< 8 { + let coder = TextInt.all.randomElement(using: &randomness.stdlib)! + let lhs = coder.encode(sign: Sign.minus, magnitude: infinite) + let rhs = coder.encode(sign: Sign.plus, magnitude: (finite)) + try #require(lhs == "-&\(rhs)") + } + } + } + } + + @Test( + "TextInt/encoding: uppercase vs lowercase", + Tag.List.tags(.generic, .random), + arguments: typesAsBinaryInteger, fuzzers + ) func uppercaseVersusLowercase( + type: any BinaryInteger.Type, randomness: consuming FuzzerInt + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + let size = IX(size: T.self) ?? conditional(debug: 256, release: 4096) + + for _ in 0 ..< conditional(debug: 64, release: 1024) { + let coder = TextInt.all.randomElement(using: &randomness.stdlib)! + let value = T.entropic(size: size, using: &randomness) + + let lowercased: String = value.description(using: coder.lowercased()) + let uppercased: String = value.description(using: coder.uppercased()) + + if coder.radix <= 10 { + try #require(lowercased == uppercased) + } + + try #require(lowercased == lowercased.lowercased()) + try #require(uppercased == uppercased.uppercased()) + } + } + } + + @Test( + "TextInt/encoding: binary integer vs sign and magnitude", + Tag.List.tags(.generic, .random), + arguments: typesAsBinaryInteger, fuzzers + ) func binaryIntegerVersusSignMagnitude( + type: any BinaryInteger.Type, randomness: consuming FuzzerInt + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + let size = IX(size: T.self) ?? 256 + + for _ in 0 ..< 64 { + let coder = TextInt.all.randomElement(using: &randomness.stdlib)! + let value = T.entropic(size: size, using: &randomness) + let magnitude: T.Magnitude = value.magnitude() + let expectation: String = coder.encode(value) + + if !value.isNegative { + let result = coder.encode(sign: Sign.plus, magnitude: magnitude) + try #require(result == expectation) + } + + if !value.isPositive { + let result = coder.encode(sign: Sign.minus, magnitude: magnitude) + try #require(result == expectation) + } + } + } + } +} diff --git a/Tests/UltimathnumTests/TextInt+Exponentiation.swift b/Tests/UltimathnumTests/TextInt+Exponentiation.swift index d892adf2..4919c74e 100644 --- a/Tests/UltimathnumTests/TextInt+Exponentiation.swift +++ b/Tests/UltimathnumTests/TextInt+Exponentiation.swift @@ -25,9 +25,7 @@ import TestKit Tag.List.tags(.exhaustive) ) func eachRadixLessThanTwoIsNil() throws { for radix: UX in 0 ..< 2 { - #expect(throws: TextInt.Error.invalid) { - try TextInt.Exponentiation(radix) - } + try #require(TextInt.Exponentiation(radix) == nil) } } @@ -38,7 +36,7 @@ import TestKit let radixLog2: UX = UX(raw: try #require(radix .ilog2())) let radixLog2Log2 = UX(raw: try #require(radixLog2.ilog2())) - let instance = try TextInt.Exponentiation(radix) + let instance = try #require(TextInt.Exponentiation(radix)) if radix == UX.lsb << (UX.lsb << radixLog2Log2) { try #require(instance.power.isZero) try #require(instance.exponent == IX(size: UX.self) >> IX(radixLog2Log2)) diff --git a/Tests/UltimathnumTests/TextInt+Letters.swift b/Tests/UltimathnumTests/TextInt+Letters.swift index 2096e125..762540d2 100644 --- a/Tests/UltimathnumTests/TextInt+Letters.swift +++ b/Tests/UltimathnumTests/TextInt+Letters.swift @@ -33,9 +33,9 @@ import TestKit ) throws { #expect(instance.start == start) - let numerals = try TextInt.Numerals(radix: 36, letters: instance) - #expect(try numerals.decode(start) == 10) - #expect(try numerals.encode(10) == start) + let numerals = try #require(TextInt.Numerals(radix: 36, letters: instance)) + #expect(numerals.decode(start) == 10) + #expect(numerals.encode(10) == start) } @Test( diff --git a/Tests/UltimathnumTests/TextInt+Numerals.swift b/Tests/UltimathnumTests/TextInt+Numerals.swift index 0f2b047d..92ff0b55 100644 --- a/Tests/UltimathnumTests/TextInt+Numerals.swift +++ b/Tests/UltimathnumTests/TextInt+Numerals.swift @@ -62,15 +62,15 @@ private let letters: [TextInt.Letters] = [.lowercase, .uppercase] for radix in radices { for letters in letters { - let numerals = try TextInt.Numerals(radix: radix, letters: letters) + let numerals = try #require( + TextInt.Numerals(radix: radix, letters: letters) + ) for key in U8.min...U8.max { if let value = expectation[key], value < radix { - try #require(try numerals.decode(key) == value) + try #require(numerals.decode(key) == value) } else { - try #require(throws: TextInt.Error.invalid) { - try numerals.decode(key) - } + try #require(numerals.decode(key) == (nil)) } } } @@ -90,16 +90,16 @@ private let letters: [TextInt.Letters] = [.lowercase, .uppercase] ) throws { for radix in radices { - let numerals = try TextInt.Numerals(radix: radix, letters: letters) + let numerals = try #require( + TextInt.Numerals(radix: radix, letters: letters) + ) for data in U8.min..(_ type: T.Type) throws where T: BinaryInteger { for radix in radices.lazy.map(T.init) { for letters in letters { - let instance = try TextInt.Numerals(radix: (radix), letters: letters) - let concrete = try TextInt.Numerals(radix: Radix(radix), letters: letters) + let instance = try #require(TextInt.Numerals(radix: (radix), letters: letters)) + let concrete = try #require(TextInt.Numerals(radix: Radix(radix), letters: letters)) try #require(instance == concrete) try #require(instance.radix == radix) @@ -185,14 +185,12 @@ private let letters: [TextInt.Letters] = [.lowercase, .uppercase] if 0 <= radix, radix <= 36 { continue } else { counter += 1 } for letters in letters { - try #require(throws: TextInt.Error.invalid) { - try TextInt.Numerals(radix: radix, letters: letters) + if let radix = T.exactly(radix).optional() { + try #require(TextInt.Numerals(radix: radix, letters: letters) == nil) } if let radix = Radix.exactly(radix).optional() { - try #require(throws: TextInt.Error.invalid) { - try TextInt.Numerals(radix: radix, letters: letters) - } + try #require(TextInt.Numerals(radix: radix, letters: letters) == nil) } } } @@ -211,15 +209,11 @@ private let letters: [TextInt.Letters] = [.lowercase, .uppercase] func whereIs(_ type: T.Type) throws where T: BinaryInteger { for letters in letters { if let radix = T.exactly(radix).optional() { - try #require(throws: TextInt.Error.invalid) { - try TextInt.Numerals(radix: radix, letters: letters) - } + try #require(TextInt.Numerals(radix: radix, letters: letters) == nil) } if let radix = Radix.exactly(radix).optional() { - try #require(throws: TextInt.Error.invalid) { - try TextInt.Numerals(radix: radix, letters: letters) - } + try #require(TextInt.Numerals(radix: radix, letters: letters) == nil) } } } diff --git a/Tests/UltimathnumTests/TextInt+Pyramids.swift b/Tests/UltimathnumTests/TextInt+Pyramids.swift new file mode 100644 index 00000000..5f832798 --- /dev/null +++ b/Tests/UltimathnumTests/TextInt+Pyramids.swift @@ -0,0 +1,149 @@ +//=----------------------------------------------------------------------------= +// This source file is part of the Ultimathnum open source project. +// +// Copyright (c) 2023 Oscar Byström Ericsson +// Licensed under Apache License, Version 2.0 +// +// See http://www.apache.org/licenses/LICENSE-2.0 for license information. +//=----------------------------------------------------------------------------= + +import CoreKit +import TestKit + +//*============================================================================* +// MARK: * Text Int x Pyramids +//*============================================================================* + +@Suite struct TextIntTestsOnPyramids { + + //=------------------------------------------------------------------------= + // MARK: Metadata + //=------------------------------------------------------------------------= + + static let coders: [TextInt] = Self.radices.map({ TextInt.radix($0)! }) + + static let radices: [UX] = conditional(debug: [10, 16], release: [UX](2...36)) + + //=------------------------------------------------------------------------= + // MARK: Tests + //=------------------------------------------------------------------------= + + /// Here we check the following sequence: + /// + /// 1 + /// 10 + /// 100 + /// 1000 + /// ..... + /// + @Test( + "TextInt/pyramids: one followed by zeros", + Tag.List.tags(.generic, .important), + arguments: typesAsBinaryInteger + ) func oneFollowedByZeros( + type: any BinaryInteger.Type + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + for coder: TextInt in Self.coders { + var encoded = String("1") + var decoded = Fallible(T(1)) + let (radix) = T(coder.radix) + + for _ in 0 ..< 64 { + try #require(decoded == coder.decode(encoded)) + + if !decoded.error { + try #require(encoded == decoded.value.description(using: coder)) + } + + encoded.append("0") + decoded = decoded.map{$0.times(radix)} + } + } + } + } + + /// Here we check the following sequence: + /// + /// 1 + /// 12 + /// 123 + /// 1234 + /// ..... + /// + @Test( + "TextInt/pyramids: ascending numeral cycle", + Tag.List.tags(.generic, .important), + arguments: typesAsBinaryInteger + ) func ascendingNumeralCycle( + type: any BinaryInteger.Type + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + for coder: TextInt in Self.coders { + var encoded = String() + var decoded = Fallible(T( )) + let radix = T(coder.radix) + + for index: U8 in 1 ... 64 { + let element = index % coder.radix + let numeral = try #require(coder.numerals.encode(element)) + + decoded = decoded.map{$0.times(T((radix)))} + decoded = decoded.map{$0.plus (T(element))} + encoded.append(String(UnicodeScalar(UInt8(numeral)))) + + try #require(decoded == coder.decode(encoded)) + + if !decoded.error { + try #require(encoded == decoded.value.description(using: coder)) + } + } + } + } + } + + /// Here we check the following sequence: + /// + /// x + /// xx + /// xxx + /// xxxx + /// ..... + /// where x is radix - 1 + /// + @Test( + "TextInt/pyramids: repeating highest numeral", + Tag.List.tags(.generic, .important), + arguments: typesAsBinaryInteger + ) func repeatingHighestNumeral( + type: any BinaryInteger.Type + ) throws { + + try whereIs(type) + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + for coder: TextInt in Self.coders { + var encoded = String() + var decoded = Fallible(T( )) + let radix = T(coder.radix) + let value = T(coder.radix - 1) + let numeral = try #require(coder.numerals.encode(coder.radix - 1)) + + for _ in 0 ..< 64 { + decoded = decoded.map{$0.times(radix)} + decoded = decoded.map{$0.plus (value)} + encoded.append(String(UnicodeScalar(UInt8(numeral)))) + + try #require(decoded == coder.decode(encoded)) + + if !decoded.error { + try #require(encoded == decoded.value.description(using: coder)) + } + } + } + } + } +} diff --git a/Tests/UltimathnumTests/TextInt+Samples.swift b/Tests/UltimathnumTests/TextInt+Samples.swift new file mode 100644 index 00000000..eb8481f5 --- /dev/null +++ b/Tests/UltimathnumTests/TextInt+Samples.swift @@ -0,0 +1,282 @@ +//=----------------------------------------------------------------------------= +// This source file is part of the Ultimathnum open source project. +// +// Copyright (c) 2023 Oscar Byström Ericsson +// Licensed under Apache License, Version 2.0 +// +// See http://www.apache.org/licenses/LICENSE-2.0 for license information. +//=----------------------------------------------------------------------------= + +import CoreKit +import DoubleIntKit +import InfiniIntKit +import TestKit + +//*============================================================================* +// MARK: * Text Int x Samples +//*============================================================================* + +@Suite(.serialized) struct TextKitTestsOnSamples { + + //=------------------------------------------------------------------------= + // MARK: Metadata + //=------------------------------------------------------------------------= + + static let coders: [TextInt] = Self.radices.map({ TextInt.radix($0)! }) + + static let radices: [UX] = conditional(debug: [10, 16], release: [UX](2...36)) + + //=------------------------------------------------------------------------= + // MARK: Tests + //=------------------------------------------------------------------------= + + @Test( + "TextInt/samples: invalid", + Tag.List.tags(.generic, .important), + arguments: Array.infer([ + + String( ), + String(" "), + String("+"), + String("-"), + String("&"), + + String(" 0"), + String(" "), + String("0 "), + String("+ "), + String("- "), + String("& "), + String(" +"), + String("0+"), + String("++"), + String("-+"), + String("&+"), + String(" -"), + String("0-"), + String("+-"), + String("--"), + String("&-"), + String(" &"), + String("0&"), + String("+&"), + String("-&"), + String("&&"), + + String(" 0 "), + String("+0 "), + String("-0 "), + String("&0 "), + String(" 0+"), + String("+0+"), + String("-0+"), + String("&0+"), + String(" 0-"), + String("+0-"), + String("-0-"), + String("&0-"), + String(" 0&"), + String("+0&"), + String("-0&"), + String("&0&"), + + ])) func invalid(description: String) throws { + for type in typesAsBinaryInteger { + try whereIs(type) + } + + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + let expectation = Optional>(nil) + for coder in Self.coders { + try #require(coder.decode(description) == expectation) + } + } + } + + @Test( + "TextInt/samples: zero", + Tag.List.tags(.generic, .important), + arguments: Array.infer([ + + String( "0"), + String( "00"), + String( "000"), + String( "0000"), + String( "00000"), + String( "000000"), + String( "0000000"), + String( "00000000"), + + String("+0"), + String("+00"), + String("+000"), + String("+0000"), + String("+00000"), + String("+000000"), + String("+0000000"), + String("+00000000"), + + String("-0"), + String("-00"), + String("-000"), + String("-0000"), + String("-00000"), + String("-000000"), + String("-0000000"), + String("-00000000"), + + ])) func zero(description: String) throws { + for destination in typesAsBinaryInteger { + try whereIs(destination) + } + + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + let expectation = Fallible(T.zero) + for coder in Self.coders { + try #require(coder.decode(description) == expectation) + } + } + } + + @Test( + "TextInt/samples: round-tripping radix 10", + Tag.List.tags(.generic, .important), + arguments: Array<(any BinaryInteger, String)>.infer([ + + (I64( U8.min) - 1, String("-1")), + (I64( U8.min), String( "0")), + + (I64( I8.min) - 1, String("-129")), + (I64( I8.min), String("-128")), + (I64( I8.max), String( "127")), + (I64( I8.max) + 1, String( "128")), + (I64( U8.max), String( "255")), + (I64( U8.max) + 1, String( "256")), + + (I64( I16.min) - 1, String("-32769")), + (I64( I16.min), String("-32768")), + (I64( I16.max), String( "32767")), + (I64( I16.max) + 1, String( "32768")), + (I64( U16.max), String( "65535")), + (I64( U16.max) + 1, String( "65536")), + + (I64( I32.min) - 1, String("-2147483649")), + (I64( I32.min), String("-2147483648")), + (I64( I32.max), String( "2147483647")), + (I64( I32.max) + 1, String( "2147483648")), + (I64( U32.max), String( "4294967295")), + (I64( U32.max) + 1, String( "4294967296")), + + (IXL( I64.min) - 1, String("-9223372036854775809")), + (IXL( I64.min), String("-9223372036854775808")), + (IXL( I64.max), String( "9223372036854775807")), + (IXL( I64.max) + 1, String( "9223372036854775808")), + (IXL( U64.max), String("18446744073709551615")), + (IXL( U64.max) + 1, String("18446744073709551616")), + + (IXL(I128.min) - 1, String("-170141183460469231731687303715884105729")), + (IXL(I128.min), String("-170141183460469231731687303715884105728")), + (IXL(I128.max), String( "170141183460469231731687303715884105727")), + (IXL(I128.max) + 1, String( "170141183460469231731687303715884105728")), + (IXL(U128.max), String( "340282366920938463463374607431768211455")), + (IXL(U128.max) + 1, String( "340282366920938463463374607431768211456")), + + (UXL.max, String("&0")), + (UXL.max - 0xf, String("&15")), + (UXL.max - 0xff, String("&255")), + (UXL.max - 0xfff, String("&4095")), + (UXL.max - 0xffff, String("&65535")), + + (IXL(-0x123456789abcdef0), String( "-1311768467463790320")), + (IXL( 0x123456789abcdef0), String( "1311768467463790320")), + (IXL(-0x5555555555555555), String( "-6148914691236517205")), + (IXL( 0x5555555555555555), String( "6148914691236517205")), + (IXL(-0xaaaaaaaaaaaaaaaa), String("-12297829382473034410")), + (IXL( 0xaaaaaaaaaaaaaaaa), String( "12297829382473034410")), + + ])) func roundtrippingRadix10(value: any BinaryInteger, description: String) throws { + let coder = TextInt.decimal + for type in typesAsBinaryInteger { + try whereIs(type) + } + + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + let expectation = T.exactly(value) + try #require(coder.decode(description) == expectation) + if let expectation = expectation.optional() { + try #require(coder.encode(expectation) == description) + } + } + } + + @Test( + "TextInt/samples: round-tripping radix 16", + Tag.List.tags(.generic, .important), + arguments: Array<(any BinaryInteger, String)>.infer([ + + (I64( U8.min) - 1, String("-1")), + (I64( U8.min), String( "0")), + + (I64( I8.min) - 1, String("-81")), + (I64( I8.min), String("-80")), + (I64( I8.max), String( "7f")), + (I64( I8.max) + 1, String( "80")), + (I64( U8.max), String( "ff")), + (I64( U8.max) + 1, String("100")), + + (I64( I16.min) - 1, String("-8001")), + (I64( I16.min), String("-8000")), + (I64( I16.max), String( "7fff")), + (I64( I16.max) + 1, String( "8000")), + (I64( U16.max), String( "ffff")), + (I64( U16.max) + 1, String("10000")), + + (I64( I32.min) - 1, String("-80000001")), + (I64( I32.min), String("-80000000")), + (I64( I32.max), String( "7fffffff")), + (I64( I32.max) + 1, String( "80000000")), + (I64( U32.max), String( "ffffffff")), + (I64( U32.max) + 1, String("100000000")), + + (IXL( I64.min) - 1, String("-8000000000000001")), + (IXL( I64.min), String("-8000000000000000")), + (IXL( I64.max), String( "7fffffffffffffff")), + (IXL( I64.max) + 1, String( "8000000000000000")), + (IXL( U64.max), String( "ffffffffffffffff")), + (IXL( U64.max) + 1, String("10000000000000000")), + + (IXL(I128.min) - 1, String("-80000000000000000000000000000001")), + (IXL(I128.min), String("-80000000000000000000000000000000")), + (IXL(I128.max), String( "7fffffffffffffffffffffffffffffff")), + (IXL(I128.max) + 1, String( "80000000000000000000000000000000")), + (IXL(U128.max), String( "ffffffffffffffffffffffffffffffff")), + (IXL(U128.max) + 1, String("100000000000000000000000000000000")), + + (UXL.max, String("&0")), + (UXL.max - 0xf, String("&f")), + (UXL.max - 0xff, String("&ff")), + (UXL.max - 0xfff, String("&fff")), + (UXL.max - 0xffff, String("&ffff")), + + (IXL(-0x123456789abcdef0), String("-123456789abcdef0")), + (IXL( 0x123456789abcdef0), String( "123456789abcdef0")), + (IXL(-0x5555555555555555), String("-5555555555555555")), + (IXL( 0x5555555555555555), String( "5555555555555555")), + (IXL(-0xaaaaaaaaaaaaaaaa), String("-aaaaaaaaaaaaaaaa")), + (IXL( 0xaaaaaaaaaaaaaaaa), String( "aaaaaaaaaaaaaaaa")), + + ])) func roundtrippingRadix16(value: any BinaryInteger, description: String) throws { + let coder = TextInt.hexadecimal + for type in typesAsBinaryInteger { + try whereIs(type) + } + + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + let expectation = T.exactly(value) + try #require(coder.decode(description) == expectation) + if let expectation = expectation.optional() { + try #require(coder.encode(expectation) == description) + } + } + } +} diff --git a/Tests/UltimathnumTests/TextInt.swift b/Tests/UltimathnumTests/TextInt.swift index 7f1da86c..c151c607 100644 --- a/Tests/UltimathnumTests/TextInt.swift +++ b/Tests/UltimathnumTests/TextInt.swift @@ -67,10 +67,10 @@ import TestKit for radix: Radix in Self.radices { let generic: some BinaryInteger = radix - try #require(try TextInt(radix: radix).letters == TextInt.Letters.lowercase) - try #require(try TextInt(radix: generic).letters == TextInt.Letters.lowercase) - try #require( TextInt.radix( radix).letters == TextInt.Letters.lowercase) - try #require( TextInt.radix( generic).letters == TextInt.Letters.lowercase) + try #require(TextInt(radix: radix)?.letters == TextInt.Letters.lowercase) + try #require(TextInt(radix: generic)?.letters == TextInt.Letters.lowercase) + try #require(TextInt.radix( radix)?.letters == TextInt.Letters.lowercase) + try #require(TextInt.radix( generic)?.letters == TextInt.Letters.lowercase) } } @@ -86,8 +86,8 @@ import TestKit func whereIs(_ type: T.Type) throws where T: BinaryInteger { for radix in Self.radices.lazy.map(T.init) { for letters in Self.letters { - let instance = try TextInt(radix: (radix), letters: letters) - let concrete = try TextInt(radix: Radix(radix), letters: letters) + let instance = try #require(TextInt(radix: (radix), letters: letters)) + let concrete = try #require(TextInt(radix: Radix(radix), letters: letters)) try #require(instance == concrete) try #require(instance.radix == radix) @@ -124,14 +124,12 @@ import TestKit if 2 <= radix, radix <= 36 { continue } else { counter += 1 } for letters in Self.letters { - try #require(throws: TextInt.Error.invalid) { - try TextInt(radix: radix, letters: letters) + if let radix = T.exactly(radix).optional() { + try #require(TextInt(radix: radix, letters: letters) == nil) } if let radix = Radix.exactly(radix).optional() { - try #require(throws: TextInt.Error.invalid) { - try TextInt(radix: radix, letters: letters) - } + try #require(TextInt(radix: radix, letters: letters) == nil) } } } @@ -150,15 +148,11 @@ import TestKit func whereIs(_ type: T.Type) throws where T: BinaryInteger { for letters in Self.letters { if let radix = T.exactly(radix).optional() { - try #require(throws: TextInt.Error.invalid) { - try TextInt(radix: radix, letters: letters) - } + try #require(TextInt(radix: radix, letters: letters) == nil) } if let radix = Radix.exactly(radix).optional() { - try #require(throws: TextInt.Error.invalid) { - try TextInt(radix: radix, letters: letters) - } + try #require(TextInt(radix: radix, letters: letters) == nil) } } } From 6163963bca51043d1a895e32798bfcfd9ef27ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20Bystr=C3=B6m=20Ericsson?= Date: Wed, 20 Nov 2024 08:13:55 +0100 Subject: [PATCH 2/2] Cleanup. --- .../Swift+Optional.swift} | 2 +- Sources/TestKit/Maybe/TextInt+Text.swift | 40 ++++++++++++++++ Sources/TestKit/Utilities+Text.swift | 32 +++---------- Tests/UltimathnumTests/TextInt+Decoding.swift | 4 +- Tests/UltimathnumTests/TextInt+Encoding.swift | 2 +- Tests/UltimathnumTests/TextInt+Samples.swift | 24 ++++++++++ Tests/UltimathnumTests/TextInt.swift | 26 ++++------ .../Utilities/Utilities+Text.swift | 48 +++++++++++++++++++ 8 files changed, 131 insertions(+), 47 deletions(-) rename Sources/TestKit/{Utilities+Optional.swift => Maybe/Swift+Optional.swift} (97%) create mode 100644 Sources/TestKit/Maybe/TextInt+Text.swift diff --git a/Sources/TestKit/Utilities+Optional.swift b/Sources/TestKit/Maybe/Swift+Optional.swift similarity index 97% rename from Sources/TestKit/Utilities+Optional.swift rename to Sources/TestKit/Maybe/Swift+Optional.swift index 6f2c90ff..160793d6 100644 --- a/Sources/TestKit/Utilities+Optional.swift +++ b/Sources/TestKit/Maybe/Swift+Optional.swift @@ -8,7 +8,7 @@ //=----------------------------------------------------------------------------= //*============================================================================* -// MARK: * Utilities x Optional +// MARK: * Swift x Optional //*============================================================================* extension Optional { diff --git a/Sources/TestKit/Maybe/TextInt+Text.swift b/Sources/TestKit/Maybe/TextInt+Text.swift new file mode 100644 index 00000000..9ed983df --- /dev/null +++ b/Sources/TestKit/Maybe/TextInt+Text.swift @@ -0,0 +1,40 @@ +//=----------------------------------------------------------------------------= +// This source file is part of the Ultimathnum open source project. +// +// Copyright (c) 2023 Oscar Byström Ericsson +// Licensed under Apache License, Version 2.0 +// +// See http://www.apache.org/licenses/LICENSE-2.0 for license information. +//=----------------------------------------------------------------------------= + +import CoreKit + +//*============================================================================* +// MARK: * Text Int x Text +//*============================================================================* + +extension TextInt: CustomStringConvertible { + + //=------------------------------------------------------------------------= + // MARK: Utilities + //=------------------------------------------------------------------------= + + @inlinable public var description: String { + self.numerals.description + } +} + +//*============================================================================* +// MARK: * Text Int x Text x Numerals +//*============================================================================* + +extension TextInt.Numerals: CustomStringConvertible { + + //=------------------------------------------------------------------------= + // MARK: Utilities + //=------------------------------------------------------------------------= + + @inlinable public var description: String { + "\(self.radix)x\(self.letters.start)" + } +} diff --git a/Sources/TestKit/Utilities+Text.swift b/Sources/TestKit/Utilities+Text.swift index 6f1c94d6..deafa89e 100644 --- a/Sources/TestKit/Utilities+Text.swift +++ b/Sources/TestKit/Utilities+Text.swift @@ -81,36 +81,16 @@ extension TextInt { // MARK: Metadata //=------------------------------------------------------------------------= - public static let all: [Self] = { - Self.lowercase + - Self.uppercase - }() - - public static let lowercase: [Self] = Self.radices.compactMap { - Self.radix($0)?.lowercased() - } - - public static let uppercase: [Self] = Self.radices.compactMap { - Self.radix($0)?.uppercased() - } - public static let radices: ClosedRange = 2...36 - //=------------------------------------------------------------------------= - // MARK: Metadata - //=------------------------------------------------------------------------= + public static let letters: [Letters] = [Letters.lowercase, Letters.uppercase] - @inlinable public static var regex: Regex<(Substring, sign: Substring?, mask: Substring?, body: Substring)> { - #/^(?\+|-)?(?&)?(?[0-9A-Za-z]+)$/# + public static let all: [Self] = Self.radices.reduce(into: []) { + $0.append(TextInt(radix: $1, letters: Letters.lowercase)!) + $0.append(TextInt(radix: $1, letters: Letters.uppercase)!) } - //=------------------------------------------------------------------------= - // MARK: Utilities - //=------------------------------------------------------------------------= - - @inlinable public static func random(using randomness: inout some Randomness) -> Self { - let radix = UX.random(in: 2...36, using: &randomness) - let letters = Letters(uppercase: Bool(U8.random(using: &randomness).lsb)) - return Self(radix: radix, letters: letters)! + @inlinable public static func regex() -> Regex<(Substring, sign: Substring?, mask: Substring?, body: Substring)> { + #/^(?\+|-)?(?&)?(?[0-9A-Za-z]+)$/# } } diff --git a/Tests/UltimathnumTests/TextInt+Decoding.swift b/Tests/UltimathnumTests/TextInt+Decoding.swift index 55141938..06af131f 100644 --- a/Tests/UltimathnumTests/TextInt+Decoding.swift +++ b/Tests/UltimathnumTests/TextInt+Decoding.swift @@ -35,7 +35,7 @@ import TestKit let size = IX(size: T.self) ?? conditional(debug: 256, release: 4096) for _ in 0 ..< conditional(debug: 64, release: 1024) { - let coder = TextInt.random(using: &randomness) + let coder = TextInt.all.randomElement(using: &randomness.stdlib)! let value = T.entropic(size: size, using: &randomness) try whereIs(value, using: coder) } @@ -300,7 +300,7 @@ import TestKit try whereIs(type) func whereIs(_ type: T.Type) throws where T: BinaryInteger { - let regex = TextInt.regex + let regex = TextInt.regex() for _ in 0 ..< conditional(debug: 8, release: 32) { let coder = TextInt.all.randomElement(using: &randomness.stdlib)! diff --git a/Tests/UltimathnumTests/TextInt+Encoding.swift b/Tests/UltimathnumTests/TextInt+Encoding.swift index 830b1eb8..4f69123f 100644 --- a/Tests/UltimathnumTests/TextInt+Encoding.swift +++ b/Tests/UltimathnumTests/TextInt+Encoding.swift @@ -31,7 +31,7 @@ import TestKit try whereIs(type) func whereIs(_ type: T.Type) throws where T: BinaryInteger { - let regex = TextInt.regex + let regex = TextInt.regex() for _ in 0 ..< 64 { let coder = TextInt.all.randomElement(using: &randomness.stdlib)! diff --git a/Tests/UltimathnumTests/TextInt+Samples.swift b/Tests/UltimathnumTests/TextInt+Samples.swift index eb8481f5..c3e45f3f 100644 --- a/Tests/UltimathnumTests/TextInt+Samples.swift +++ b/Tests/UltimathnumTests/TextInt+Samples.swift @@ -279,4 +279,28 @@ import TestKit } } } + + @Test( + "TextInt/samples: -&? as binary integer is lossy", + Tag.List.tags(.generic, .important), + arguments: Array<(Fallible, String)>.infer([ + + (Fallible(IXL(1), error: true), String("-&0")), + (Fallible(IXL(2), error: true), String("-&1")), + + ])) func negativeInfinityAsBinaryIntegerIsLossy( + result: Fallible, description: String + ) throws { + + for type in typesAsBinaryInteger { + try whereIs(type) + } + + func whereIs(_ type: T.Type) throws where T: BinaryInteger { + let expectation = result.map(T.init(load:)) + for coder in TextInt.all { + try #require(coder.decode(description) == expectation) + } + } + } } diff --git a/Tests/UltimathnumTests/TextInt.swift b/Tests/UltimathnumTests/TextInt.swift index c151c607..91268e8c 100644 --- a/Tests/UltimathnumTests/TextInt.swift +++ b/Tests/UltimathnumTests/TextInt.swift @@ -23,14 +23,6 @@ import TestKit /// typealias Radix = UX // TODO: consider IX or fancy branches - //=------------------------------------------------------------------------= - // MARK: Metadata - //=------------------------------------------------------------------------= - - static let radices: Range = 2 ..< 37 - - static let letters: [TextInt.Letters] = [.lowercase, .uppercase] - //=------------------------------------------------------------------------= // MARK: Tests //=------------------------------------------------------------------------= @@ -41,10 +33,10 @@ import TestKit ParallelizationTrait.serialized, arguments: Array<(TextInt, U8, TextInt.Letters)>.infer([ - (TextInt.binary, U8(02), TextInt.Letters.lowercase), - (TextInt.decimal, U8(10), TextInt.Letters.lowercase), - (TextInt.hexadecimal, U8(16), TextInt.Letters.lowercase), - + (TextInt.binary, U8(02), TextInt.Letters.lowercase), + (TextInt.decimal, U8(10), TextInt.Letters.lowercase), + (TextInt.hexadecimal, U8(16), TextInt.Letters.lowercase), + ])) func namedInstances( instance: TextInt, radix: U8, letters: TextInt.Letters ) throws { @@ -65,7 +57,7 @@ import TestKit ) func defaultLettersIsLowercase() throws { try #require(TextInt().letters == TextInt.Letters.lowercase) - for radix: Radix in Self.radices { + for radix: Radix in TextInt.radices { let generic: some BinaryInteger = radix try #require(TextInt(radix: radix)?.letters == TextInt.Letters.lowercase) try #require(TextInt(radix: generic)?.letters == TextInt.Letters.lowercase) @@ -84,8 +76,8 @@ import TestKit try whereIs(type) func whereIs(_ type: T.Type) throws where T: BinaryInteger { - for radix in Self.radices.lazy.map(T.init) { - for letters in Self.letters { + for radix in TextInt.radices.lazy.map(T.init) { + for letters in TextInt.letters { let instance = try #require(TextInt(radix: (radix), letters: letters)) let concrete = try #require(TextInt(radix: Radix(radix), letters: letters)) try #require(instance == concrete) @@ -123,7 +115,7 @@ import TestKit let radix = T.entropic(through: Shift.max(or: 255), using: &randomness) if 2 <= radix, radix <= 36 { continue } else { counter += 1 } - for letters in Self.letters { + for letters in TextInt.letters { if let radix = T.exactly(radix).optional() { try #require(TextInt(radix: radix, letters: letters) == nil) } @@ -146,7 +138,7 @@ import TestKit try whereIs(type) func whereIs(_ type: T.Type) throws where T: BinaryInteger { - for letters in Self.letters { + for letters in TextInt.letters { if let radix = T.exactly(radix).optional() { try #require(TextInt(radix: radix, letters: letters) == nil) } diff --git a/Tests/UltimathnumTests/Utilities/Utilities+Text.swift b/Tests/UltimathnumTests/Utilities/Utilities+Text.swift index 9a36200f..c3777de5 100644 --- a/Tests/UltimathnumTests/Utilities/Utilities+Text.swift +++ b/Tests/UltimathnumTests/Utilities/Utilities+Text.swift @@ -86,3 +86,51 @@ import TestKit } } } + +//*============================================================================* +// MARK: * Utilities x Text x Metadata +//*============================================================================* + +@Suite(.serialized) struct UtilitiesTestsOnTextMetadata { + + //=------------------------------------------------------------------------= + // MARK: Tests + //=------------------------------------------------------------------------= + + @Test( + "Utilities/text/metadata: TextInt.radices", + Tag.List.tags(.exhaustive), + arguments: CollectionOfOne(TextInt.radices) + ) func allTextIntRadices(coders: ClosedRange) { + #expect(coders.count == 35) + #expect(coders.lowerBound == 02) + #expect(coders.upperBound == 36) + } + + @Test( + "Utilities/text/metadata: TextInt.letters", + Tag.List.tags(.exhaustive), + arguments: CollectionOfOne(TextInt.letters) + ) func allTextIntLetters(coders: [TextInt.Letters]) { + #expect(coders.count == 2) + #expect(coders.contains(.lowercase)) + #expect(coders.contains(.uppercase)) + } + + @Test( + "Utilities/text/metadata: TextInt.all", + Tag.List.tags(.exhaustive), + arguments: CollectionOfOne(TextInt.all) + ) func allTextInt(coders: [TextInt]) { + + let radices = [U8](2...36) + let lowercase = coders.filter({ $0.letters == .lowercase }) + let uppercase = coders.filter({ $0.letters == .uppercase }) + + #expect(coders .count == 70) + #expect(lowercase.count == 35) + #expect(uppercase.count == 35) + #expect(lowercase.map(\.radix).sorted() == radices) + #expect(uppercase.map(\.radix).sorted() == radices) + } +}