Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Sources/ASCII/StringProtocol+INCITS_4_1986.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ extension StringProtocol {
_ s: S,
to lineEnding: INCITS_4_1986.FormatEffectors.Line.Ending
) -> S {
return .init(decoding: INCITS_4_1986.normalized([UInt8](s.utf8), to: lineEnding), as: UTF8.self)
return .init(
decoding: INCITS_4_1986.normalized([UInt8](s.utf8), to: lineEnding),
as: UTF8.self
)
}
// swiftlint:enable prefer_self_in_static_references

Expand Down Expand Up @@ -215,7 +218,8 @@ extension StringProtocol {
/// let s = String(ascii: codes) // "Hello"
/// ```
@inlinable
public init<Codes: Sequence>(ascii codes: Codes) where Codes.Element == ASCII_Primitives.ASCII.Code {
public init<Codes: Sequence>(ascii codes: Codes)
where Codes.Element == ASCII_Primitives.ASCII.Code {
self.init(decoding: codes.lazy.map(\.underlying), as: UTF8.self)
}

Expand Down
94 changes: 75 additions & 19 deletions Tests/ASCII Tests/BrutalEdgeCases Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,30 +35,48 @@ struct `Brutal` {
func `exhaustive byte classification`(byte: UInt8) {
// Every byte must be either ASCII or not - no exceptions
let isASCII = byte <= 0x7F
#expect(isAllASCII([Byte(byte)]) == isASCII, "Byte 0x\(String(byte, radix: 16)) classification inconsistent")
#expect(
isAllASCII([Byte(byte)]) == isASCII,
"Byte 0x\(String(byte, radix: 16)) classification inconsistent"
)

// Predicates must be consistent for all bytes
if byte.ascii.isControl {
// Control characters are never letters/digits
#expect(!byte.ascii.isLetter, "Control byte 0x\(String(byte, radix: 16)) cannot be letter")
#expect(!byte.ascii.isDigit, "Control byte 0x\(String(byte, radix: 16)) cannot be digit")
#expect(
!byte.ascii.isLetter,
"Control byte 0x\(String(byte, radix: 16)) cannot be letter"
)
#expect(
!byte.ascii.isDigit,
"Control byte 0x\(String(byte, radix: 16)) cannot be digit"
)
}

// Visible implies printable
if byte.ascii.isVisible {
#expect(byte.ascii.isPrintable, "Visible byte 0x\(String(byte, radix: 16)) must be printable")
#expect(
byte.ascii.isPrintable,
"Visible byte 0x\(String(byte, radix: 16)) must be printable"
)
}
}

@Test(arguments: Array(UInt8(0)...UInt8(255)))
func `case conversion idempotence for every byte`(byte: UInt8) {
let upper = byte.ascii(case: .upper)
let upperAgain = upper.ascii(case: .upper)
#expect(upper == upperAgain, "Uppercase idempotence failed for 0x\(String(byte, radix: 16))")
#expect(
upper == upperAgain,
"Uppercase idempotence failed for 0x\(String(byte, radix: 16))"
)

let lower = byte.ascii(case: .lower)
let lowerAgain = lower.ascii(case: .lower)
#expect(lower == lowerAgain, "Lowercase idempotence failed for 0x\(String(byte, radix: 16))")
#expect(
lower == lowerAgain,
"Lowercase idempotence failed for 0x\(String(byte, radix: 16))"
)
}

@Test(arguments: Array(UInt8(0)...UInt8(255)))
Expand Down Expand Up @@ -88,9 +106,15 @@ struct `Brutal` {
if byte == UInt8.ascii.sp.underlying {
#expect(!isControl && isPrintable)
} else if byte <= 0x1F || byte == 0x7F {
#expect(isControl && !isPrintable, "0x\(String(byte, radix: 16)) should be control, not printable")
#expect(
isControl && !isPrintable,
"0x\(String(byte, radix: 16)) should be control, not printable"
)
} else {
#expect(!isControl && isPrintable, "0x\(String(byte, radix: 16)) should be printable, not control")
#expect(
!isControl && isPrintable,
"0x\(String(byte, radix: 16)) should be printable, not control"
)
}
}

