Skip to content

Commit 656aa4c

Browse files
committed
Improved inits and added static inits
1 parent 896e876 commit 656aa4c

File tree

6 files changed

+205
-32
lines changed

6 files changed

+205
-32
lines changed

Sources/SwiftASCII/ASCIICharacter.swift

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,25 @@ public struct ASCIICharacter: Hashable {
3333

3434
}
3535

36+
@inlinable public init(_ lossy: Character) {
37+
38+
guard let getASCIIValue = lossy.asciiValue else {
39+
// if ASCII encoding fails, fall back to a default character instead of throwing an exception
40+
41+
var translated = String(lossy).asciiStringLossy
42+
if translated.stringValue.isEmpty { translated = "?" }
43+
44+
characterValue = Character(translated.stringValue)
45+
asciiValue = characterValue.asciiValue ?? 0x3F
46+
47+
return
48+
}
49+
50+
characterValue = lossy
51+
asciiValue = getASCIIValue
52+
53+
}
54+
3655
@inlinable public init?(exactly source: String) {
3756

3857
guard source.count == 1,
@@ -47,7 +66,14 @@ public struct ASCIICharacter: Hashable {
4766
asciiValue = getASCIIValue
4867

4968
}
50-
69+
70+
@inlinable public init(_ lossy: String) {
71+
72+
let char: Character = lossy.first ?? "?"
73+
74+
self.init(char)
75+
76+
}
5177

5278
@inlinable public init?(exactly source: Data) {
5379

@@ -82,20 +108,7 @@ extension ASCIICharacter: ExpressibleByExtendedGraphemeClusterLiteral {
82108

83109
public init(extendedGraphemeClusterLiteral value: Character) {
84110

85-
guard let getASCIIValue = value.asciiValue else {
86-
// if ASCII encoding fails, fall back to a default character instead of throwing an exception
87-
88-
var translated = String(value).asciiStringLossy
89-
if translated.stringValue.isEmpty { translated = "?" }
90-
91-
characterValue = Character(translated.stringValue)
92-
asciiValue = characterValue.asciiValue ?? 0x3F
93-
94-
return
95-
}
96-
97-
characterValue = value
98-
asciiValue = getASCIIValue
111+
self.init(value)
99112

100113
}
101114

@@ -156,3 +169,37 @@ extension ASCIICharacter: Equatable {
156169
}
157170

158171
}
172+
173+
extension ASCIICharacter {
174+
175+
/// Convenience syntactic sugar
176+
public static func exactly(_ source: Character) -> ASCIICharacter? {
177+
Self(exactly: source)
178+
}
179+
180+
/// Convenience syntactic sugar
181+
public static func lossy(_ source: Character) -> ASCIICharacter {
182+
Self(source)
183+
}
184+
185+
/// Convenience syntactic sugar
186+
public static func exactly(_ source: String) -> ASCIICharacter? {
187+
Self(exactly: source)
188+
}
189+
190+
/// Convenience syntactic sugar
191+
public static func lossy(_ source: String) -> ASCIICharacter {
192+
Self(source)
193+
}
194+
195+
/// Convenience syntactic sugar
196+
public static func exactly(_ source: Data) -> ASCIICharacter? {
197+
Self(exactly: source)
198+
}
199+
200+
/// Convenience syntactic sugar
201+
public static func exactly<T: BinaryInteger>(_ value: T) -> ASCIICharacter? {
202+
Self(value)
203+
}
204+
205+
}

Sources/SwiftASCII/ASCIIString.swift

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import Foundation
1010

1111
/// A type containing a String instance that is guaranteed to conform to ASCII encoding.
12-
public struct ASCIIString: Equatable, Hashable {
12+
public struct ASCIIString: Hashable {
1313

1414
/// The ASCII string returned as a `String`
1515
public let stringValue: String
@@ -39,28 +39,34 @@ public struct ASCIIString: Equatable, Hashable {
3939

4040
}
4141

42-
}
43-
44-
extension ASCIIString: ExpressibleByStringLiteral {
45-
46-
public typealias StringLiteralType = String
47-
48-
@inlinable public init(stringLiteral: String) {
42+
@inlinable public init(_ lossy: String) {
4943

50-
guard stringLiteral.allSatisfy({ $0.isASCII }),
51-
let asciiData = stringLiteral.data(using: .ascii) else {
44+
guard lossy.allSatisfy({ $0.isASCII }),
45+
let asciiData = lossy.data(using: .ascii) else {
5246

5347
// if ASCII encoding fails, fall back to a default string instead of throwing an exception
5448

55-
stringValue = stringLiteral.asciiStringLossy.stringValue
49+
stringValue = lossy.asciiStringLossy.stringValue
5650
rawData = stringValue.data(using: .ascii) ?? Data([])
5751
return
5852
}
5953

60-
stringValue = stringLiteral
54+
stringValue = lossy
6155
rawData = asciiData
6256

6357
}
58+
59+
}
60+
61+
extension ASCIIString: ExpressibleByStringLiteral {
62+
63+
public typealias StringLiteralType = String
64+
65+
@inlinable public init(stringLiteral: String) {
66+
67+
self.init(stringLiteral)
68+
69+
}
6470

6571
}
6672

@@ -82,13 +88,11 @@ extension ASCIIString: CustomDebugStringConvertible {
8288

8389
extension ASCIIString: LosslessStringConvertible {
8490

85-
public init?(_ description: String) {
86-
self.init(exactly: description)
87-
}
91+
// required init already implemented above
8892

8993
}
9094

91-
extension ASCIIString {
95+
extension ASCIIString: Equatable {
9296

9397
public static func == <T: StringProtocol>(lhs: Self, rhs: T) -> Bool {
9498
lhs.stringValue == rhs
@@ -107,3 +111,22 @@ extension ASCIIString {
107111
}
108112

109113
}
114+
115+
extension ASCIIString {
116+
117+
/// Convenience syntactic sugar
118+
public static func exactly(_ source: String) -> ASCIIString? {
119+
Self(exactly: source)
120+
}
121+
122+
/// Convenience syntactic sugar
123+
public static func exactly(_ source: Data) -> ASCIIString? {
124+
Self(exactly: source)
125+
}
126+
127+
/// Convenience syntactic sugar
128+
public static func lossy(_ source: String) -> ASCIIString {
129+
Self(source)
130+
}
131+
132+
}

Sources/SwiftASCII/String.swift

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ import Foundation
1010

1111
extension String {
1212

13+
/// Converts a String to `ASCIIString` exactly.
14+
/// Returns nil if `self` is not encodable as ASCII.
15+
@inlinable public var asciiString: ASCIIString? {
16+
ASCIIString(exactly: self)
17+
}
18+
19+
/// Converts a String to `ASCIIString` lossily.
20+
///
1321
/// Performs a lossy conversion, transforming characters to printable ASCII substitutions where necessary.
1422
///
1523
/// Note that some characters may be transformed to representations that occupy more than one ASCII character. For example: char 189 (½) will be converted to "1/2"
@@ -24,7 +32,7 @@ extension String {
2432
reverse: false)
2533

2634
let components =
27-
(transformed ?? self)
35+
(transformed ?? Self(self))
2836
.components(separatedBy: CharacterSet.asciiPrintable.inverted)
2937

3038
return ASCIIString(exactly: components.joined(separator: "?"))
@@ -35,3 +43,27 @@ extension String {
3543
}
3644

3745
}
46+
47+
extension Substring {
48+
49+
/// Converts a String to `ASCIIString` exactly.
50+
/// Returns nil if `self` is not encodable as ASCII.
51+
@inlinable public var asciiString: ASCIIString? {
52+
ASCIIString(exactly: String(self))
53+
}
54+
55+
/// Converts a String to `ASCIIString` lossily.
56+
///
57+
/// Performs a lossy conversion, transforming characters to printable ASCII substitutions where necessary.
58+
///
59+
/// Note that some characters may be transformed to representations that occupy more than one ASCII character. For example: char 189 (½) will be converted to "1/2"
60+
///
61+
/// Where a suitable character substitution can't reasonably be performed, a question-mark "?" will be substituted.
62+
@available(OSX 10.11, iOS 9.0, *)
63+
public var asciiStringLossy: ASCIIString {
64+
65+
String(self).asciiStringLossy
66+
67+
}
68+
69+
}

Tests/SwiftASCIITests/ASCIICharacter Tests.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,30 @@ class ASCIICharacterTests: XCTestCase {
132132

133133
}
134134

135+
func testStaticInits() {
136+
137+
let str: ASCIICharacter = .lossy("😃")
138+
139+
XCTAssertEqual(str.characterValue, Character("?"))
140+
141+
let _: [ASCIICharacter] = [.lossy("A"),
142+
.lossy("A string"),
143+
.lossy(Character("A")),
144+
.exactly(Character("A"))!,
145+
.exactly("A")!,
146+
.exactly(Data([65]))!,
147+
.exactly(65)!]
148+
149+
let _: [ASCIICharacter?] = [.lossy("A"),
150+
.lossy("A string"),
151+
.lossy(Character("A")),
152+
.exactly(Character("A"))!,
153+
.exactly("A")!,
154+
.exactly(Data([65]))!,
155+
.exactly(65)!]
156+
157+
}
158+
135159
}
136160

137161
#endif

Tests/SwiftASCIITests/ASCIIString Tests.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ class ASCIIStringTests: XCTestCase {
4646

4747
}
4848

49+
func testInit_StringVariable() {
50+
51+
let str1 = ""
52+
XCTAssertEqual(ASCIIString(str1).stringValue, "" as String)
53+
54+
let str2 = "A string"
55+
XCTAssertEqual(ASCIIString(str2).stringValue, "A string" as String)
56+
57+
let str3 = "Emöji 😃"
58+
XCTAssertEqual(ASCIIString(str3).stringValue, "Emoji ?" as String)
59+
60+
}
61+
4962
func testInit_stringLiteral() {
5063

5164
// init(stringLiteral:)
@@ -86,6 +99,22 @@ class ASCIIStringTests: XCTestCase {
8699

87100
}
88101

102+
func testStaticInits() {
103+
104+
let str: ASCIIString = .lossy("Emöji 😃")
105+
106+
XCTAssertEqual(str.stringValue, "Emoji ?" as String)
107+
108+
let _: [ASCIIString] = [.lossy("A string"),
109+
.exactly("")!,
110+
.exactly(Data([65]))!]
111+
112+
let _: [ASCIIString?] = [.lossy("A string"),
113+
.exactly("")!,
114+
.exactly(Data([65]))!]
115+
116+
}
117+
89118
}
90119

91120
#endif

Tests/SwiftASCIITests/String Tests.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,24 @@ class StringTests: XCTestCase {
1616
override func setUp() { super.setUp() }
1717
override func tearDown() { super.tearDown() }
1818

19+
func testString_asciiString() {
20+
21+
// String
22+
23+
XCTAssertEqual("An ASCII String.".asciiString?.stringValue,
24+
"An ASCII String.")
25+
26+
XCTAssertNil("Ãñ ÂŚÇÏÎ Strïńg.".asciiString)
27+
28+
// Substring
29+
30+
XCTAssertEqual(Substring("An ASCII String.").asciiString?.stringValue,
31+
"An ASCII String.")
32+
33+
XCTAssertNil(Substring("Ãñ ÂŚÇÏÎ Strïńg.").asciiString)
34+
35+
}
36+
1937
func testString_asciiStringLossy() {
2038

2139
// printable ASCII chars - ensure they are kept intact and not translated

0 commit comments

Comments
 (0)