diff --git a/Sources/Parsing/ParserPrinters/Take.swift b/Sources/Parsing/ParserPrinters/Take.swift new file mode 100644 index 0000000000..1f709539ac --- /dev/null +++ b/Sources/Parsing/ParserPrinters/Take.swift @@ -0,0 +1,80 @@ +/// A parser that returns input from the until the `terminator` `Parser` +/// matches. This provides a method of "lazy" (as opposed to "greedy") consumption of the input. +/// +/// ```swift +/// enum ParkType { +/// case park +/// case world +/// } +/// +/// let lineParser: some Parser = Take { +/// Prefix(0...).map(.string) +/// } upTo: { +/// OneOf { +/// "Park".map { ParkType.park } +/// "World".map { ParkType.world } +/// } +/// } +/// +/// var input = "Jurrasic World"[...] +/// let parsed = try line.parse(&input) // ("Jurrasic ", .world) +/// ``` +public struct Take: Parser + where Input.SubSequence == Input, Terminator.Input == Input, Taken.Input == Input { + public let taken: Taken + public let terminator: Terminator + + @inlinable + public init( + @ParserBuilder _ taken: () -> Taken, + @ParserBuilder upTo terminator: () -> Terminator + ) { + self.taken = taken() + self.terminator = terminator() + } + + @inlinable + @inline(__always) + public func parse(_ input: inout Input) throws -> (Taken.Output, Terminator.Output) { + let original = input + + var currentIndex = input.startIndex + while currentIndex <= input.endIndex { + let terminatorOutput: Terminator.Output + var takenInput: Input + + do { + var test = input[currentIndex...] + terminatorOutput = try terminator.parse(&test) + input = test + takenInput = original[..: Parser +where Input.SubSequence == Input, Upstream.Input == Input +{ + public let terminator: Upstream + + @inlinable + public init( + _ terminator: Upstream + ) { + self.terminator = terminator + } + + @inlinable + public init( + @ParserBuilder _ terminator: () -> Upstream + ) { + self.terminator = terminator() + } + + @inlinable + @inline(__always) + public func parse(_ input: inout Input) throws -> Input { + let original = input + + var currentIndex = input.startIndex + while currentIndex <= input.endIndex { + do { + var test = input[currentIndex...] + let _ = try terminator.parse(&test) + input = original[currentIndex...] + return original[.. = Take { + Int.parser() + } upTo: { + "." + } + + var input = "123.456"[...] + XCTAssertEqual(123, try parser.parse(&input).0) + } + + func testUnterminated() throws { + let parser: some Parser = Take { + Int.parser() + } upTo: { + "." + } + + var input = "123456"[...] + + XCTAssertThrowsError(try parser.parse(&input)) + } + + func testTakeStringAndInt() throws { + let parser: some Parser = Take { + Prefix(0...).map(.string) + } upTo: { + "456".map { 456 } + } + + var input = "123456"[...] + let output = try parser.parse(&input) + XCTAssertEqual("123", output.0) + XCTAssertEqual(456, output.1) + } + + func testTakeUpToEnd() throws { + let parser: some Parser = Take { + Prefix(0...) + } upTo: { + End() + } + + var input = "123"[...] + XCTAssertEqual("123", try parser.parse(&input).0) + XCTAssertEqual("", input) + } + + func testTakeCantParseUpTo() throws { + let parser: some Parser = Take { + Int.parser() + } upTo: { + End() + } + + var input = "123abc"[...] + XCTAssertThrowsError(try parser.parse(&input)) + } + + func testComplextInitMap() throws { + enum Example: Equatable { + case a + case b + } + + struct ComplexType: Equatable { + let string: String + let number: Int + let example: Example? + } + + let parser: some Parser = Parse(ComplexType.init(string:number:example:)) { + Take { + Prefix(0...).map(.string) + } upTo: { + Int.parser() + } + Optionally { + OneOf { + "a".map { Example.a } + "b".map { Example.b } + } + } + } + + var input = "Hello1b"[...] + XCTAssertEqual(ComplexType(string: "Hello", number: 1, example: .b), try parser.parse(&input)) + XCTAssertTrue(input.isEmpty) + } +} diff --git a/Tests/ParsingTests/TakeUpToTests.swift b/Tests/ParsingTests/TakeUpToTests.swift new file mode 100644 index 0000000000..8e1877f1fb --- /dev/null +++ b/Tests/ParsingTests/TakeUpToTests.swift @@ -0,0 +1,54 @@ +import Parsing +import XCTest + +final class TakeUpToTests: XCTestCase { + func testSimple() throws { + let parser = TakeUpTo { + "." + } + + var input = "123.456"[...] + XCTAssertEqual("123", try parser.parse(&input)) + XCTAssertEqual(".456", input) + } + + func testUnterminated() throws { + let parser = TakeUpTo { + "." + } + + var input = "123456"[...] + + XCTAssertThrowsError(try parser.parse(&input)) + } + + func testTakeUpToString() throws { + let parser = TakeUpTo { + "456".map { 456 } + } + var input = "123456"[...] + let output = try parser.parse(&input) + XCTAssertEqual("123", output) + XCTAssertEqual("456", input) + } + + func testTakeSubstring() throws { + let parser = TakeUpTo { + "456".map { 456 } + } + var input = "123456"[...] + let output = try parser.parse(&input) + XCTAssertEqual("123", output) + XCTAssertEqual("456", input) + } + + func testTakeUpToEnd() throws { + let parser = TakeUpTo { + End() + } + + var input = "123"[...] + XCTAssertEqual("123", try parser.parse(&input)) + XCTAssertTrue(input.isEmpty) + } +}