diff --git a/src/Gaze.Test/Gaze.Test.fsproj b/src/Gaze.Test/Gaze.Test.fsproj new file mode 100644 index 0000000..7d919b8 --- /dev/null +++ b/src/Gaze.Test/Gaze.Test.fsproj @@ -0,0 +1,25 @@ + + + + Exe + net6.0 + false + + + + + + + + + + + + + + + + + + + diff --git a/src/Gaze.Test/GazeSuite.fs b/src/Gaze.Test/GazeSuite.fs new file mode 100644 index 0000000..2d80d3b --- /dev/null +++ b/src/Gaze.Test/GazeSuite.fs @@ -0,0 +1,49 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +module GazeSuite + +open Expecto + +[] +let tests = + testList "Gaze Suite" [ + testCase "empty String input" <| fun _ -> + let gaze = Gaze.fromString("") + Expect.isTrue (Gaze.isComplete gaze) "" + Expect.equal (Gaze.peek gaze) None "" + Expect.equal (Gaze.peek gaze) None "" + Expect.isTrue (Gaze.isComplete gaze) "" + + testCase "empty array input" <| fun _ -> + let gaze = Gaze.fromArray([||]) + Expect.isTrue (Gaze.isComplete gaze) "" + Expect.equal (Gaze.peek gaze) None "" + Expect.equal (Gaze.peek gaze) None "" + Expect.isTrue (Gaze.isComplete gaze) "" + + testCase "init Gaze with one value" <| fun _ -> + let gaze = Gaze.fromArray([|'a'|]) + Expect.isFalse (Gaze.isComplete gaze) "" + Expect.equal (Gaze.peek gaze) (Some('a')) "" + Expect.equal (Gaze.peek gaze) (Some('a')) "" + Expect.equal (Gaze.next gaze) (Some('a')) "" + Expect.equal (Gaze.next gaze) None "" + Expect.isTrue (Gaze.isComplete gaze) "" + + testCase "init Gaze with single char String" <| fun _ -> + let gaze = Gaze.fromString("a") + Expect.isFalse (Gaze.isComplete gaze) "" + Expect.equal (Gaze.peek gaze) (Some('a')) "" + Expect.equal (Gaze.peek gaze) (Some('a')) "" + Expect.equal (Gaze.next gaze) (Some('a')) "" + Expect.equal (Gaze.next gaze) None "" + Expect.isTrue (Gaze.isComplete gaze) "" + + testCase "map digit" <| fun _ -> + let gaze = Gaze.fromString("1") + let result = Some(1) + let nibbler = Gaze.map Gaze.next (fun char -> int (string char)) + Expect.equal(Gaze.attempt nibbler gaze) result "" + ] diff --git a/src/Gaze.Test/Main.fs b/src/Gaze.Test/Main.fs new file mode 100644 index 0000000..ba9c2d2 --- /dev/null +++ b/src/Gaze.Test/Main.fs @@ -0,0 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +open Expecto + +[] +let main argv = + Tests.runTestsInAssembly defaultConfig argv diff --git a/src/Gaze.Test/NibblersSuite.fs b/src/Gaze.Test/NibblersSuite.fs new file mode 100644 index 0000000..520ff63 --- /dev/null +++ b/src/Gaze.Test/NibblersSuite.fs @@ -0,0 +1,156 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +module NibblersSuite + +open Expecto + +[] +let tests = testList "Nibbler Tests" [ + testList "Take Suite" [ + testCase "take with single value" <| fun _ -> + let gaze = Gaze.fromString("a") + Expect.equal (Gaze.attempt (Nibblers.take 'a') gaze) (Some('a')) "" + Expect.isTrue (Gaze.isComplete gaze) "" + ] + + testList "Take List Suite" [ + testCase "takeList simple" <| fun _ -> + let gaze = Gaze.fromString("hello, world") + let list = ['h'; 'e'; 'l'; 'l'; 'o'] + Expect.equal (Gaze.attempt (Nibblers.takeList list) gaze) (Some(list)) "" + Expect.equal(Gaze.peek gaze) (Some(',')) "" + ] + + testList "Take String Suite" [ + testCase "takeString simple" <| fun _ -> + let gaze = Gaze.fromString("hello, world") + Expect.equal (Gaze.attempt(Nibblers.takeString "hello") gaze) (Some(['h'; 'e'; 'l'; 'l'; 'o'])) "" + Expect.equal (Gaze.peek gaze) (Some(',')) "" + ] + + testList "Take Cond Suite" [ + testCase "takeCond with a single value" <| fun _ -> + let gaze = Gaze.fromString("a") + Expect.equal + (Gaze.attempt(Nibblers.takeCond (fun c -> c = 'a')) gaze) + (Some('a')) "" + assert(Gaze.isComplete(gaze)) + testCase "takeCond with multiple values" <| fun _ -> + let gaze = Gaze.fromString("cab") + Expect.equal + (Gaze.attempt(Nibblers.takeCond(fun c -> List.contains c ['a'; 'b'; 'c'; 'd'])) gaze) + (Some('c')) "" + Expect.isFalse (Gaze.isComplete gaze) "" + testCase "takeCond match beginning" <| fun _ -> + let gaze = Gaze.fromString("abc123") + Expect.equal + (Gaze.attempt(Nibblers.takeCond(fun c -> List.contains c ['a'; 'b'; 'c'; 'd'])) gaze) + (Some('a')) "" + Expect.isFalse (Gaze.isComplete gaze) "" + testCase "takeCond no match beginning" <| fun _ -> + let gaze = Gaze.fromString("123abc") + Expect.equal + (Gaze.attempt(Nibblers.takeCond(fun c -> List.contains c ['a'; 'b'; 'c'; 'd'])) gaze) + None "" + Expect.isFalse (Gaze.isComplete gaze) "" + ] + + testList "Take While Suite" [ + testCase "takeWhile with a single value" <| fun _ -> + let gaze = Gaze.fromString("a") + Expect.equal (Gaze.attempt(Nibblers.takeWhile(fun c -> c = 'a')) gaze) (Some(['a'])) "" + Expect.isTrue (Gaze.isComplete gaze) "" + testCase "takeWhile with multiple values" <| fun _ -> + let gaze = Gaze.fromString("cab") + Expect.equal + (Gaze.attempt (Nibblers.takeWhile(fun c -> List.contains c ['a'; 'b'; 'c'; 'd'])) gaze) + (Some(['c'; 'a'; 'b'])) "" + Expect.isTrue (Gaze.isComplete gaze) "" + testCase "takeWhile match beginning" <| fun _ -> + let gaze = Gaze.fromString("abc123") + Expect.equal + (Gaze.attempt(Nibblers.takeWhile(fun c -> List.contains c ['a'; 'b'; 'c'; 'd'])) gaze) + (Some(['a'; 'b'; 'c'])) "" + Expect.isFalse (Gaze.isComplete gaze) "" + testCase "takeWhile no match beginning" <| fun _ -> + let gaze = Gaze.fromString("123abc") + Expect.equal + (Gaze.attempt(Nibblers.takeWhile(fun c -> List.contains c ['a'; 'b'; 'c'; 'd'])) gaze) None "" + Expect.isFalse (Gaze.isComplete gaze) "" + ] + + testList "Take While Index Suite" [ + testCase "takeWhileIndex simple" <| fun _ -> + let gaze = Gaze.fromString("abc") + Expect.equal + (Gaze.attempt(Nibblers.takeWhileIndex(fun (c, i) -> (c = 'a' && i = 0) || (c = 'b' && i = 1))) gaze) + (Some(['a'; 'b'])) "" + Expect.isFalse(Gaze.isComplete gaze) "" + ] + + testList "Take Until Suite" [ + testCase "takeUntil simple" <| fun _ -> + let gaze = Gaze.fromString("hello, world") + Expect.equal + (Gaze.attempt (Nibblers.takeUntil (Nibblers.take ',')) gaze) + (Some(['h'; 'e'; 'l'; 'l'; 'o'])) "" + Expect.equal (Gaze.peek gaze) (Some(',')) "" + ] + + testList "Between Suite" [ + testCase "between simple" <| fun _ -> + let gaze = Gaze.fromString("abbbbbc") + Expect.equal + (Gaze.attempt(Nibblers.between 'a' (Nibblers.takeWhile(fun c -> c = 'b')) 'c') gaze) + (Some(['b'; 'b'; 'b'; 'b'; 'b'])) "" + Expect.isTrue (Gaze.isComplete gaze) "" + ] + + testList "Optional Suite" [ + testCase "optional simple" <| fun _ -> + let gaze = Gaze.fromString("a") + Expect.equal + (Gaze.attempt(Nibblers.optional(Gaze.map (Nibblers.take 'b') (fun r -> [r]))) gaze) + (Some([])) "" + Expect.equal (Gaze.peek gaze) (Some('a')) "" + ] + + testList "Repeat Suite" [ + testCase "repeat simple" <| fun _ -> + let gaze = Gaze.fromString("aaaabbbbbc") + Expect.equal(Gaze.attempt(Nibblers.repeat(Nibblers.takeCond(fun c -> c = 'a'))) gaze) (Some(['a'; 'a'; 'a'; 'a'])) "" + Expect.isFalse(Gaze.isComplete gaze) "" + Expect.equal(Gaze.attempt(Nibblers.repeat(Nibblers.takeCond(fun c -> c = 'b'))) gaze) (Some(['b'; 'b'; 'b'; 'b'; 'b'])) "" + Expect.isFalse(Gaze.isComplete gaze) "" + Expect.equal(Gaze.attempt(Nibblers.repeat(Nibblers.takeCond(fun c -> c = 'c'))) gaze) (Some(['c'])) "" + Expect.isTrue(Gaze.isComplete gaze) "" + ] + + testList "Take All Suite" [ + testCase "take all simple" <| fun _ -> + let gaze = Gaze.fromString("aaaabbbbbc") + let takeAs = Nibblers.repeat(Nibblers.takeCond(fun c -> c = 'a')) + let takeBs = Nibblers.repeat(Nibblers.takeCond(fun c -> c = 'b')) + let takeCs = Nibblers.repeat(Nibblers.takeCond(fun c -> c = 'c')) + let takeAll = Nibblers.takeAll [takeAs; takeBs; takeCs] + Expect.equal + (Gaze.attempt takeAll gaze) + (Some([['a'; 'a'; 'a'; 'a']; ['b'; 'b'; 'b'; 'b'; 'b']; ['c']])) "" + Expect.isTrue(Gaze.isComplete gaze) "" + ] + + testList "Take First Suite" [ + testCase "take first simple" <| fun _ -> + let gaze = Gaze.fromString("aaaabbbbbc") + let takeAs = Nibblers.repeat(Nibblers.takeCond(fun c -> c = 'a')) + let takeBs = Nibblers.repeat(Nibblers.takeCond(fun c -> c = 'b')) + let takeCs = Nibblers.repeat(Nibblers.takeCond(fun c -> c = 'c')) + let takeFirst = Nibblers.takeFirst [takeCs; takeBs; takeAs] + Expect.equal + (Gaze.attempt takeFirst gaze) + (Some(['a'; 'a'; 'a'; 'a'])) "" + Expect.isFalse(Gaze.isComplete gaze) "" + ] +] diff --git a/src/Gaze/Gaze.fs b/src/Gaze/Gaze.fs index a133326..8efd8e6 100644 --- a/src/Gaze/Gaze.fs +++ b/src/Gaze/Gaze.fs @@ -11,7 +11,7 @@ type Gaze<'input> = { type Nibbler<'input, 'output> = Gaze<'input> -> 'output option -let private explode (s:string) = +let explode (s:string) = [| for c in s -> c |] /// Create an instance of Gaze that works with a String as input. @@ -50,15 +50,14 @@ let check nibbler gaze = let attempt nibbler gaze = let startOffset = gaze.offset match nibbler gaze with - | Some(res) -> Some(res) - | None -> - gaze.offset <- startOffset - None + | Some(res) -> Some(res) + | None -> + gaze.offset <- startOffset + None let offset gaze = gaze.offset let map nibbler mapper gaze = match attempt nibbler gaze with - | Some(result) -> - Some(mapper(result)) - | None -> None + | Some(result) -> Some(mapper(result)) + | None -> None diff --git a/src/Gaze/Gaze.fsproj b/src/Gaze/Gaze.fsproj new file mode 100644 index 0000000..28efe02 --- /dev/null +++ b/src/Gaze/Gaze.fsproj @@ -0,0 +1,13 @@ + + + + Exe + net6.0 + + + + + + + + diff --git a/src/Gaze/Nibblers.fs b/src/Gaze/Nibblers.fs new file mode 100644 index 0000000..f0036ab --- /dev/null +++ b/src/Gaze/Nibblers.fs @@ -0,0 +1,187 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +module Nibblers + +/// Create a Nibbler that take a single literal value. +/// The literal to take. +/// A Nibbler that takes a single literal. +let take t gaze = + if Gaze.next gaze = Some(t) then + Some(t) + else + None + +/// Create a Nibbler that takes a list of tokens. +/// The list of tokens to take. +/// The newly created Nibbler. +let takeList list gaze = + let length = List.length(list) + let mutable index = 0 + let mutable cont = true + while not (Gaze.isComplete gaze) && cont && index < length do + let next = Gaze.next(gaze) + match next with + | Some value -> + if value = list.Item(index) then + index <- index + 1 + else + cont <- false + | None -> + cont <- false + if cont && index = length then + Some list + else + None + +/// Create a Nibbler that takes a String when working with a Gaze of Chars. +/// This is just a helper function that relies on takeList. +/// The String to match. +/// The newly created Nibbler. +let takeString string = + takeList(Array.toList(Gaze.explode(string))) + +/// Create a Nibbler that accepts input based on a function that recieves the current token +/// and returns a bool. +/// The function used to decide if a token matches. +/// A Nibbler that consumes one item as long as the predicate passes. +let takeCond predicate gaze = + let next = Gaze.peek(gaze) + match next with + | Some(value) when predicate(value) -> + Gaze.next(gaze) |> ignore + Some(value) + | _ -> None + +/// Create a Nibbler that accepts input based on a function that recieves the current token +/// and returns a bool. +/// The function used to decide if a token matches. +/// A Nibbler that consumes input as long as the predicate passes. +let takeWhile predicate gaze = + let mutable cont = true + let mutable results = [] + while cont do + let next = Gaze.peek(gaze) + match next with + | Some value when predicate value -> + Gaze.next(gaze) |> ignore + results <- results @ [value] + | _ -> + cont <- false + if List.length(results) = 0 then + None + else + Some results + +/// Create a Nibbler that accepts input based on a function that recieves the current token +/// with index starting at 0 and returns a bool. +/// The function used to decide if a token matches. +/// A Nibbler that consumes input as long as the predicate passes. +let takeWhileIndex predicate gaze = + let mutable index = 0 + let mutable cont = true + let mutable results = [] + while cont do + let next = Gaze.peek(gaze) + match next with + | Some(value) when predicate(value, index) -> + Gaze.next(gaze) |> ignore + results <- results @ [value] + index <- index + 1 + | _ -> cont <- false + if List.length(results) = 0 then + None + else + Some(results) + +/// Create a Nibbler that consumes input until the given Nibbler succeeds. +/// The Nibbler used to test. +/// The newly created Nibbler. +let takeUntil nibbler gaze = + let mutable results = [] + let mutable cont = true + while cont && not (Gaze.isComplete gaze) do + let res = Gaze.check nibbler gaze + match res with + | Some(_) -> + cont <- false + | None -> + let next = Gaze.next(gaze) + results <- results @ [next.Value] //??? + Some(results) + +/// Create a Nibbler that accepts a start. +/// The starting token. +/// The Nibbler used to decide the matched content. +/// The ending token. +/// A Nibbler that consumes a starting and ending token and returns the content that matches in between. +let between start content last gaze = + if Gaze.next(gaze) = Some(start) then + match Gaze.attempt content gaze with + | Some(result) -> + if Gaze.next(gaze) = Some(last) then + Some(result) + else + None + | None -> None + else + None + +/// Creates a Nibbler that wraps another Nibbler and will never fail but will instead return an empty List. +/// The Nibbler to wrap. +/// The newly created Nibbler. +let optional nibbler gaze = + match Gaze.attempt nibbler gaze with + | Some(res) -> Some(res) + | None -> Some([]) + +let repeat nibbler gaze = + let mutable cont = true + let mutable results = [] + while cont do + match Gaze.attempt nibbler gaze with + | Some(result) -> + results <- results @ [result] + | None -> cont <- false + if results = [] then + None + else + Some(results) + +/// Create a Nibbler that accepts a List of Nibblers and only succeeds if all of the +/// passed in Nibblers succeed in order. +/// A List of nibblers. +/// A List of all of the results from each Nibbler internally grouped in Lists. +let takeAll nibblers gaze = + let mutable results = [] + let mutable nibblerIndex = 0 + while nibblerIndex >= 0 && nibblerIndex < List.length(nibblers) do + let nibbler = nibblers.Item(nibblerIndex) + match Gaze.attempt nibbler gaze with + | Some(result) -> + results <- results @ [result] + nibblerIndex <- nibblerIndex + 1 + | None -> + nibblerIndex <- -1 + if results = [] || nibblerIndex = -1 then + None + else + Some(results) + +/// Create a Nibbler that accepts a List of Nibblers and matches on the first that succeeds. +/// If all fail the created Nibbler will fail as well. +/// A list of Nibblers to check. +/// The newly created Nibbler. +let takeFirst nibblers gaze = + let mutable result = None + let mutable nibblerIndex = 0 + while nibblerIndex >= 0 && nibblerIndex < List.length(nibblers) do + let nibbler = nibblers.Item(nibblerIndex) + match Gaze.attempt nibbler gaze with + | Some(res) -> + result <- Some(res) + nibblerIndex <- -1 + | None -> + nibblerIndex <- nibblerIndex + 1 + result