Expand All @@ -109,14 +133,20 @@ struct `Brutal` {
@Test(arguments: Array(UInt8(0)...UInt8(127)))
func `letter implies alphanumeric`(byte: UInt8) {
if byte.ascii.isLetter {
#expect(byte.ascii.isAlphanumeric, "Letter 0x\(String(byte, radix: 16)) must be alphanumeric")
#expect(
byte.ascii.isAlphanumeric,
"Letter 0x\(String(byte, radix: 16)) must be alphanumeric"
)
}
}

@Test(arguments: Array(UInt8(0)...UInt8(127)))
func `digit implies alphanumeric`(byte: UInt8) {
if byte.ascii.isDigit {
#expect(byte.ascii.isAlphanumeric, "Digit 0x\(String(byte, radix: 16)) must be alphanumeric")
#expect(
byte.ascii.isAlphanumeric,
"Digit 0x\(String(byte, radix: 16)) must be alphanumeric"
)
}
}

Expand All @@ -126,13 +156,22 @@ struct `Brutal` {
let isLower = byte.ascii.isLowercase

// Cannot be both
#expect(!(isUpper && isLower), "Byte 0x\(String(byte, radix: 16)) cannot be both upper and lower")
#expect(
!(isUpper && isLower),
"Byte 0x\(String(byte, radix: 16)) cannot be both upper and lower"
)

// If letter, must be exactly one
if byte.ascii.isLetter {
#expect(isUpper != isLower, "Letter 0x\(String(byte, radix: 16)) must be exactly one of upper or lower")
#expect(
isUpper != isLower,
"Letter 0x\(String(byte, radix: 16)) must be exactly one of upper or lower"
)
} else {
#expect(!isUpper && !isLower, "Non-letter 0x\(String(byte, radix: 16)) cannot be upper or lower")
#expect(
!isUpper && !isLower,
"Non-letter 0x\(String(byte, radix: 16)) cannot be upper or lower"
)
}
}
}
Expand All @@ -151,7 +190,10 @@ struct `Brutal` {
]

for seq in multiByteSequences {
#expect(!isAllASCII(seq), "Multi-byte UTF-8 \(seq.map { String($0.underlying, radix: 16) }) should fail")
#expect(
!isAllASCII(seq),
"Multi-byte UTF-8 \(seq.map { String($0.underlying, radix: 16) }) should fail"
)
}
}

Expand All @@ -174,7 +216,10 @@ struct `Brutal` {
func `look-alike characters not confused with ASCII`() {
// Cyrillic 'а' looks like Latin 'a' but is U+0430 (multi-byte UTF-8)
let cyrillicA = "а" // U+0430
#expect(UInt8(ascii: Character(cyrillicA)) == nil, "Cyrillic 'а' should not convert to ASCII")
#expect(
UInt8(ascii: Character(cyrillicA)) == nil,
"Cyrillic 'а' should not convert to ASCII"
)

// Greek 'Α' looks like Latin 'A' but is U+0391
let greekA = "Α" // U+0391
Expand All @@ -184,14 +229,20 @@ struct `Brutal` {
@Test
func `zero-width characters rejected`() {
let zeroWidth = "\u{200B}" // Zero-width space
#expect(UInt8(ascii: Character(zeroWidth)) == nil, "Zero-width space should be rejected")
#expect(
UInt8(ascii: Character(zeroWidth)) == nil,
"Zero-width space should be rejected"
)
}

@Test
func `combining characters rejected`() {
// Combining diacritical marks
let combining = "\u{0301}" // Combining acute accent
#expect(UInt8(ascii: Character(combining)) == nil, "Combining character should be rejected")
#expect(
UInt8(ascii: Character(combining)) == nil,
"Combining character should be rejected"
)
}

