master 38a13abea850 cached
18 files
28.3 KB
7.9k tokens
1 requests
Download .txt
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<Stock, Stock.CodingKeys> = #"""
\b
(?<\#(.symbol)>[A-Z]{1,4}) \s+
(?<\#(.price)>\d{1,}\.\d{2}) \s*
(?<\#(.sign)>([▲▼=])
(?<\#(.change)>\d{1,}\.\d{2})
\b
"""#

let decoder = try RegularExpressionDecoder<Stock>(
                    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 <dfn>regular expression</dfn>,
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 <dfn>capture groups</dfn>,
of which there are two varieties:
<dfn>positional capture groups</dfn> and
<dfn>named capture groups</dfn>.

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 `(?<NAME>___)`.

```perl
/\b
(?<symbol>[A-Z]{1,4}) \s+
(?<price>\d{1,}\.\d{2}) \s*
(?<sign>([▲▼=])
(?<change>\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<Stock, Stock.CodingKeys> = #"""
\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:
> <dfn>raw string literals</dfn>.
> 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<Stock>(
                        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<Stock, Stock.CodingKeys> = #"""
\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<Stock>(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
================================================
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='5.0' target-platform='macos' executeOnSourceChanges='false'>
    <timeline fileName='timeline.xctimeline'/>
</playground>

================================================
FILE: RegularExpressionDecoder.xcworkspace/contents.xcworkspacedata
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "group:RegularExpressionDecoder.playground">
   </FileRef>
   <FileRef
      location = "group:RegularExpressionDecoder.xcodeproj">
   </FileRef>
</Workspace>


================================================
FILE: RegularExpressionDecoder.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>IDEDidComputeMac32BitWarning</key>
	<true/>
</dict>
</plist>


================================================
FILE: Sources/RegularExpressionDecoder/KeyedDecodingContainer.swift
================================================
import Foundation

@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)
extension NSTextCheckingResult {
    func range<Key>(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<Key> 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<String.Index>? {
            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<T>(_ 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<T>(_ 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<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> 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<T: Decodable> {
    private(set) var regularExpression: NSRegularExpression
    var captureGroupNames: [String]?

    public enum Error: Swift.Error {
        case noMatches
        case tooManyMatches
    }

    public init<CodingKeys>(pattern: RegularExpressionPattern<T, CodingKeys>, 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..<string.endIndex, in: string)
        let matches = self.regularExpression.matches(in: string, options: options, range: range)

        switch matches.count {
        case 0:
            throw Error.noMatches
        case 1:
            let decoder = _RegularExpressionDecoder(string: string, matches: matches)
            decoder.userInfo[._captureGroupNames] = self.captureGroupNames

            return try T(from: decoder)
        default:
            throw Error.tooManyMatches
        }
    }

    public func decode(_ type: [T].Type, from string: String, options: NSRegularExpression.MatchingOptions = []) throws -> [T] {
        let range = NSRange(string.startIndex..<string.endIndex, in: string)
        let matches = self.regularExpression.matches(in: string, options: options, range: range)

        switch matches.count {
        case 0:
            return []
        default:
            let decoder = _RegularExpressionDecoder(string: string, matches: matches)
            decoder.userInfo[._captureGroupNames] = self.captureGroupNames

            return try [T](from: decoder)
        }
    }
}

@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)
// swiftlint:disable:next type_name
final class _RegularExpressionDecoder {
    var codingPath: [CodingKey] = []

    var userInfo: [CodingUserInfoKey: Any] = [:]

    var container: RegularExpressionDecodingContainer?
    fileprivate var string: String
    fileprivate var matches: [NSTextCheckingResult]

    init(string: String, matches: [NSTextCheckingResult]) {
        self.string = string
        self.matches = matches
    }
}

extension CodingUserInfoKey {
    // swiftlint:disable:next identifier_name
    static let _captureGroupNames = CodingUserInfoKey(rawValue: "captureGroupNames")!
}

@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)
extension _RegularExpressionDecoder: Decoder {
    fileprivate func assertCanCreateContainer() {
        precondition(self.container == nil)
    }

    func singleValueContainer() -> 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<Key>(keyedBy type: Key.Type) -> KeyedDecodingContainer<Key> where Key : CodingKey {
        assertCanCreateContainer()

        let container = KeyedContainer<Key>(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<T, CodingKeys>: LosslessStringConvertible, ExpressibleByStringInterpolation where T: Decodable, CodingKeys: CodingKey & Hashable {
    public var description: String
    public var captures: Set<CodingKeys>?

    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<CodingKeys> = []

        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<T>(_ 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<T>(_ 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<T>(_ 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<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
        defer { self.currentIndex += 1 }

        let container = _RegularExpressionDecoder.KeyedContainer<NestedKey>(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<Stock>!

    override func setUp() {
        let pattern: RegularExpressionPattern<Stock, Stock.CodingKeys> = #"""
        \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<Stock>(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))
    }
}
Download .txt
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
Condensed preview — 18 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (32K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 60,
    "preview": "github: [mattt]\ncustom: https://flight.school/books/strings\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 555,
    "preview": "name: CI\n\non:\n  push:\n    branches: [master]\n  pull_request:\n    branches: [master]\n\njobs:\n  macos:\n    runs-on: macos-l"
  },
  {
    "path": ".github/workflows/documentation.yml",
    "chars": 653,
    "preview": "name: Documentation\n\non:\n  push:\n    branches:\n      - master\n    paths:\n      - .github/workflows/documentation.yml\n   "
  },
  {
    "path": ".gitignore",
    "chars": 41,
    "preview": ".DS_Store\n/.build\n/Packages\n/*.xcodeproj\n"
  },
  {
    "path": "LICENSE.md",
    "chars": 1064,
    "preview": "Copyright 2019 Read Evaluate Press, LLC\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of"
  },
  {
    "path": "Package.swift",
    "chars": 1189,
    "preview": "// swift-tools-version:5.0\n// The swift-tools-version declares the minimum version of Swift required to build this packa"
  },
  {
    "path": "README.md",
    "chars": 7395,
    "preview": "# Regular Expression Decoder\n\n[![Build Status][build status badge]][build status]\n[![License][license badge]][license]\n["
  },
  {
    "path": "RegularExpressionDecoder.playground/Contents.swift",
    "chars": 517,
    "preview": "import RegularExpressionDecoder\n\nlet ticker = \"\"\"\nAAPL 170.69▲0.51\nGOOG 1122.57▲2.41\nAMZN 1621.48▼18.52\nMSFT 106.57=0.00"
  },
  {
    "path": "RegularExpressionDecoder.playground/Sources/Stock.swift",
    "chars": 873,
    "preview": "public struct Stock: Decodable {\n    public let symbol: String\n    public var price: Double\n    \n    enum Sign: String, "
  },
  {
    "path": "RegularExpressionDecoder.playground/contents.xcplayground",
    "chars": 198,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<playground version='5.0' target-platform='macos' executeOnSourc"
  },
  {
    "path": "RegularExpressionDecoder.xcworkspace/contents.xcworkspacedata",
    "chars": 258,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:RegularExpress"
  },
  {
    "path": "RegularExpressionDecoder.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "chars": 238,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "Sources/RegularExpressionDecoder/KeyedDecodingContainer.swift",
    "chars": 3782,
    "preview": "import Foundation\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nextension NSTextCheckingResult {\n    func range<"
  },
  {
    "path": "Sources/RegularExpressionDecoder/RegularExpressionDecoder.swift",
    "chars": 3894,
    "preview": "import Foundation\n\n/**\n\n */\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nfinal public class RegularExpressionDec"
  },
  {
    "path": "Sources/RegularExpressionDecoder/RegularExpressionPattern.swift",
    "chars": 1348,
    "preview": "public struct RegularExpressionPattern<T, CodingKeys>: LosslessStringConvertible, ExpressibleByStringInterpolation where"
  },
  {
    "path": "Sources/RegularExpressionDecoder/SingleValueDecodingContainer.swift",
    "chars": 1571,
    "preview": "import Foundation\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nextension _RegularExpressionDecoder {\n    final "
  },
  {
    "path": "Sources/RegularExpressionDecoder/UnkeyedDecodingContainer.swift",
    "chars": 2342,
    "preview": "import Foundation\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nextension _RegularExpressionDecoder {\n    final "
  },
  {
    "path": "Tests/RegularExpressionDecoderTests/RegularExpressionDecodingTests.swift",
    "chars": 2986,
    "preview": "import XCTest\nimport Foundation\n@testable import RegularExpressionDecoder\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchO"
  }
]

About this extraction

This page contains the full source code of the Flight-School/RegularExpressionDecoder GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 18 files (28.3 KB), approximately 7.9k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!