Skip to content

Commit

Permalink
Improved inits and added static inits
Browse files Browse the repository at this point in the history
  • Loading branch information
orchetect committed Jan 30, 2021
1 parent 896e876 commit 656aa4c
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 32 deletions.
77 changes: 62 additions & 15 deletions Sources/SwiftASCII/ASCIICharacter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ public struct ASCIICharacter: Hashable {

}

@inlinable public init(_ lossy: Character) {

guard let getASCIIValue = lossy.asciiValue else {
// if ASCII encoding fails, fall back to a default character instead of throwing an exception

var translated = String(lossy).asciiStringLossy
if translated.stringValue.isEmpty { translated = "?" }

characterValue = Character(translated.stringValue)
asciiValue = characterValue.asciiValue ?? 0x3F

return
}

characterValue = lossy
asciiValue = getASCIIValue

}

@inlinable public init?(exactly source: String) {

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

}


@inlinable public init(_ lossy: String) {

let char: Character = lossy.first ?? "?"

self.init(char)

}

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

Expand Down Expand Up @@ -82,20 +108,7 @@ extension ASCIICharacter: ExpressibleByExtendedGraphemeClusterLiteral {

public init(extendedGraphemeClusterLiteral value: Character) {

guard let getASCIIValue = value.asciiValue else {
// if ASCII encoding fails, fall back to a default character instead of throwing an exception

var translated = String(value).asciiStringLossy
if translated.stringValue.isEmpty { translated = "?" }

characterValue = Character(translated.stringValue)
asciiValue = characterValue.asciiValue ?? 0x3F

return
}

characterValue = value
asciiValue = getASCIIValue
self.init(value)

}

Expand Down Expand Up @@ -156,3 +169,37 @@ extension ASCIICharacter: Equatable {
}

}

extension ASCIICharacter {

/// Convenience syntactic sugar
public static func exactly(_ source: Character) -> ASCIICharacter? {
Self(exactly: source)
}

/// Convenience syntactic sugar
public static func lossy(_ source: Character) -> ASCIICharacter {
Self(source)
}

/// Convenience syntactic sugar
public static func exactly(_ source: String) -> ASCIICharacter? {
Self(exactly: source)
}

/// Convenience syntactic sugar
public static func lossy(_ source: String) -> ASCIICharacter {
Self(source)
}

/// Convenience syntactic sugar
public static func exactly(_ source: Data) -> ASCIICharacter? {
Self(exactly: source)
}

/// Convenience syntactic sugar
public static func exactly<T: BinaryInteger>(_ value: T) -> ASCIICharacter? {
Self(value)
}

}
55 changes: 39 additions & 16 deletions Sources/SwiftASCII/ASCIIString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Foundation

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

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

}

}