@Test
Expand All @@ -210,15 +261,20 @@ struct `Brutal` {

@Suite
struct `Brutal - Buffer Boundaries` {
@Test(arguments: [0, 1, 2, 3, 4, 7, 8, 15, 16, 31, 32, 63, 64, 127, 128, 255, 256, 511, 512, 1023, 1024])
@Test(arguments: [
0, 1, 2, 3, 4, 7, 8, 15, 16, 31, 32, 63, 64, 127, 128, 255, 256, 511, 512, 1023, 1024,
])
func `validation at power-of-2 boundaries`(size: Int) {
let validASCII: [Byte] = Array(repeating: Byte.ascii.A, count: size)
#expect(isAllASCII(validASCII), "Valid ASCII array of size \(size) should pass")

var invalidASCII = validASCII
if size > 0 {
invalidASCII[size - 1] = 0x80
#expect(!isAllASCII(invalidASCII), "Invalid ASCII array of size \(size) should fail")
#expect(
!isAllASCII(invalidASCII),
"Invalid ASCII array of size \(size) should fail"
)
}
}

Expand Down
4 changes: 3 additions & 1 deletion Tests/ASCII Tests/Character+INCITS_4_1986 Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ struct `Character Tests` {
#expect(char.ascii.isAlphanumeric, "Character '\(char)' should be alphanumeric")
}

@Test(arguments: [" ", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "+", "="])
@Test(arguments: [
" ", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "+", "=",
])
func `special characters are not alphanumeric`(char: Character) {
#expect(!char.ascii.isAlphanumeric)
}
Expand Down
9 changes: 7 additions & 2 deletions Tests/ASCII Tests/EdgeCases Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,15 +228,20 @@ struct `Edge Cases Tests` {
@Test
func `all extended ASCII bytes invalid`() {
for value in UInt8(0x80)...UInt8(0xFF) {
#expect(!isAllASCII([Byte(value)]), "Byte 0x\(String(value, radix: 16)) should be invalid")
#expect(
!isAllASCII([Byte(value)]),
"Byte 0x\(String(value, radix: 16)) should be invalid"
)
}
}

@Test
func `all standard ASCII bytes valid`() {
// `Byte` is not Strideable per [API-BYTE-002]; iterate on UInt8
// and bridge to Byte at the lift site.
let allASCII: [Byte] = (UInt8.ascii.nul.underlying...UInt8.ascii.del.underlying).map(Byte.init)
let allASCII: [Byte] = (UInt8.ascii.nul.underlying...UInt8.ascii.del.underlying).map(
Byte.init
)
#expect(isAllASCII(allASCII))
}
}
Expand Down
13 changes: 5 additions & 8 deletions Tests/ASCII Tests/INCITS_4_1986.CaseConversion Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,10 @@ struct `Case Conversion Tests` {
struct `Case Conversion - Mathematical Properties` {
@Test
func `conversion offset is exactly 32`() {
let a = UInt8(ascii: "a")!
let A = UInt8(ascii: "A")!
#expect(a - A == 32)
#expect(a - A == INCITS_4_1986.Case.Conversion.offset)
let lowerA = UInt8(ascii: "a")!
let upperA = UInt8(ascii: "A")!
#expect(lowerA - upperA == 32)
#expect(lowerA - upperA == INCITS_4_1986.Case.Conversion.offset)
}

@Test(
Expand All @@ -155,10 +155,7 @@ struct `Case Conversion Tests` {
// Range iteration requires Strideable; `ASCII.Code` is not Strideable
// per [API-BYTE-002], so drop to `.underlying` (UInt8) for the
// a...z / A...Z enumeration.
#expect(
lower - upper == 32,

)
#expect(lower - upper == 32)
}
}
}
Expand Down
Loading
Loading