diff --git a/0126-generalized-parsing-pt3/GeneralizedParsing.playground/Contents.swift b/0126-generalized-parsing-pt3/GeneralizedParsing.playground/Contents.swift
new file mode 100644
index 00000000..7bc91dd3
--- /dev/null
+++ b/0126-generalized-parsing-pt3/GeneralizedParsing.playground/Contents.swift
@@ -0,0 +1,812 @@
+import Foundation
+
+struct Parser {
+ let run: (inout Input) -> Output?
+}
+
+extension Parser where Input == [String: String] {
+ static func key(_ key: String, _ parser: Parser) -> Self {
+ Self { dict in
+ guard var value = dict[key]?[...]
+ else { return nil }
+
+ guard let output = parser.run(&value)
+ else { return nil }
+
+ dict[key] = value.isEmpty ? nil : String(value)
+ return output
+ }
+ }
+}
+
+let xcodePath = Parser.key("IPHONE_SIMULATOR_ROOT", .prefix(through: ".app"))
+
+xcodePath.run(ProcessInfo.processInfo.environment).rest["IPHONE_SIMULATOR_ROOT"]
+
+ProcessInfo.processInfo.environment["SIMULATOR_HOST_HOME"]
+
+extension Parser where Input == Substring, Output == Substring {
+ static var rest: Self {
+// Self.prefix(while: { _ in true })
+ Self { input in
+ let rest = input
+ input = ""
+ return rest
+ }
+ }
+}
+
+let username = Parser.key("SIMULATOR_HOST_HOME", Parser.prefix("/Users/")
+ .take(.rest))
+
+username.run(ProcessInfo.processInfo.environment).rest["SIMULATOR_HOST_HOME"]
+
+xcodePath.take(username)
+
+//dump(
+Parser.prefix(through: ".app")
+ .run(
+ ProcessInfo.processInfo.environment["IPHONE_SIMULATOR_ROOT"]![...]
+)
+//)
+
+
+
+//Parser
+//Parser
+
+struct RequestData {
+ var body: Data?
+ var headers: [String: Substring]
+ var method: String?
+ var pathComponents: ArraySlice
+ var queryItems: [(name: String, value: Substring)]
+}
+
+extension Parser where Input == RequestData, Output == Void {
+ static func method(_ method: String) -> Self {
+ .init { input in
+ guard input.method?.uppercased() == method.uppercased()
+ else { return nil }
+ input.method = nil
+ return ()
+ }
+ }
+}
+
+// /foo/2
+// /foo3/2
+
+extension Parser where Input == RequestData {
+ static func path(_ parser: Parser) -> Self {
+ return .init { input in
+ guard var firstComponent = input.pathComponents.first
+ else { return nil }
+
+ let output = parser.run(&firstComponent)
+ guard firstComponent.isEmpty
+ else { return nil }
+
+ input.pathComponents.removeFirst()
+ return output
+ }
+ }
+}
+
+extension Parser where Input == RequestData {
+ static func query(name: String, _ parser: Parser) -> Self {
+ .init { input in
+ guard let index = input.queryItems.firstIndex(where: { n, value in n == name})
+ else { return nil }
+
+ let original = input.queryItems[index].value
+ guard let output = parser.run(&input.queryItems[index].value)
+ else { return nil }
+
+ guard input.queryItems[index].value.isEmpty
+ else {
+ input.queryItems[index].value = original
+ return nil
+ }
+
+ input.queryItems.remove(at: index)
+ return output
+ }
+ }
+}
+
+// optional: (Parser) -> Parser
+
+extension Parser {
+// var optional: Parser {
+// .init { input in
+// .some(self.run(&input))
+// }
+// }
+ static func optional(_ parser: Parser) -> Self where Output == Optional {
+ .init { input in
+ .some(parser.run(&input))
+ }
+ }
+}
+
+extension Parser where Input == RequestData, Output == Void {
+ static let end = Self { input in
+ guard
+ input.pathComponents.isEmpty,
+ input.method == nil
+ else { return nil }
+
+ input.body = nil
+ input.queryItems = []
+ input.headers = [:]
+ return ()
+ }
+}
+
+// GET /episodes/42?time=120
+// GET /episodes/42
+let episode = Parser.method("GET")
+ .skip(.path("episodes"))
+ .take(.path(.int))
+ .take(.optional(.query(name: "time", .int)))
+ .skip(.end)
+
+
+let request = RequestData(
+ body: nil,
+ headers: ["User-Agent": "Safari"],
+ method: "GET",
+ pathComponents: ["episodes", "1", "comments"],
+ queryItems: [(name: "time", value: "120")]
+)
+
+dump(
+episode.run(request)
+)
+
+enum Route {
+ // GET /episodes/:int?time=:int
+ case episode(id: Int, time: Int?)
+
+ // GET /episodes/:int/comments
+ case episodeComments(id: Int)
+}
+
+let episodeSection = Parser.method("GET")
+ .skip(.path("episodes"))
+ .take(.path(.int))
+
+let router = Parser.oneOf(
+ episodeSection
+ .take(.optional(.query(name: "time", .int)))
+ .skip(.end)
+ .map(Route.episode(id:time:)),
+
+ episodeSection
+ .skip(.path("comments"))
+ .skip(.end)
+ .map(Route.episodeComments(id:))
+)
+
+dump(
+router.run(request)
+)
+
+
+//URLComponents
+let components = URLComponents(string: "https://www.pointfree.co/episodes/1?time=120")
+components?.host
+components?.path
+components?.query
+components?.queryItems
+//URLQueryItem.init(name: <#T##String#>, value: <#T##String?#>)
+
+// swift package init --name=MyPackage --type=executable
+// swift build -v
+// swift build --verbose --sanitize
+// swift build --sanitize --verbose
+//dump(CommandLine.arguments)
+
+[
+ "/Users/point-free/Library/Developer/XCPGDevices/7791186F-129C-4B2D-A0DF-2B685D338A84/data/Containers/Bundle/Application/F6013A8B-FED5-4A87-B7E7-D2BA154AE029/GeneralizedParsing-17305-7.app/GeneralizedParsing",
+ "-DVTDeviceRunExecutableOptionREPLMode",
+ "1"
+]
+
+extension Parser {
+ func run(_ input: Input) -> (match: Output?, rest: Input) {
+ var input = input
+ let match = self.run(&input)
+ return (match, input)
+ }
+}
+
+extension Parser where Input == Substring, Output == Int {
+ static let int = Self { input in
+ let original = input
+
+ var isFirstCharacter = true
+ let intPrefix = input.prefix { character in
+ defer { isFirstCharacter = false }
+ return (character == "-" || character == "+") && isFirstCharacter
+ || character.isNumber
+ }
+
+ guard let match = Int(intPrefix)
+ else {
+ input = original
+ return nil
+ }
+ input.removeFirst(intPrefix.count)
+ return match
+ }
+}
+
+Parser.int.run("123 Hello")
+
+
+extension Parser where Input == Substring, Output == Double {
+ static let double = Self { input in
+ let original = input
+ let sign: Double
+ if input.first == "-" {
+ sign = -1
+ input.removeFirst()
+ } else if input.first == "+" {
+ sign = 1
+ input.removeFirst()
+ } else {
+ sign = 1
+ }
+
+ var decimalCount = 0
+ let prefix = input.prefix { char in
+ if char == "." { decimalCount += 1 }
+ return char.isNumber || (char == "." && decimalCount <= 1)
+ }
+
+ guard let match = Double(prefix)
+ else {
+ input = original
+ return nil
+ }
+
+ input.removeFirst(prefix.count)
+ return match * sign
+ }
+}
+
+Parser.double.run("123.4 Hello")
+
+
+extension Parser where Input == Substring, Output == Character {
+ static let char = Self { input in
+ guard !input.isEmpty else { return nil }
+ return input.removeFirst()
+ }
+}
+
+extension Parser {
+ static func always(_ output: Output) -> Self {
+ Self { _ in output }
+ }
+
+ static var never: Self {
+ Self { _ in nil }
+ }
+}
+
+extension Parser {
+ func map(_ f: @escaping (Output) -> NewOutput) -> Parser {
+ .init { input in
+ self.run(&input).map(f)
+ }
+ }
+}
+
+extension Parser {
+ func flatMap(
+ _ f: @escaping (Output) -> Parser
+ ) -> Parser {
+ .init { input in
+ let original = input
+ let output = self.run(&input)
+ let newParser = output.map(f)
+ guard let newOutput = newParser?.run(&input) else {
+ input = original
+ return nil
+ }
+ return newOutput
+ }
+ }
+}
+
+func zip(
+ _ p1: Parser,
+ _ p2: Parser
+) -> Parser {
+
+ .init { input -> (Output1, Output2)? in
+ let original = input
+ guard let output1 = p1.run(&input) else { return nil }
+ guard let output2 = p2.run(&input) else {
+ input = original
+ return nil
+ }
+ return (output1, output2)
+ }
+}
+
+extension Parser {
+ static func oneOf(_ ps: [Self]) -> Self {
+ .init { input in
+ for p in ps {
+ if let match = p.run(&input) {
+ return match
+ }
+ }
+ return nil
+ }
+ }
+
+ static func oneOf(_ ps: Self...) -> Self {
+ self.oneOf(ps)
+ }
+}
+
+extension Parser {
+ func zeroOrMore(
+ separatedBy separator: Parser = .always(())
+ ) -> Parser {
+ Parser { input in
+ var rest = input
+ var matches: [Output] = []
+ while let match = self.run(&input) {
+ rest = input
+ matches.append(match)
+ if separator.run(&input) == nil {
+ return matches
+ }
+ }
+ input = rest
+ return matches
+ }
+ }
+}
+
+extension Parser
+where Input: Collection,
+ Input.SubSequence == Input,
+ Output == Void,
+ Input.Element: Equatable {
+ static func prefix(_ p: Input.SubSequence) -> Self {
+ Self { input in
+ guard input.starts(with: p) else { return nil }
+ input.removeFirst(p.count)
+ return ()
+ }
+ }
+}
+
+extension Parser where Input == Substring, Output == Substring {
+ static func prefix(while p: @escaping (Character) -> Bool) -> Self {
+ Self { input in
+ let output = input.prefix(while: p)
+ input.removeFirst(output.count)
+ return output
+ }
+ }
+
+ static func prefix(upTo substring: Substring) -> Self {
+ Self { input in
+ guard let endIndex = input.range(of: substring)?.lowerBound
+ else { return nil }
+
+ let match = input[.. Self {
+ Self { input in
+ guard let endIndex = input.range(of: substring)?.upperBound
+ else { return nil }
+
+ let match = input[.. Parser {
+ p.map { _ in () }
+ }
+
+ func skip(_ p: Parser) -> Self {
+ zip(self, p).map { a, _ in a }
+ }
+
+ func take(_ p: Parser) -> Parser {
+ zip(self, p)
+ }
+
+ func take(_ p: Parser) -> Parser
+ where Output == Void {
+ zip(self, p).map { _, a in a }
+ }
+
+ func take(_ p: Parser) -> Parser
+ where Output == (A, B) {
+ zip(self, p).map { ab, c in
+ (ab.0, ab.1, c)
+ }
+ }
+}
+
+
+
+let temperature = Parser.int.skip("°F")
+
+temperature.run("100°F")
+temperature.run("-100°F")
+
+let northSouth = Parser.char.flatMap {
+ $0 == "N" ? .always(1.0)
+ : $0 == "S" ? .always(-1)
+ : .never
+}
+
+let eastWest = Parser.char.flatMap {
+ $0 == "E" ? .always(1.0)
+ : $0 == "W" ? .always(-1)
+ : .never
+}
+
+//"40.446° N"
+//"40.446° S"
+let latitude = Parser.double
+ .skip("° ")
+ .take(northSouth)
+ .map(*)
+
+let longitude = Parser.double
+ .skip("° ")
+ .take(eastWest)
+ .map(*)
+
+struct Coordinate {
+ let latitude: Double
+ let longitude: Double
+}
+
+let zeroOrMoreSpaces = Parser.prefix(" ").zeroOrMore()
+
+//"40.446° N, 79.982° W"
+let coord = latitude
+ .skip(",")
+ .skip(zeroOrMoreSpaces)
+ .take(longitude)
+ .map(Coordinate.init)
+
+
+Parser.prefix([1, 2]).run([1, 2, 3, 4, 5, 6][...])
+Parser, Void>.prefix([1, 2]).run([1, 2, 3, 4, 5, 6])
+
+Parser.prefix([1, 2]).run([1, 2, 3, 4, 5, 6][...])
+
+enum Currency { case eur, gbp, usd }
+
+let currency = Parser.oneOf(
+ Parser.prefix("€").map { Currency.eur },
+ Parser.prefix("£").map { .gbp },
+ Parser.prefix("$").map { .usd }
+)
+
+struct Money {
+ let currency: Currency
+ let value: Double
+}
+
+//"$100"
+let money = zip(currency, .double)
+ .map(Money.init(currency:value:))
+
+money.run("$100")
+money.run("£100")
+money.run("€100")
+
+
+
+let upcomingRaces = """
+ New York City, $300
+ 40.60248° N, 74.06433° W
+ 40.61807° N, 74.02966° W
+ 40.64953° N, 74.00929° W
+ 40.67884° N, 73.98198° W
+ 40.69894° N, 73.95701° W
+ 40.72791° N, 73.95314° W
+ 40.74882° N, 73.94221° W
+ 40.75740° N, 73.95309° W
+ 40.76149° N, 73.96142° W
+ 40.77111° N, 73.95362° W
+ 40.80260° N, 73.93061° W
+ 40.80409° N, 73.92893° W
+ 40.81432° N, 73.93292° W
+ 40.80325° N, 73.94472° W
+ 40.77392° N, 73.96917° W
+ 40.77293° N, 73.97671° W
+ ---
+ Berlin, €100
+ 13.36015° N, 52.51516° E
+ 13.33999° N, 52.51381° E
+ 13.32539° N, 52.51797° E
+ 13.33696° N, 52.52507° E
+ 13.36454° N, 52.52278° E
+ 13.38152° N, 52.52295° E
+ 13.40072° N, 52.52969° E
+ 13.42555° N, 52.51508° E
+ 13.41858° N, 52.49862° E
+ 13.40929° N, 52.48882° E
+ 13.37968° N, 52.49247° E
+ 13.34898° N, 52.48942° E
+ 13.34103° N, 52.47626° E
+ 13.32851° N, 52.47122° E
+ 13.30852° N, 52.46797° E
+ 13.28742° N, 52.47214° E
+ 13.29091° N, 52.48270° E
+ 13.31084° N, 52.49275° E
+ 13.32052° N, 52.50190° E
+ 13.34577° N, 52.50134° E
+ 13.36903° N, 52.50701° E
+ 13.39155° N, 52.51046° E
+ 13.37256° N, 52.51598° E
+ ---
+ London, £500
+ 51.48205° N, 0.04283° E
+ 51.47439° N, 0.02170° E
+ 51.47618° N, 0.02199° E
+ 51.49295° N, 0.05658° E
+ 51.47542° N, 0.03019° E
+ 51.47537° N, 0.03015° E
+ 51.47435° N, 0.03733° E
+ 51.47954° N, 0.04866° E
+ 51.48604° N, 0.06293° E
+ 51.49314° N, 0.06104° E
+ 51.49248° N, 0.04740° E
+ 51.48888° N, 0.03564° E
+ 51.48655° N, 0.01830° E
+ 51.48085° N, 0.02223° W
+ 51.49210° N, 0.04510° W
+ 51.49324° N, 0.04699° W
+ 51.50959° N, 0.05491° W
+ 51.50961° N, 0.05390° W
+ 51.49950° N, 0.01356° W
+ 51.50898° N, 0.02341° W
+ 51.51069° N, 0.04225° W
+ 51.51056° N, 0.04353° W
+ 51.50946° N, 0.07810° W
+ 51.51121° N, 0.09786° W
+ 51.50964° N, 0.11870° W
+ 51.50273° N, 0.13850° W
+ 51.50095° N, 0.12411° W
+ """
+
+struct Race {
+ let location: String
+ let entranceFee: Money
+ let path: [Coordinate]
+}
+
+
+let locationName = Parser.prefix(while: { $0 != "," })
+
+let race = locationName.map(String.init)
+ .skip(",")
+ .skip(zeroOrMoreSpaces)
+ .take(money)
+ .skip("\n")
+ .take(coord.zeroOrMore(separatedBy: "\n"))
+ .map(Race.init(location:entranceFee:path:))
+
+let races = race.zeroOrMore(separatedBy: "\n---\n")
+
+races.run(upcomingRaces[...])
+
+
+
+let logs = """
+Test Suite 'All tests' started at 2020-08-19 12:36:12.062
+Test Suite 'VoiceMemosTests.xctest' started at 2020-08-19 12:36:12.062
+Test Suite 'VoiceMemosTests' started at 2020-08-19 12:36:12.062
+Test Case '-[VoiceMemosTests.VoiceMemosTests testDeleteMemo]' started.
+Test Case '-[VoiceMemosTests.VoiceMemosTests testDeleteMemo]' passed (0.004 seconds).
+Test Case '-[VoiceMemosTests.VoiceMemosTests testDeleteMemoWhilePlaying]' started.
+Test Case '-[VoiceMemosTests.VoiceMemosTests testDeleteMemoWhilePlaying]' passed (0.002 seconds).
+Test Case '-[VoiceMemosTests.VoiceMemosTests testPermissionDenied]' started.
+/Users/point-free/projects/swift-composable-architecture/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift:107: error: -[VoiceMemosTests.VoiceMemosTests testPermissionDenied] : XCTAssertTrue failed
+Test Case '-[VoiceMemosTests.VoiceMemosTests testPermissionDenied]' failed (0.003 seconds).
+Test Case '-[VoiceMemosTests.VoiceMemosTests testPlayMemoFailure]' started.
+Test Case '-[VoiceMemosTests.VoiceMemosTests testPlayMemoFailure]' passed (0.002 seconds).
+Test Case '-[VoiceMemosTests.VoiceMemosTests testPlayMemoHappyPath]' started.
+Test Case '-[VoiceMemosTests.VoiceMemosTests testPlayMemoHappyPath]' passed (0.002 seconds).
+Test Case '-[VoiceMemosTests.VoiceMemosTests testRecordMemoFailure]' started.
+/Users/point-free/projects/swift-composable-architecture/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift:144: error: -[VoiceMemosTests.VoiceMemosTests testRecordMemoFailure] : State change does not match expectation: …
+
+ VoiceMemosState(
+ − alert: nil,
+ + alert: AlertState(
+ + title: "Voice memo recording failed.",
+ + message: nil,
+ + primaryButton: nil,
+ + secondaryButton: nil
+ + ),
+ audioRecorderPermission: RecorderPermission.allowed,
+ currentRecording: nil,
+ voiceMemos: [
+ ]
+ )
+
+(Expected: −, Actual: +)
+Test Case '-[VoiceMemosTests.VoiceMemosTests testRecordMemoFailure]' failed (0.009 seconds).
+Test Case '-[VoiceMemosTests.VoiceMemosTests testRecordMemoHappyPath]' started.
+/Users/point-free/projects/swift-composable-architecture/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift:56: error: -[VoiceMemosTests.VoiceMemosTests testRecordMemoHappyPath] : State change does not match expectation: …
+
+ VoiceMemosState(
+ alert: nil,
+ audioRecorderPermission: RecorderPermission.allowed,
+ currentRecording: CurrentRecording(
+ date: 2001-01-01T00:00:00Z,
+ − duration: 3.0,
+ + duration: 2.0,
+ mode: Mode.recording,
+ url: file:///tmp/DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF.m4a
+ ),
+ voiceMemos: [
+ ]
+ )
+
+(Expected: −, Actual: +)
+Test Case '-[VoiceMemosTests.VoiceMemosTests testRecordMemoHappyPath]' failed (0.006 seconds).
+Test Case '-[VoiceMemosTests.VoiceMemosTests testStopMemo]' started.
+Test Case '-[VoiceMemosTests.VoiceMemosTests testStopMemo]' passed (0.001 seconds).
+Test Suite 'VoiceMemosTests' failed at 2020-08-19 12:36:12.094.
+ Executed 8 tests, with 3 failures (0 unexpected) in 0.029 (0.032) seconds
+Test Suite 'VoiceMemosTests.xctest' failed at 2020-08-19 12:36:12.094.
+ Executed 8 tests, with 3 failures (0 unexpected) in 0.029 (0.032) seconds
+Test Suite 'All tests' failed at 2020-08-19 12:36:12.095.
+ Executed 8 tests, with 3 failures (0 unexpected) in 0.029 (0.033) seconds
+2020-08-19 12:36:19.538 xcodebuild[45126:3958202] [MT] IDETestOperationsObserverDebug: 14.165 elapsed -- Testing started completed.
+2020-08-19 12:36:19.538 xcodebuild[45126:3958202] [MT] IDETestOperationsObserverDebug: 0.000 sec, +0.000 sec -- start
+2020-08-19 12:36:19.538 xcodebuild[45126:3958202] [MT] IDETestOperationsObserverDebug: 14.165 sec, +14.165 sec -- end
+
+Test session results, code coverage, and logs:
+ /Users/point-free/Library/Developer/Xcode/DerivedData/ComposableArchitecture-fnpkwoynrpjrkrfemkkhfdzooaes/Logs/Test/Test-VoiceMemos-2020.08.19_12-35-57--0400.xcresult
+
+Failing tests:
+ VoiceMemosTests:
+ VoiceMemosTests.testPermissionDenied()
+ VoiceMemosTests.testRecordMemoFailure()
+ VoiceMemosTests.testRecordMemoHappyPath()
+
+"""
+
+let testCaseFinishedLine = Parser
+ .skip(.prefix(through: " ("))
+ .take(.double)
+ .skip(" seconds).\n")
+
+testCaseFinishedLine.run("""
+Test Case '-[VoiceMemosTests.VoiceMemosTests testPermissionDenied]' failed (0.003 seconds).
+
+""")
+
+let testCaseStartedLine = Parser
+ .skip(.prefix(upTo: "Test Case '-["))
+ .take(.prefix(through: "\n"))
+ .map { line in
+ line.split(separator: " ")[3].dropLast(2)
+ }
+
+let fileName = Parser
+ .skip("/")
+ .take(.prefix(through: ".swift"))
+ .flatMap { path in
+ path.split(separator: "/").last.map(Parser.always)
+ ?? .never
+ }
+
+let testCaseBody = fileName
+ .skip(":")
+ .take(.int)
+ .skip(.prefix(through: "] : "))
+ .take(Parser.prefix(upTo: "Test Case '-[").map { $0.dropLast() })
+
+testCaseBody.run("""
+/Users/point-free/projects/swift-composable-architecture/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift:107: error: -[VoiceMemosTests.VoiceMemosTests testPermissionDenied] : XCTAssertTrue failed
+Test Case '-[VoiceMemosTests.VoiceMemosTests testPermissionDenied]' failed (0.003 seconds).
+""")
+
+fileName.run("/Users/point-free/projects/swift-composable-architecture/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift:107: error: -[VoiceMemosTests.VoiceMemosTests testPermissionDenied] : XCTAssertTrue failed")
+
+enum TestResult {
+ case failed(failureMessage: Substring, file: Substring, line: Int, testName: Substring, time: TimeInterval)
+ case passed(testName: Substring, time: TimeInterval)
+}
+
+let testFailed = testCaseStartedLine
+ .take(testCaseBody)
+ .take(testCaseFinishedLine)
+ .map { testName, bodyData, time in
+ TestResult.failed(failureMessage: bodyData.2, file: bodyData.0, line: bodyData.1, testName: testName, time: time)
+ }
+
+let testPassed = testCaseStartedLine
+ .take(testCaseFinishedLine)
+ .map(TestResult.passed(testName:time:))
+
+let testResult = Parser.oneOf(testFailed, testPassed)
+
+let testResults = testResult.zeroOrMore()
+
+testResults.run(logs[...])
+
+testCaseStartedLine.run(logs[...])
+
+//VoiceMemoTests.swift:123, testDelete failed in 2.00 seconds.
+// ┃
+// ┃ XCTAssertTrue failed
+// ┃
+// ┗━━──────────────
+func format(result: TestResult) -> String {
+ switch result {
+ case .failed(failureMessage: let failureMessage, file: let file, line: let line, testName: let testName, time: let time):
+ var output = "\(file):\(line), \(testName) failed in \(time) seconds."
+ output.append("\n")
+ output.append(" ┃")
+ output.append("\n")
+ output.append(
+ failureMessage
+ .split(separator: "\n")
+ .map { " ┃ \($0)" }
+ .joined(separator: "\n")
+ )
+ output.append("\n")
+ output.append(" ┃")
+ output.append("\n")
+ output.append(" ┗━━──────────────")
+ output.append("\n")
+ return output
+ case .passed(testName: let testName, time: let time):
+ return "\(testName) passed in \(time) seconds."
+ }
+}
+
+format(result: .failed(failureMessage: "XCTAssertTrue failed", file: "VoiceMemosTest.swift", line: 123, testName: "testFailed", time: 0.03))
+
+while let line = readLine() {
+ // process line
+}
diff --git a/0126-generalized-parsing-pt3/GeneralizedParsing.playground/contents.xcplayground b/0126-generalized-parsing-pt3/GeneralizedParsing.playground/contents.xcplayground
new file mode 100644
index 00000000..a751024c
--- /dev/null
+++ b/0126-generalized-parsing-pt3/GeneralizedParsing.playground/contents.xcplayground
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/0126-generalized-parsing-pt3/README.md b/0126-generalized-parsing-pt3/README.md
new file mode 100644
index 00000000..178f22ac
--- /dev/null
+++ b/0126-generalized-parsing-pt3/README.md
@@ -0,0 +1,5 @@
+## [Point-Free](https://www.pointfree.co)
+
+> #### This directory contains code from Point-Free Episode: [Generalized Parsing: Part 3](https://www.pointfree.co/episodes/ep126-generalized-parsing-part-3)
+>
+> Generalizing the parser type has allowed us to parse more types of inputs, but that is only scratching the surface. It also unlocks many new things that were previously impossible to see, including the ability to parse a stream of inputs and stream its output, making our parsers much more performant.
diff --git a/0126-generalized-parsing-pt3/stdin/.gitignore b/0126-generalized-parsing-pt3/stdin/.gitignore
new file mode 100644
index 00000000..95c43209
--- /dev/null
+++ b/0126-generalized-parsing-pt3/stdin/.gitignore
@@ -0,0 +1,5 @@
+.DS_Store
+/.build
+/Packages
+/*.xcodeproj
+xcuserdata/
diff --git a/0126-generalized-parsing-pt3/stdin/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/0126-generalized-parsing-pt3/stdin/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..919434a6
--- /dev/null
+++ b/0126-generalized-parsing-pt3/stdin/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/0126-generalized-parsing-pt3/stdin/Package.swift b/0126-generalized-parsing-pt3/stdin/Package.swift
new file mode 100644
index 00000000..ba918fca
--- /dev/null
+++ b/0126-generalized-parsing-pt3/stdin/Package.swift
@@ -0,0 +1,22 @@
+// swift-tools-version:5.3
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "stdin",
+ 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 this package depends on.
+ .target(
+ name: "stdin",
+ dependencies: []),
+ .testTarget(
+ name: "stdinTests",
+ dependencies: ["stdin"]),
+ ]
+)
diff --git a/0126-generalized-parsing-pt3/stdin/README.md b/0126-generalized-parsing-pt3/stdin/README.md
new file mode 100644
index 00000000..0f8ba03a
--- /dev/null
+++ b/0126-generalized-parsing-pt3/stdin/README.md
@@ -0,0 +1,3 @@
+# stdin
+
+A description of this package.
diff --git a/0126-generalized-parsing-pt3/stdin/Sources/stdin/ParsingStreams.swift b/0126-generalized-parsing-pt3/stdin/Sources/stdin/ParsingStreams.swift
new file mode 100644
index 00000000..ac896a1f
--- /dev/null
+++ b/0126-generalized-parsing-pt3/stdin/Sources/stdin/ParsingStreams.swift
@@ -0,0 +1,813 @@
+import Foundation
+
+struct Parser {
+ let run: (inout Input) -> Output?
+}
+
+extension Parser where Input == [String: String] {
+ static func key(_ key: String, _ parser: Parser) -> Self {
+ Self { dict in
+ guard var value = dict[key]?[...]
+ else { return nil }
+
+ guard let output = parser.run(&value)
+ else { return nil }
+
+ dict[key] = value.isEmpty ? nil : String(value)
+ return output
+ }
+ }
+}
+
+let xcodePath = Parser.key("IPHONE_SIMULATOR_ROOT", .prefix(through: ".app"))
+
+//xcodePath.run(ProcessInfo.processInfo.environment).rest["IPHONE_SIMULATOR_ROOT"]
+
+//ProcessInfo.processInfo.environment["SIMULATOR_HOST_HOME"]
+
+extension Parser where Input == Substring, Output == Substring {
+ static var rest: Self {
+// Self.prefix(while: { _ in true })
+ Self { input in
+ let rest = input
+ input = ""
+ return rest
+ }
+ }
+}
+
+let username = Parser.key("SIMULATOR_HOST_HOME", Parser.prefix("/Users/")
+ .take(.rest))
+
+//username.run(ProcessInfo.processInfo.environment).rest["SIMULATOR_HOST_HOME"]
+
+//xcodePath.take(username)
+
+//dump(
+//Parser.prefix(through: ".app")
+// .run(
+// ProcessInfo.processInfo.environment["IPHONE_SIMULATOR_ROOT"]![...]
+//)
+//)
+
+
+
+//Parser
+//Parser
+
+struct RequestData {
+ var body: Data?
+ var headers: [String: Substring]
+ var method: String?
+ var pathComponents: ArraySlice
+ var queryItems: [(name: String, value: Substring)]
+}
+
+extension Parser where Input == RequestData, Output == Void {
+ static func method(_ method: String) -> Self {
+ .init { input in
+ guard input.method?.uppercased() == method.uppercased()
+ else { return nil }
+ input.method = nil
+ return ()
+ }
+ }
+}
+
+// /foo/2
+// /foo3/2
+
+extension Parser where Input == RequestData {
+ static func path(_ parser: Parser) -> Self {
+ return .init { input in
+ guard var firstComponent = input.pathComponents.first
+ else { return nil }
+
+ let output = parser.run(&firstComponent)
+ guard firstComponent.isEmpty
+ else { return nil }
+
+ input.pathComponents.removeFirst()
+ return output
+ }
+ }
+}
+
+extension Parser where Input == RequestData {
+ static func query(name: String, _ parser: Parser) -> Self {
+ .init { input in
+ guard let index = input.queryItems.firstIndex(where: { n, value in n == name})
+ else { return nil }
+
+ let original = input.queryItems[index].value
+ guard let output = parser.run(&input.queryItems[index].value)
+ else { return nil }
+
+ guard input.queryItems[index].value.isEmpty
+ else {
+ input.queryItems[index].value = original
+ return nil
+ }
+
+ input.queryItems.remove(at: index)
+ return output
+ }
+ }
+}
+
+// optional: (Parser) -> Parser
+
+extension Parser {
+// var optional: Parser {
+// .init { input in
+// .some(self.run(&input))
+// }
+// }
+ static func optional(_ parser: Parser) -> Self where Output == Optional {
+ .init { input in
+ .some(parser.run(&input))
+ }
+ }
+}
+
+extension Parser where Input == RequestData, Output == Void {
+ static let end = Self { input in
+ guard
+ input.pathComponents.isEmpty,
+ input.method == nil
+ else { return nil }
+
+ input.body = nil
+ input.queryItems = []
+ input.headers = [:]
+ return ()
+ }
+}
+
+// GET /episodes/42?time=120
+// GET /episodes/42
+let episode = Parser.method("GET")
+ .skip(.path("episodes"))
+ .take(.path(.int))
+ .take(.optional(.query(name: "time", .int)))
+ .skip(.end)
+
+
+let request = RequestData(
+ body: nil,
+ headers: ["User-Agent": "Safari"],
+ method: "GET",
+ pathComponents: ["episodes", "1", "comments"],
+ queryItems: [(name: "time", value: "120")]
+)
+
+//dump(
+//episode.run(request)
+//)
+
+enum Route {
+ // GET /episodes/:int?time=:int
+ case episode(id: Int, time: Int?)
+
+ // GET /episodes/:int/comments
+ case episodeComments(id: Int)
+}
+
+let episodeSection = Parser.method("GET")
+ .skip(.path("episodes"))
+ .take(.path(.int))
+
+let router = Parser.oneOf(
+ episodeSection
+ .take(.optional(.query(name: "time", .int)))
+ .skip(.end)
+ .map(Route.episode(id:time:)),
+
+ episodeSection
+ .skip(.path("comments"))
+ .skip(.end)
+ .map(Route.episodeComments(id:))
+)
+
+//dump(
+//router.run(request)
+//)
+
+
+//URLComponents
+let components = URLComponents(string: "https://www.pointfree.co/episodes/1?time=120")
+//components?.host
+//components?.path
+//components?.query
+//components?.queryItems
+//URLQueryItem.init(name: <#T##String#>, value: <#T##String?#>)
+
+// swift package init --name=MyPackage --type=executable
+// swift build -v
+// swift build --verbose --sanitize
+// swift build --sanitize --verbose
+//dump(CommandLine.arguments)
+
+//[
+// "/Users/point-free/Library/Developer/XCPGDevices/7791186F-129C-4B2D-A0DF-2B685D338A84/data/Containers/Bundle/Application/F6013A8B-FED5-4A87-B7E7-D2BA154AE029/GeneralizedParsing-17305-7.app/GeneralizedParsing",
+// "-DVTDeviceRunExecutableOptionREPLMode",
+// "1"
+//]
+
+extension Parser {
+ func run(_ input: Input) -> (match: Output?, rest: Input) {
+ var input = input
+ let match = self.run(&input)
+ return (match, input)
+ }
+}
+
+extension Parser where Input == Substring, Output == Int {
+ static let int = Self { input in
+ let original = input
+
+ var isFirstCharacter = true
+ let intPrefix = input.prefix { character in
+ defer { isFirstCharacter = false }
+ return (character == "-" || character == "+") && isFirstCharacter
+ || character.isNumber
+ }
+
+ guard let match = Int(intPrefix)
+ else {
+ input = original
+ return nil
+ }
+ input.removeFirst(intPrefix.count)
+ return match
+ }
+}
+
+//Parser.int.run("123 Hello")
+
+
+extension Parser where Input == Substring, Output == Double {
+ static let double = Self { input in
+ let original = input
+ let sign: Double
+ if input.first == "-" {
+ sign = -1
+ input.removeFirst()
+ } else if input.first == "+" {
+ sign = 1
+ input.removeFirst()
+ } else {
+ sign = 1
+ }
+
+ var decimalCount = 0
+ let prefix = input.prefix { char in
+ if char == "." { decimalCount += 1 }
+ return char.isNumber || (char == "." && decimalCount <= 1)
+ }
+
+ guard let match = Double(prefix)
+ else {
+ input = original
+ return nil
+ }
+
+ input.removeFirst(prefix.count)
+ return match * sign
+ }
+}
+
+//Parser.double.run("123.4 Hello")
+
+
+extension Parser where Input == Substring, Output == Character {
+ static let char = Self { input in
+ guard !input.isEmpty else { return nil }
+ return input.removeFirst()
+ }
+}
+
+extension Parser {
+ static func always(_ output: Output) -> Self {
+ Self { _ in output }
+ }
+
+ static var never: Self {
+ Self { _ in nil }
+ }
+}
+
+extension Parser {
+ func map(_ f: @escaping (Output) -> NewOutput) -> Parser {
+ .init { input in
+ self.run(&input).map(f)
+ }
+ }
+}
+
+extension Parser {
+ func flatMap(
+ _ f: @escaping (Output) -> Parser
+ ) -> Parser {
+ .init { input in
+ let original = input
+ let output = self.run(&input)
+ let newParser = output.map(f)
+ guard let newOutput = newParser?.run(&input) else {
+ input = original
+ return nil
+ }
+ return newOutput
+ }
+ }
+}
+
+func zip(
+ _ p1: Parser,
+ _ p2: Parser
+) -> Parser {
+
+ .init { input -> (Output1, Output2)? in
+ let original = input
+ guard let output1 = p1.run(&input) else { return nil }
+ guard let output2 = p2.run(&input) else {
+ input = original
+ return nil
+ }
+ return (output1, output2)
+ }
+}
+
+extension Parser {
+ static func oneOf(_ ps: [Self]) -> Self {
+ .init { input in
+ for p in ps {
+ if let match = p.run(&input) {
+ return match
+ }
+ }
+ return nil
+ }
+ }
+
+ static func oneOf(_ ps: Self...) -> Self {
+ self.oneOf(ps)
+ }
+}
+
+extension Parser {
+ func zeroOrMore(
+ separatedBy separator: Parser = .always(())
+ ) -> Parser {
+ Parser { input in
+ var rest = input
+ var matches: [Output] = []
+ while let match = self.run(&input) {
+ rest = input
+ matches.append(match)
+ if separator.run(&input) == nil {
+ return matches
+ }
+ }
+ input = rest
+ return matches
+ }
+ }
+}
+
+extension Parser
+where Input: Collection,
+ Input.SubSequence == Input,
+ Output == Void,
+ Input.Element: Equatable {
+ static func prefix(_ p: Input.SubSequence) -> Self {
+ Self { input in
+ guard input.starts(with: p) else { return nil }
+ input.removeFirst(p.count)
+ return ()
+ }
+ }
+}
+
+extension Parser where Input == Substring, Output == Substring {
+ static func prefix(while p: @escaping (Character) -> Bool) -> Self {
+ Self { input in
+ let output = input.prefix(while: p)
+ input.removeFirst(output.count)
+ return output
+ }
+ }
+
+ static func prefix(upTo substring: Substring) -> Self {
+ Self { input in
+ guard let endIndex = input.range(of: substring)?.lowerBound
+ else { return nil }
+
+ let match = input[.. Self {
+ Self { input in
+ guard let endIndex = input.range(of: substring)?.upperBound
+ else { return nil }
+
+ let match = input[.. Parser {
+ p.map { _ in () }
+ }
+
+ func skip(_ p: Parser) -> Self {
+ zip(self, p).map { a, _ in a }
+ }
+
+ func take(_ p: Parser) -> Parser {
+ zip(self, p)
+ }
+
+ func take(_ p: Parser) -> Parser
+ where Output == Void {
+ zip(self, p).map { _, a in a }
+ }
+
+ func take(_ p: Parser) -> Parser
+ where Output == (A, B) {
+ zip(self, p).map { ab, c in
+ (ab.0, ab.1, c)
+ }
+ }
+}
+
+
+
+let temperature = Parser.int.skip("°F")
+
+//temperature.run("100°F")
+//temperature.run("-100°F")
+
+let northSouth = Parser.char.flatMap {
+ $0 == "N" ? .always(1.0)
+ : $0 == "S" ? .always(-1)
+ : .never
+}
+
+let eastWest = Parser.char.flatMap {
+ $0 == "E" ? .always(1.0)
+ : $0 == "W" ? .always(-1)
+ : .never
+}
+
+//"40.446° N"
+//"40.446° S"
+let latitude = Parser.double
+ .skip("° ")
+ .take(northSouth)
+ .map(*)
+
+let longitude = Parser.double
+ .skip("° ")
+ .take(eastWest)
+ .map(*)
+
+struct Coordinate {
+ let latitude: Double
+ let longitude: Double
+}
+
+let zeroOrMoreSpaces = Parser.prefix(" ").zeroOrMore()
+
+//"40.446° N, 79.982° W"
+let coord = latitude
+ .skip(",")
+ .skip(zeroOrMoreSpaces)
+ .take(longitude)
+ .map(Coordinate.init)
+
+
+//Parser.prefix([1, 2]).run([1, 2, 3, 4, 5, 6][...])
+//Parser, Void>.prefix([1, 2]).run([1, 2, 3, 4, 5, 6])
+//
+//Parser.prefix([1, 2]).run([1, 2, 3, 4, 5, 6][...])
+
+enum Currency { case eur, gbp, usd }
+
+let currency = Parser.oneOf(
+ Parser.prefix("€").map { Currency.eur },
+ Parser.prefix("£").map { .gbp },
+ Parser.prefix("$").map { .usd }
+)
+
+struct Money {
+ let currency: Currency
+ let value: Double
+}
+
+//"$100"
+let money = zip(currency, .double)
+ .map(Money.init(currency:value:))
+
+//money.run("$100")
+//money.run("£100")
+//money.run("€100")
+
+
+
+let upcomingRaces = """
+ New York City, $300
+ 40.60248° N, 74.06433° W
+ 40.61807° N, 74.02966° W
+ 40.64953° N, 74.00929° W
+ 40.67884° N, 73.98198° W
+ 40.69894° N, 73.95701° W
+ 40.72791° N, 73.95314° W
+ 40.74882° N, 73.94221° W
+ 40.75740° N, 73.95309° W
+ 40.76149° N, 73.96142° W
+ 40.77111° N, 73.95362° W
+ 40.80260° N, 73.93061° W
+ 40.80409° N, 73.92893° W
+ 40.81432° N, 73.93292° W
+ 40.80325° N, 73.94472° W
+ 40.77392° N, 73.96917° W
+ 40.77293° N, 73.97671° W
+ ---
+ Berlin, €100
+ 13.36015° N, 52.51516° E
+ 13.33999° N, 52.51381° E
+ 13.32539° N, 52.51797° E
+ 13.33696° N, 52.52507° E
+ 13.36454° N, 52.52278° E
+ 13.38152° N, 52.52295° E
+ 13.40072° N, 52.52969° E
+ 13.42555° N, 52.51508° E
+ 13.41858° N, 52.49862° E
+ 13.40929° N, 52.48882° E
+ 13.37968° N, 52.49247° E
+ 13.34898° N, 52.48942° E
+ 13.34103° N, 52.47626° E
+ 13.32851° N, 52.47122° E
+ 13.30852° N, 52.46797° E
+ 13.28742° N, 52.47214° E
+ 13.29091° N, 52.48270° E
+ 13.31084° N, 52.49275° E
+ 13.32052° N, 52.50190° E
+ 13.34577° N, 52.50134° E
+ 13.36903° N, 52.50701° E
+ 13.39155° N, 52.51046° E
+ 13.37256° N, 52.51598° E
+ ---
+ London, £500
+ 51.48205° N, 0.04283° E
+ 51.47439° N, 0.02170° E
+ 51.47618° N, 0.02199° E
+ 51.49295° N, 0.05658° E
+ 51.47542° N, 0.03019° E
+ 51.47537° N, 0.03015° E
+ 51.47435° N, 0.03733° E
+ 51.47954° N, 0.04866° E
+ 51.48604° N, 0.06293° E
+ 51.49314° N, 0.06104° E
+ 51.49248° N, 0.04740° E
+ 51.48888° N, 0.03564° E
+ 51.48655° N, 0.01830° E
+ 51.48085° N, 0.02223° W
+ 51.49210° N, 0.04510° W
+ 51.49324° N, 0.04699° W
+ 51.50959° N, 0.05491° W
+ 51.50961° N, 0.05390° W
+ 51.49950° N, 0.01356° W
+ 51.50898° N, 0.02341° W
+ 51.51069° N, 0.04225° W
+ 51.51056° N, 0.04353° W
+ 51.50946° N, 0.07810° W
+ 51.51121° N, 0.09786° W
+ 51.50964° N, 0.11870° W
+ 51.50273° N, 0.13850° W
+ 51.50095° N, 0.12411° W
+ """
+
+struct Race {
+ let location: String
+ let entranceFee: Money
+ let path: [Coordinate]
+}
+
+
+let locationName = Parser.prefix(while: { $0 != "," })
+
+let race = locationName.map(String.init)
+ .skip(",")
+ .skip(zeroOrMoreSpaces)
+ .take(money)
+ .skip("\n")
+ .take(coord.zeroOrMore(separatedBy: "\n"))
+ .map(Race.init(location:entranceFee:path:))
+
+let races = race.zeroOrMore(separatedBy: "\n---\n")
+
+//races.run(upcomingRaces[...])
+
+
+
+let logs = """
+Test Suite 'All tests' started at 2020-08-19 12:36:12.062
+Test Suite 'VoiceMemosTests.xctest' started at 2020-08-19 12:36:12.062
+Test Suite 'VoiceMemosTests' started at 2020-08-19 12:36:12.062
+Test Case '-[VoiceMemosTests.VoiceMemosTests testDeleteMemo]' started.
+Test Case '-[VoiceMemosTests.VoiceMemosTests testDeleteMemo]' passed (0.004 seconds).
+Test Case '-[VoiceMemosTests.VoiceMemosTests testDeleteMemoWhilePlaying]' started.
+Test Case '-[VoiceMemosTests.VoiceMemosTests testDeleteMemoWhilePlaying]' passed (0.002 seconds).
+Test Case '-[VoiceMemosTests.VoiceMemosTests testPermissionDenied]' started.
+/Users/point-free/projects/swift-composable-architecture/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift:107: error: -[VoiceMemosTests.VoiceMemosTests testPermissionDenied] : XCTAssertTrue failed
+Test Case '-[VoiceMemosTests.VoiceMemosTests testPermissionDenied]' failed (0.003 seconds).
+Test Case '-[VoiceMemosTests.VoiceMemosTests testPlayMemoFailure]' started.
+Test Case '-[VoiceMemosTests.VoiceMemosTests testPlayMemoFailure]' passed (0.002 seconds).
+Test Case '-[VoiceMemosTests.VoiceMemosTests testPlayMemoHappyPath]' started.
+Test Case '-[VoiceMemosTests.VoiceMemosTests testPlayMemoHappyPath]' passed (0.002 seconds).
+Test Case '-[VoiceMemosTests.VoiceMemosTests testRecordMemoFailure]' started.
+/Users/point-free/projects/swift-composable-architecture/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift:144: error: -[VoiceMemosTests.VoiceMemosTests testRecordMemoFailure] : State change does not match expectation: …
+
+ VoiceMemosState(
+ − alert: nil,
+ + alert: AlertState(
+ + title: "Voice memo recording failed.",
+ + message: nil,
+ + primaryButton: nil,
+ + secondaryButton: nil
+ + ),
+ audioRecorderPermission: RecorderPermission.allowed,
+ currentRecording: nil,
+ voiceMemos: [
+ ]
+ )
+
+(Expected: −, Actual: +)
+Test Case '-[VoiceMemosTests.VoiceMemosTests testRecordMemoFailure]' failed (0.009 seconds).
+Test Case '-[VoiceMemosTests.VoiceMemosTests testRecordMemoHappyPath]' started.
+/Users/point-free/projects/swift-composable-architecture/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift:56: error: -[VoiceMemosTests.VoiceMemosTests testRecordMemoHappyPath] : State change does not match expectation: …
+
+ VoiceMemosState(
+ alert: nil,
+ audioRecorderPermission: RecorderPermission.allowed,
+ currentRecording: CurrentRecording(
+ date: 2001-01-01T00:00:00Z,
+ − duration: 3.0,
+ + duration: 2.0,
+ mode: Mode.recording,
+ url: file:///tmp/DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF.m4a
+ ),
+ voiceMemos: [
+ ]
+ )
+
+(Expected: −, Actual: +)
+Test Case '-[VoiceMemosTests.VoiceMemosTests testRecordMemoHappyPath]' failed (0.006 seconds).
+Test Case '-[VoiceMemosTests.VoiceMemosTests testStopMemo]' started.
+Test Case '-[VoiceMemosTests.VoiceMemosTests testStopMemo]' passed (0.001 seconds).
+Test Suite 'VoiceMemosTests' failed at 2020-08-19 12:36:12.094.
+ Executed 8 tests, with 3 failures (0 unexpected) in 0.029 (0.032) seconds
+Test Suite 'VoiceMemosTests.xctest' failed at 2020-08-19 12:36:12.094.
+ Executed 8 tests, with 3 failures (0 unexpected) in 0.029 (0.032) seconds
+Test Suite 'All tests' failed at 2020-08-19 12:36:12.095.
+ Executed 8 tests, with 3 failures (0 unexpected) in 0.029 (0.033) seconds
+2020-08-19 12:36:19.538 xcodebuild[45126:3958202] [MT] IDETestOperationsObserverDebug: 14.165 elapsed -- Testing started completed.
+2020-08-19 12:36:19.538 xcodebuild[45126:3958202] [MT] IDETestOperationsObserverDebug: 0.000 sec, +0.000 sec -- start
+2020-08-19 12:36:19.538 xcodebuild[45126:3958202] [MT] IDETestOperationsObserverDebug: 14.165 sec, +14.165 sec -- end
+
+Test session results, code coverage, and logs:
+ /Users/point-free/Library/Developer/Xcode/DerivedData/ComposableArchitecture-fnpkwoynrpjrkrfemkkhfdzooaes/Logs/Test/Test-VoiceMemos-2020.08.19_12-35-57--0400.xcresult
+
+Failing tests:
+ VoiceMemosTests:
+ VoiceMemosTests.testPermissionDenied()
+ VoiceMemosTests.testRecordMemoFailure()
+ VoiceMemosTests.testRecordMemoHappyPath()
+
+"""
+
+let testCaseFinishedLine = Parser
+ .skip(.prefix(through: " ("))
+ .take(.double)
+ .skip(" seconds).\n")
+
+//testCaseFinishedLine.run("""
+//Test Case '-[VoiceMemosTests.VoiceMemosTests testPermissionDenied]' failed (0.003 seconds).
+//
+//""")
+
+let testCaseStartedLine = Parser
+ .skip(.prefix(upTo: "Test Case '-["))
+ .take(.prefix(through: "\n"))
+ .map { line in
+ line.split(separator: " ")[3].dropLast(2)
+ }
+
+let fileName = Parser
+ .skip("/")
+ .take(.prefix(through: ".swift"))
+ .flatMap { path in
+ path.split(separator: "/").last.map(Parser.always)
+ ?? .never
+ }
+
+let testCaseBody = fileName
+ .skip(":")
+ .take(.int)
+ .skip(.prefix(through: "] : "))
+ .take(Parser.prefix(upTo: "Test Case '-[").map { $0.dropLast() })
+
+//testCaseBody.run("""
+///Users/point-free/projects/swift-composable-architecture/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift:107: error: -[VoiceMemosTests.VoiceMemosTests testPermissionDenied] : XCTAssertTrue failed
+//Test Case '-[VoiceMemosTests.VoiceMemosTests testPermissionDenied]' failed (0.003 seconds).
+//""")
+//
+//fileName.run("/Users/point-free/projects/swift-composable-architecture/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift:107: error: -[VoiceMemosTests.VoiceMemosTests testPermissionDenied] : XCTAssertTrue failed")
+
+enum TestResult {
+ case failed(failureMessage: Substring, file: Substring, line: Int, testName: Substring, time: TimeInterval)
+ case passed(testName: Substring, time: TimeInterval)
+}
+
+let testFailed = testCaseStartedLine
+ .take(testCaseBody)
+ .take(testCaseFinishedLine)
+ .map { testName, bodyData, time in
+ TestResult.failed(failureMessage: bodyData.2, file: bodyData.0, line: bodyData.1, testName: testName, time: time)
+ }
+
+let testPassed = testCaseStartedLine
+ .take(testCaseFinishedLine)
+ .map(TestResult.passed(testName:time:))
+
+let testResult = Parser.oneOf(testFailed, testPassed)
+
+let testResults = testResult.zeroOrMore()
+
+//testResults.run(logs[...])
+//
+//testCaseStartedLine.run(logs[...])
+
+//VoiceMemoTests.swift:123, testDelete failed in 2.00 seconds.
+// ┃
+// ┃ XCTAssertTrue failed
+// ┃
+// ┗━━──────────────
+func format(result: TestResult) -> String {
+ switch result {
+ case .failed(failureMessage: let failureMessage, file: let file, line: let line, testName: let testName, time: let time):
+ var output = "\(file):\(line), \(testName) failed in \(time) seconds."
+ output.append("\n")
+ output.append(" ┃")
+ output.append("\n")
+ output.append(
+ failureMessage
+ .split(separator: "\n")
+ .map { " ┃ \($0)" }
+ .joined(separator: "\n")
+ )
+ output.append("\n")
+ output.append(" ┃")
+ output.append("\n")
+ output.append(" ┗━━──────────────")
+ output.append("\n")
+ return output
+ case .passed(testName: let testName, time: let time):
+ return "\(testName) passed in \(time) seconds."
+ }
+}
+
+//format(result: .failed(failureMessage: "XCTAssertTrue failed", file: "VoiceMemosTest.swift", line: 123, testName: "testFailed", time: 0.03))
+//
+//while let line = readLine() {
+// // process line
+//}
+
diff --git a/0126-generalized-parsing-pt3/stdin/Sources/stdin/main.swift b/0126-generalized-parsing-pt3/stdin/Sources/stdin/main.swift
new file mode 100644
index 00000000..e6ab02e4
--- /dev/null
+++ b/0126-generalized-parsing-pt3/stdin/Sources/stdin/main.swift
@@ -0,0 +1,102 @@
+//print("Starting...")
+//while let line = readLine() {
+// print("You typed: \(line)")
+//}
+//print("Done!")
+
+struct NaturalNumbers: IteratorProtocol {
+ var count = 0
+
+ mutating func next() -> Int? {
+ defer { self.count += 1 }
+ return self.count
+ }
+}
+
+struct OneBillionNumbers: IteratorProtocol {
+ var count = 0
+
+ mutating func next() -> Int? {
+ defer { self.count += 1 }
+ return self.count <= 1_000_000_000
+ ? self.count
+ : nil
+ }
+}
+
+//Array(1...1_000_000_000)
+
+var naturals = sequence(first: 0, next: { $0 + 1 })
+var oneBillion = sequence(first: 0, next: { $0 < 1_000_000_000 ? $0 + 1 : nil })
+
+let randomNumbers = AnyIterator {
+ // produce next value
+ Int.random(in: 1 ... .max)
+}
+
+// (Parser) -> Parser, [Output]>
+
+extension Parser where Input: RangeReplaceableCollection {
+ var stream: Parser, [Output]> {
+ .init { stream in
+ var buffer = Input()
+ var outputs: [Output] = []
+ while let chunk = stream.next() {
+ buffer.append(contentsOf: chunk)
+
+ while let output = self.run(&buffer) {
+ outputs.append(output)
+ }
+ }
+
+ return outputs
+ }
+ }
+}
+
+var stdin = AnyIterator { readLine(strippingNewline: false)?[...] }
+
+testResult
+ .stream
+ .run(stdin)
+ .match?
+ .forEach {
+ print(format(result: $0))
+ }
+
+extension Parser where Input: RangeReplaceableCollection {
+ func run(
+ input: inout AnyIterator,
+ output streamOut: (Output) -> Void
+ ) {
+ print("start")
+ var buffer = Input()
+ while let chunk = input.next() {
+ print("chunk", chunk)
+ buffer.append(contentsOf: chunk)
+
+ while let output = self.run(&buffer) {
+ print("output", output)
+ streamOut(output)
+ }
+ }
+ print("done")
+ }
+}
+
+testResult
+ .run(input: &stdin, output: { print(format(result: $0)) })
+
+//
+//var stdinLogs: Substring = ""
+//var results: [TestResult] = []
+//while let line = readLine() {
+// stdinLogs.append(contentsOf: line)
+// stdinLogs.append("\n")
+// if let result = testResult.run(&stdinLogs) {
+// results.append(result)
+// }
+//}
+//results.forEach { result in
+// print(format(result: result))
+//}
diff --git a/0126-generalized-parsing-pt3/stdin/Tests/LinuxMain.swift b/0126-generalized-parsing-pt3/stdin/Tests/LinuxMain.swift
new file mode 100644
index 00000000..a4b5d29f
--- /dev/null
+++ b/0126-generalized-parsing-pt3/stdin/Tests/LinuxMain.swift
@@ -0,0 +1,7 @@
+import XCTest
+
+import stdinTests
+
+var tests = [XCTestCaseEntry]()
+tests += stdinTests.allTests()
+XCTMain(tests)
diff --git a/0126-generalized-parsing-pt3/stdin/Tests/stdinTests/XCTestManifests.swift b/0126-generalized-parsing-pt3/stdin/Tests/stdinTests/XCTestManifests.swift
new file mode 100644
index 00000000..c29f1280
--- /dev/null
+++ b/0126-generalized-parsing-pt3/stdin/Tests/stdinTests/XCTestManifests.swift
@@ -0,0 +1,9 @@
+import XCTest
+
+#if !canImport(ObjectiveC)
+public func allTests() -> [XCTestCaseEntry] {
+ return [
+ testCase(stdinTests.allTests),
+ ]
+}
+#endif
diff --git a/0126-generalized-parsing-pt3/stdin/Tests/stdinTests/stdinTests.swift b/0126-generalized-parsing-pt3/stdin/Tests/stdinTests/stdinTests.swift
new file mode 100644
index 00000000..a441b891
--- /dev/null
+++ b/0126-generalized-parsing-pt3/stdin/Tests/stdinTests/stdinTests.swift
@@ -0,0 +1,47 @@
+import XCTest
+import class Foundation.Bundle
+
+final class stdinTests: XCTestCase {
+ func testExample() throws {
+ // This is an example of a functional test case.
+ // Use XCTAssert and related functions to verify your tests produce the correct
+ // results.
+
+ // Some of the APIs that we use below are available in macOS 10.13 and above.
+ guard #available(macOS 10.13, *) else {
+ return
+ }
+
+ let fooBinary = productsDirectory.appendingPathComponent("stdin")
+
+ let process = Process()
+ process.executableURL = fooBinary
+
+ let pipe = Pipe()
+ process.standardOutput = pipe
+
+ try process.run()
+ process.waitUntilExit()
+
+ let data = pipe.fileHandleForReading.readDataToEndOfFile()
+ let output = String(data: data, encoding: .utf8)
+
+ XCTAssertEqual(output, "Hello, world!\n")
+ }
+
+ /// Returns path to the built products directory.
+ var productsDirectory: URL {
+ #if os(macOS)
+ for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
+ return bundle.bundleURL.deletingLastPathComponent()
+ }
+ fatalError("couldn't find the products directory")
+ #else
+ return Bundle.main.bundleURL
+ #endif
+ }
+
+ static var allTests = [
+ ("testExample", testExample),
+ ]
+}
diff --git a/README.md b/README.md
index 09ea2b35..0d863ce9 100644
--- a/README.md
+++ b/README.md
@@ -128,3 +128,4 @@ This repository is the home of code written on episodes of
1. [Fluently Zipping Parsers](0123-fluently-zipping-parsers)
1. [Generalized Parsing: Part 1](0124-generalized-parsing-pt1)
1. [Generalized Parsing: Part 2](0125-generalized-parsing-pt2)
+1. [Generalized Parsing: Part 3](0126-generalized-parsing-pt3)