extension ASCIIString: ExpressibleByStringLiteral {

public typealias StringLiteralType = String

@inlinable public init(stringLiteral: String) {
@inlinable public init(_ lossy: String) {

guard stringLiteral.allSatisfy({ $0.isASCII }),
let asciiData = stringLiteral.data(using: .ascii) else {
guard lossy.allSatisfy({ $0.isASCII }),
let asciiData = lossy.data(using: .ascii) else {

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

stringValue = stringLiteral.asciiStringLossy.stringValue
stringValue = lossy.asciiStringLossy.stringValue
rawData = stringValue.data(using: .ascii) ?? Data([])
return
}

stringValue = stringLiteral
stringValue = lossy
rawData = asciiData

}

}

extension ASCIIString: ExpressibleByStringLiteral {

public typealias StringLiteralType = String

@inlinable public init(stringLiteral: String) {

self.init(stringLiteral)

}

}

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

extension ASCIIString: LosslessStringConvertible {

public init?(_ description: String) {
self.init(exactly: description)
}
// required init already implemented above

}

extension ASCIIString {
extension ASCIIString: Equatable {

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

}

extension ASCIIString {

/// Convenience syntactic sugar
public static func exactly(_ source: String) -> ASCIIString? {
Self(exactly: source)
}

/// Convenience syntactic sugar
public static func exactly(_ source: Data) -> ASCIIString? {
Self(exactly: source)
}

/// Convenience syntactic sugar
public static func lossy(_ source: String) -> ASCIIString {
Self(source)
}

}
34 changes: 33 additions & 1 deletion Sources/SwiftASCII/String.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ import Foundation

extension String {

/// Converts a String to `ASCIIString` exactly.
/// Returns nil if `self` is not encodable as ASCII.
@inlinable public var asciiString: ASCIIString? {
ASCIIString(exactly: self)
}

/// Converts a String to `ASCIIString` lossily.
///
/// Performs a lossy conversion, transforming characters to printable ASCII substitutions where necessary.
///
/// 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"
Expand All @@ -24,7 +32,7 @@ extension String {
reverse: false)

let components =
(transformed ?? self)
(transformed ?? Self(self))
.components(separatedBy: CharacterSet.asciiPrintable.inverted)

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

}

extension Substring {

/// Converts a String to `ASCIIString` exactly.
/// Returns nil if `self` is not encodable as ASCII.
@inlinable public var asciiString: ASCIIString? {
ASCIIString(exactly: String(self))
}

/// Converts a String to `ASCIIString` lossily.
///
/// Performs a lossy conversion, transforming characters to printable ASCII substitutions where necessary.
///
/// 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"
///
/// Where a suitable character substitution can't reasonably be performed, a question-mark "?" will be substituted.
@available(OSX 10.11, iOS 9.0, *)
public var asciiStringLossy: ASCIIString {

String(self).asciiStringLossy

}

}
24 changes: 24 additions & 0 deletions Tests/SwiftASCIITests/ASCIICharacter Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,30 @@ class ASCIICharacterTests: XCTestCase {

}

func testStaticInits() {

let str: ASCIICharacter = .lossy("😃")

XCTAssertEqual(str.characterValue, Character("?"))

let _: [ASCIICharacter] = [.lossy("A"),
.lossy("A string"),
.lossy(Character("A")),
.exactly(Character("A"))!,
.exactly("A")!,
.exactly(Data([65]))!,
.exactly(65)!]

let _: [ASCIICharacter?] = [.lossy("A"),
.lossy("A string"),
.lossy(Character("A")),
.exactly(Character("A"))!,
.exactly("A")!,
.exactly(Data([65]))!,
.exactly(65)!]

}

}

#endif
29 changes: 29 additions & 0 deletions Tests/SwiftASCIITests/ASCIIString Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ class ASCIIStringTests: XCTestCase {

}

func testInit_StringVariable() {

let str1 = ""
XCTAssertEqual(ASCIIString(str1).stringValue, "" as String)

let str2 = "A string"
XCTAssertEqual(ASCIIString(str2).stringValue, "A string" as String)

let str3 = "Emöji 😃"
XCTAssertEqual(ASCIIString(str3).stringValue, "Emoji ?" as String)

}

func testInit_stringLiteral() {

// init(stringLiteral:)
Expand Down Expand Up @@ -86,6 +99,22 @@ class ASCIIStringTests: XCTestCase {

}

func testStaticInits() {

let str: ASCIIString = .lossy("Emöji 😃")

XCTAssertEqual(str.stringValue, "Emoji ?" as String)

let _: [ASCIIString] = [.lossy("A string"),
.exactly("")!,
.exactly(Data([65]))!]

let _: [ASCIIString?] = [.lossy("A string"),
.exactly("")!,
.exactly(Data([65]))!]

}

}

#endif
18 changes: 18 additions & 0 deletions Tests/SwiftASCIITests/String Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@ class StringTests: XCTestCase {
override func setUp() { super.setUp() }
override func tearDown() { super.tearDown() }

func testString_asciiString() {

// String

XCTAssertEqual("An ASCII String.".asciiString?.stringValue,
"An ASCII String.")

XCTAssertNil("Ãñ ÂŚÇÏÎ Strïńg.".asciiString)

// Substring

XCTAssertEqual(Substring("An ASCII String.").asciiString?.stringValue,
"An ASCII String.")

XCTAssertNil(Substring("Ãñ ÂŚÇÏÎ Strïńg.").asciiString)

}

func testString_asciiStringLossy() {

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

0 comments on commit 656aa4c

Please sign in to comment.