Repository: Flight-School/RegularExpressionDecoder Branch: master Commit: 38a13abea850 Files: 18 Total size: 28.3 KB Directory structure: gitextract_sq6cel8s/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── ci.yml │ └── documentation.yml ├── .gitignore ├── LICENSE.md ├── Package.swift ├── README.md ├── RegularExpressionDecoder.playground/ │ ├── Contents.swift │ ├── Sources/ │ │ └── Stock.swift │ └── contents.xcplayground ├── RegularExpressionDecoder.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ └── IDEWorkspaceChecks.plist ├── Sources/ │ └── RegularExpressionDecoder/ │ ├── KeyedDecodingContainer.swift │ ├── RegularExpressionDecoder.swift │ ├── RegularExpressionPattern.swift │ ├── SingleValueDecodingContainer.swift │ └── UnkeyedDecodingContainer.swift └── Tests/ └── RegularExpressionDecoderTests/ └── RegularExpressionDecodingTests.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: [mattt] custom: https://flight.school/books/strings ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [master] pull_request: branches: [master] jobs: macos: runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v1 - name: Build and Test run: swift test linux: runs-on: ubuntu-latest strategy: matrix: swift: ["5.1", "5.2", "latest"] container: image: swift:${{ matrix.swift }} steps: - name: Checkout uses: actions/checkout@v1 - name: Build and Test run: swift test --enable-test-discovery ================================================ FILE: .github/workflows/documentation.yml ================================================ name: Documentation on: push: branches: - master paths: - .github/workflows/documentation.yml - Sources/**.swift jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v1 - name: Generate Documentation uses: SwiftDocOrg/swift-doc@master with: inputs: "Sources/SwiftDoc" output: "Documentation" - name: Upload Documentation to Wiki uses: SwiftDocOrg/github-wiki-publish-action@v1 with: path: "Documentation" env: GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} ================================================ FILE: .gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj ================================================ FILE: LICENSE.md ================================================ Copyright 2019 Read Evaluate Press, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "RegularExpressionDecoder", products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "RegularExpressionDecoder", targets: ["RegularExpressionDecoder"]), ], dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "RegularExpressionDecoder", dependencies: [], path: "Sources" ), .testTarget( name: "RegularExpressionDecoderTests", dependencies: ["RegularExpressionDecoder"], path: "Tests" ), ] ) ================================================ FILE: README.md ================================================ # Regular Expression Decoder [![Build Status][build status badge]][build status] [![License][license badge]][license] [![Swift Version][swift version badge]][swift version] A decoder that constructs objects from regular expression matches. --- For more information about creating your own custom decoders, consult Chapter 7 of the [Flight School Guide to Swift Codable](https://flight.school/books/codable). For more information about using regular expressions in Swift, check out Chapter 6 of the [Flight School Guide to Swift Strings](https://flight.school/books/strings). ## Requirements - Swift 5+ - iOS 11+ or macOS 10.13+ ## Usage ```swift import RegularExpressionDecoder let ticker = """ AAPL 170.69▲0.51 GOOG 1122.57▲2.41 AMZN 1621.48▼18.52 MSFT 106.57=0.00 SWIFT 5.0.0▲1.0.0 """ let pattern: RegularExpressionPattern = #""" \b (?<\#(.symbol)>[A-Z]{1,4}) \s+ (?<\#(.price)>\d{1,}\.\d{2}) \s* (?<\#(.sign)>([▲▼=]) (?<\#(.change)>\d{1,}\.\d{2}) \b """# let decoder = try RegularExpressionDecoder( pattern: pattern, options: .allowCommentsAndWhitespace ) try decoder.decode([Stock].self, from: ticker) // Decodes [AAPL, GOOG, AMZN, MSFT] (but not SWIFT, which is invalid) ``` ## Explanation Let's say that you're building an app that parses stock quotes from a text-based stream of price changes. ```swift let ticker = """ AAPL 170.69▲0.51 GOOG 1122.57▲2.41 AMZN 1621.48▼18.52 MSFT 106.57=0.00 """ ``` Each stock is represented by the following structure: - The **symbol**, consisting of 1 to 4 uppercase letters, followed by a space - The **price**, formatted as a number with 2 decimal places - A **sign**, indicating a price gain (`▲`), loss (`▼`), or no change (`=`) - The **magnitude** of the gain or loss, formatted the same as the price These format constraints lend themselves naturally to representation by a regular expression, such as: ```perl /\b[A-Z]{1,4} \d{1,}\.\d{2}[▲▼=]\d{1,}\.\d{2}\b/ ``` > Note: > The `\b` metacharacter anchors matches to word boundaries. This regular expression can distinguish between valid and invalid stock quotes. ```swift "AAPL 170.69▲0.51" // valid "SWIFT 5.0.0▲1.0.0" // invalid ``` However, to extract individual components from a quote (symbol, price, etc.) the regular expression must contain capture groups, of which there are two varieties: positional capture groups and named capture groups. Positional capture groups are demarcated in the pattern by enclosing parentheses (`(___)`). With some slight modifications, we can make original regular expression capture each part of the stock quote: ```perl /\b([A-Z]{1,4}) (\d{1,}\.\d{2})([▲▼=])(\d{1,}\.\d{2})\b/ ``` When matched, the symbol can be accessed by the first capture group, the price by the second, and so on. For large numbers of capture groups --- especially in patterns with nested groups --- one can easily lose track of which parts correspond to which positions. So another approach is to assign names to capture groups, which are denoted by the syntax `(?___)`. ```perl /\b (?[A-Z]{1,4}) \s+ (?\d{1,}\.\d{2}) \s* (?([▲▼=]) (?\d{1,}\.\d{2}) \b/ ``` > Note: > Most regular expression engines --- > including the one used by `NSRegularExpression` --- > provide a mode to ignore whitespace; > this lets you segment long patterns over multiple lines, > making them easier to read and understand. Theoretically, this approach allows you to access each group by name for each match of the regular expression. In practice, doing this in Swift can be inconvenient, as it requires you to interact with cumbersome `NSRegularExpression` APIs and somehow incorporate it into your model layer. `RegularExpressionDecoder` provides a convenient solution to constructing `Decodable` objects from regular expression matches by automatically matching coding keys to capture group names. And it can do so safely, thanks to the new `ExpressibleByStringInterpolation` protocol in Swift 5. To understand how, let's start by considering the following `Stock` model, which adopts the `Decodable` protocol: ```swift struct Stock: Decodable { let symbol: String var price: Double enum Sign: String, Decodable { case gain = "▲" case unchanged = "=" case loss = "▼" } private var sign: Sign private var change: Double = 0.0 var movement: Double { switch sign { case .gain: return +change case .unchanged: return 0.0 case .loss: return -change } } } ``` So far, so good. Now, normally, the Swift compiler automatically synthesizes conformance to `Decodable`, including a nested `CodingKeys` type. But in order to make this next part work correctly, we'll have to do this ourselves: ```swift extension Stock { enum CodingKeys: String, CodingKey { case symbol case price case sign case change } } ``` Here's where things get really interesting: remember our regular expression with named capture patterns from before? _We can replace the hard-coded names with interpolations of the `Stock` type's coding keys._ ```swift import RegularExpressionDecoder let pattern: RegularExpressionPattern = #""" \b (?<\#(.symbol)>[A-Z]{1,4}) \s+ (?<\#(.price)>\d{1,}\.\d{2}) \s* (?<\#(.sign)>[▲▼=]) (?<\#(.change)>\d{1,}\.\d{2}) \b """# ``` > Note: > This example benefits greatly from another new feature in Swift 5: > raw string literals. > Those octothorps (`#`) at the start and end > tell the compiler to ignore escape characters (`\`) > unless they also include an octothorp (`\#( )`). > Using raw string literals, > we can write regular expression metacharacters like `\b`, `\d`, and `\s` > without double escaping them (i.e. `\\b`). Thanks to `ExpressibleByStringInterpolation`, we can restrict interpolation segments to only accept those coding keys, thereby ensuring a direct 1:1 match between capture groups and their decoded properties. And not only that --- this approach lets us to verify that keys have valid regex-friendly names and aren't captured more than once. It's enormously powerful, allowing code to be incredibly expressive without compromising safety or performance. When all is said and done, `RegularExpressionDecoder` lets you decode types from a string according to a regular expression pattern much the same as you might from JSON or a property list using a decoder: ```swift let decoder = try RegularExpressionDecoder( pattern: pattern, options: .allowCommentsAndWhitespace ) try decoder.decode([Stock].self, from: ticker) // Decodes [AAPL, GOOG, AMZN, MSFT] ``` ## License MIT ## Contact Mattt ([@mattt](https://twitter.com/mattt)) [build status]: https://github.com/Flight-School/RegularExpressionDecoder/actions?query=workflow%3ACI [build status badge]: https://github.com/Flight-School/RegularExpressionDecoder/workflows/CI/badge.svg [license]: http://img.shields.io/badge/license-MIT-blue.svg?style=flat [license badge]: http://img.shields.io/badge/license-MIT-blue.svg?style=flat [swift version]: https://swift.org/download/ [swift version badge]: http://img.shields.io/badge/swift%20version-5.0-orange.svg?style=flat ================================================ FILE: RegularExpressionDecoder.playground/Contents.swift ================================================ import RegularExpressionDecoder let ticker = """ AAPL 170.69▲0.51 GOOG 1122.57▲2.41 AMZN 1621.48▼18.52 MSFT 106.57=0.00 SWIFT 5.0.0▲1.0.0 """ let pattern: RegularExpressionPattern = #""" \b (?<\#(.symbol)>[A-Z]{1,4}) \s+ (?<\#(.price)>\d{1,}\.\d{2}) \s* (?<\#(.sign)>([▲▼](?!0\.00))|(=(?=0\.00))) (?<\#(.change)>\d{1,}\.\d{2}) \b """# let decoder = try RegularExpressionDecoder(pattern: pattern, options: .allowCommentsAndWhitespace) try decoder.decode([Stock].self, from: ticker) ================================================ FILE: RegularExpressionDecoder.playground/Sources/Stock.swift ================================================ public struct Stock: Decodable { public let symbol: String public var price: Double enum Sign: String, Decodable { case gain = "▲" case unchanged = "=" case loss = "▼" } private var sign: Sign private var change: Double = 0.0 public var movement: Double { switch sign { case .gain: return +change case .unchanged: return 0.0 case .loss: return -change } } public enum CodingKeys: String, CodingKey { case symbol case price case sign case change } } extension Stock.Sign: CustomStringConvertible { var description: String { return self.rawValue } } extension Stock: CustomStringConvertible { public var description: String { return "\(self.symbol) \(self.price)\(self.sign)\(self.change)" } } ================================================ FILE: RegularExpressionDecoder.playground/contents.xcplayground ================================================ ================================================ FILE: RegularExpressionDecoder.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: RegularExpressionDecoder.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Sources/RegularExpressionDecoder/KeyedDecodingContainer.swift ================================================ import Foundation @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) extension NSTextCheckingResult { func range(for key: Key) -> NSRange? where Key: CodingKey { if let position = key.intValue { return self.range(at: position) } else { return self.range(withName: key.stringValue) } } } @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) extension _RegularExpressionDecoder { final class KeyedContainer where Key: CodingKey { let string: String let match: NSTextCheckingResult? var codingPath: [CodingKey] var userInfo: [CodingUserInfoKey: Any] init(string: String, match: NSTextCheckingResult?, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) { self.string = string self.match = match self.codingPath = codingPath self.userInfo = userInfo } func range(for key: Key) -> Range? { guard let nsrange = self.match?.range(for: key), nsrange.location != NSNotFound else { return nil } return Range(nsrange, in: self.string) } func string(for key: Key) -> String? { guard let range = self.range(for: key) else { return nil } return String(self.string[range]) } } } @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) extension _RegularExpressionDecoder.KeyedContainer: KeyedDecodingContainerProtocol { var allKeys: [Key] { guard let captureGroupNames = self.userInfo[._captureGroupNames] as? [String] else { return [] } return captureGroupNames.compactMap { Key(stringValue: $0) } } func contains(_ key: Key) -> Bool { return self.range(for: key) != nil } func decodeNil(forKey key: Key) throws -> Bool { return self.match == nil || !contains(key) } func decode(_ type: String.Type, forKey key: Key) throws -> String { guard let string = self.string(for: key) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "String: \(self.string)") throw DecodingError.keyNotFound(key, context) } return string } func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable, T: LosslessStringConvertible { let string = try self.decode(String.self, forKey: key) guard let value = T(string) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "String: \(self.string)") throw DecodingError.typeMismatch(type, context) } return value } func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable { let string = try self.decode(String.self, forKey: key) let decoder = _RegularExpressionDecoder(string: string, matches: [self.match].compactMap {$0}) let value = try T(from: decoder) return value } func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { fatalError("Unimplemented") } func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { fatalError("Unimplemented") } func superDecoder() throws -> Decoder { fatalError("Unimplemented") } func superDecoder(forKey key: Key) throws -> Decoder { fatalError("Unimplemented") } } @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) extension _RegularExpressionDecoder.KeyedContainer: RegularExpressionDecodingContainer {} ================================================ FILE: Sources/RegularExpressionDecoder/RegularExpressionDecoder.swift ================================================ import Foundation /** */ @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) final public class RegularExpressionDecoder { private(set) var regularExpression: NSRegularExpression var captureGroupNames: [String]? public enum Error: Swift.Error { case noMatches case tooManyMatches } public init(pattern: RegularExpressionPattern, options: NSRegularExpression.Options = []) throws { self.regularExpression = try NSRegularExpression(pattern: pattern.description, options: options) self.captureGroupNames = pattern.captures?.map { $0.stringValue } } public func decode(_ type: T.Type, from string: String, options: NSRegularExpression.MatchingOptions = []) throws -> T { let range = NSRange(string.startIndex.. [T] { let range = NSRange(string.startIndex.. SingleValueDecodingContainer { assertCanCreateContainer() let container = SingleValueContainer(string: self.string, match: self.matches.first, codingPath: self.codingPath, userInfo: self.userInfo) self.container = container return container } func container(keyedBy type: Key.Type) -> KeyedDecodingContainer where Key : CodingKey { assertCanCreateContainer() let container = KeyedContainer(string: self.string, match: self.matches.first, codingPath: self.codingPath, userInfo: self.userInfo) self.container = container return KeyedDecodingContainer(container) } func unkeyedContainer() -> UnkeyedDecodingContainer { assertCanCreateContainer() let container = UnkeyedContainer(string: self.string, matches: self.matches, codingPath: self.codingPath, userInfo: self.userInfo) self.container = container return container } } @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) protocol RegularExpressionDecodingContainer: class {} ================================================ FILE: Sources/RegularExpressionDecoder/RegularExpressionPattern.swift ================================================ public struct RegularExpressionPattern: LosslessStringConvertible, ExpressibleByStringInterpolation where T: Decodable, CodingKeys: CodingKey & Hashable { public var description: String public var captures: Set? public init?(_ description: String) { self.description = description } public init(stringLiteral value: String) { self.init(value)! } public init(stringInterpolation: StringInterpolation) { self.init(stringInterpolation.string)! self.captures = stringInterpolation.captures } public struct StringInterpolation: StringInterpolationProtocol { var string: String = "" var captures: Set = [] public init(literalCapacity: Int, interpolationCount: Int) { self.string.reserveCapacity(literalCapacity) } public mutating func appendLiteral(_ literal: String) { self.string.append(literal) } public mutating func appendInterpolation(_ key: CodingKeys) { precondition(!self.captures.contains(key), "\(key) already captured") precondition(!key.stringValue.contains { !$0.isLetter }, "invalid capture name \(key.stringValue)") self.string.append(key.stringValue) self.captures.insert(key) } } } ================================================ FILE: Sources/RegularExpressionDecoder/SingleValueDecodingContainer.swift ================================================ import Foundation @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) extension _RegularExpressionDecoder { final class SingleValueContainer { let string: String let match: NSTextCheckingResult? var codingPath: [CodingKey] var userInfo: [CodingUserInfoKey: Any] init(string: String, match: NSTextCheckingResult?, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) { self.string = string self.match = match self.codingPath = codingPath self.userInfo = userInfo } } } @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) extension _RegularExpressionDecoder.SingleValueContainer: SingleValueDecodingContainer { func decodeNil() -> Bool { return self.match == nil } func decode(_ type: T.Type) throws -> T where T : Decodable { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot decode type \(type)") throw DecodingError.typeMismatch(type, context) } func decode(_ type: T.Type) throws -> T where T: Decodable, T: LosslessStringConvertible { guard let value = T(self.string) else { let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot decode type \(type)") throw DecodingError.typeMismatch(type, context) } return value } } @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) extension _RegularExpressionDecoder.SingleValueContainer: RegularExpressionDecodingContainer {} ================================================ FILE: Sources/RegularExpressionDecoder/UnkeyedDecodingContainer.swift ================================================ import Foundation @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) extension _RegularExpressionDecoder { final class UnkeyedContainer { let string: String let matches: [NSTextCheckingResult] var codingPath: [CodingKey] var userInfo: [CodingUserInfoKey: Any] var currentIndex: Int = 0 init(string: String, matches: [NSTextCheckingResult], codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) { self.string = string self.matches = matches self.codingPath = codingPath self.userInfo = userInfo } } } @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) extension _RegularExpressionDecoder.UnkeyedContainer: UnkeyedDecodingContainer { var count: Int? { return self.matches.count } var isAtEnd: Bool { return self.currentIndex >= self.count ?? 0 } func decodeNil() throws -> Bool { return !isAtEnd } func decode(_ type: T.Type) throws -> T where T : Decodable { guard !isAtEnd else { throw DecodingError.dataCorruptedError(in: self, debugDescription: "no more matches") } defer { self.currentIndex += 1 } let decoder = _RegularExpressionDecoder(string: self.string, matches: [self.matches[self.currentIndex]]) return try T(from: decoder) } func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { defer { self.currentIndex += 1 } return _RegularExpressionDecoder.UnkeyedContainer(string: self.string, matches: [self.matches[self.currentIndex]], codingPath: self.codingPath, userInfo: self.userInfo) } func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey { defer { self.currentIndex += 1 } let container = _RegularExpressionDecoder.KeyedContainer(string: self.string, match: self.matches[self.currentIndex], codingPath: self.codingPath, userInfo: self.userInfo) return KeyedDecodingContainer(container) } func superDecoder() throws -> Decoder { fatalError("Unimplemented") } } @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) extension _RegularExpressionDecoder.UnkeyedContainer: RegularExpressionDecodingContainer {} ================================================ FILE: Tests/RegularExpressionDecoderTests/RegularExpressionDecodingTests.swift ================================================ import XCTest import Foundation @testable import RegularExpressionDecoder @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) struct Stock: Decodable { let symbol: String var price: Double enum Sign: String, Decodable { case gain = "▲" case unchanged = "=" case loss = "▼" } private var sign: Sign private var change: Double = 0.0 var movement: Double { switch sign { case .gain: return +change case .unchanged: return 0.0 case .loss: return -change } } enum CodingKeys: String, CodingKey { case symbol case price case sign case change } } // swiftlint:disable force_try @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) class RegularExpressionDecodingTests: XCTestCase { var decoder: RegularExpressionDecoder! override func setUp() { let pattern: RegularExpressionPattern = #""" \b (?<\#(.symbol)>[A-Z]{1,4}) \s+ (?<\#(.price)>\d{1,}\.\d{2}) \s* (?<\#(.sign)>([▲▼](?!0\.00))|(=(?=0\.00))) (?<\#(.change)>\d{1,}\.\d{2}) \b """# self.decoder = try! RegularExpressionDecoder(pattern: pattern, options: .allowCommentsAndWhitespace) } func testDecodeSingle() { let string = "AAPL 170.69▲0.51" let stock = try! self.decoder.decode(Stock.self, from: string) XCTAssertEqual(stock.symbol, "AAPL") XCTAssertEqual(stock.price, 170.69, accuracy: 0.01) XCTAssertEqual(stock.movement, 0.51, accuracy: 0.01) } func testDecodeMultiple() { let string = """ AAPL 170.69▲0.51 GOOG 1122.57▲2.41 AMZN 1621.48▼18.52 MSFT 106.57=0.00 SWIFT 5.0▲1.0.0 """ let stocks = try! self.decoder.decode([Stock].self, from: string) guard stocks.count == 4 else { XCTFail("decoded \(stocks.count) of 4 valid stocks") return } let AAPL = stocks[0] XCTAssertEqual(AAPL.symbol, "AAPL") XCTAssertEqual(AAPL.price, 170.69, accuracy: 0.01) XCTAssertEqual(AAPL.movement, 0.51, accuracy: 0.01) let GOOG = stocks[1] XCTAssertEqual(GOOG.symbol, "GOOG") XCTAssertEqual(GOOG.price, 1122.57, accuracy: 0.01) XCTAssertEqual(GOOG.movement, 2.41, accuracy: 0.01) let AMZN = stocks[2] XCTAssertEqual(AMZN.symbol, "AMZN") XCTAssertEqual(AMZN.price, 1621.48, accuracy: 0.01) XCTAssertEqual(AMZN.movement, -18.52, accuracy: 0.01) let MSFT = stocks[3] XCTAssertEqual(MSFT.symbol, "MSFT") XCTAssertEqual(MSFT.price, 106.57, accuracy: 0.01) XCTAssertEqual(MSFT.movement, 0.0, accuracy: 0.01) } func testDecodeInvalid() { let string = "AAPL 170.69" // missing sign and change XCTAssertThrowsError(try self.decoder.decode(Stock.self, from: string)) } }