-
-
Notifications
You must be signed in to change notification settings - Fork 110
Result concept exercise (initial draft) #1364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 4 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
abe0b6d
Initial draft
blackk-foxx b48a70b
Add ingredient parsing with integer quantities
blackk-foxx c2ea535
Clean up function signatures
blackk-foxx 3c08d02
Simplify & clean up
blackk-foxx 5e9a104
Non-recursive version using fold
blackk-foxx 98dabd9
Refactor for clarity & brevity
blackk-foxx b8751b8
Rename exemplar file
blackk-foxx 448c06d
Modify test content to be non-alcoholic
blackk-foxx bf00b5c
Remove redundant conditional
blackk-foxx e299424
Reduce exercise scope
blackk-foxx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| module FamilyRecipes | ||
|
|
||
| type ParseError = | ||
| | MissingTitle | ||
| | MissingIngredients | ||
| | MissingInstructions | ||
| | InvalidIngredientQuantity | ||
| | MissingIngredientItem | ||
|
|
||
| type Ingredient = { | ||
| Quantity: int | ||
| Item: string | ||
| } | ||
|
|
||
| type Recipe = { | ||
| Title: string | ||
| Ingredients: Ingredient list | ||
| Instructions: string | ||
| } | ||
|
|
||
| type ParseState = | ||
| | ReadingTitle | ||
| | SeekingIngredientsHeading | ||
| | ReadingIngredientsList | ||
| | SeekingInstructionsHeading | ||
| | ReadingInstructions | ||
|
|
||
|
|
||
| let splitOnce (text: string) (separator: char): (string * string) option = | ||
| match text.IndexOf(separator) with | ||
| | -1 -> None | ||
| | index -> Some (text.Substring(0, index), text.Substring(index + 1)) | ||
|
|
||
| let parseIngredient (text: string): Result<Ingredient, ParseError> = | ||
| match splitOnce text ' ' with | ||
| | Some (head, tail) -> | ||
| let success, quantity = System.Int32.TryParse head | ||
| if success then | ||
| Ok { | ||
| Quantity = quantity | ||
| Item = tail | ||
| } | ||
| else | ||
| Error InvalidIngredientQuantity | ||
| | _ -> Error MissingIngredientItem | ||
|
|
||
| let rec parseLines (lines: string array) (state: ParseState) (recipe: Recipe): Result<Recipe, ParseError> = | ||
| match state with | ||
| | ReadingTitle -> parseTitle lines recipe | ||
| | SeekingIngredientsHeading -> parseIngredientsHeading lines recipe | ||
| | ReadingIngredientsList -> parseIngredientsList lines recipe | ||
| | SeekingInstructionsHeading -> parseInstructionsHeading lines recipe | ||
| | ReadingInstructions -> parseReadingInstructions lines recipe | ||
|
|
||
| and parseTitle lines recipe = | ||
| if lines.Length = 0 || lines[0].Length = 0 then | ||
| Error MissingTitle | ||
| else | ||
| parseLines lines[1..] SeekingIngredientsHeading { recipe with Title = lines[0] } | ||
|
|
||
| and parseIngredientsHeading lines recipe = | ||
| if lines.Length = 0 then | ||
| Error MissingIngredients | ||
| else | ||
| let nextState = if lines[0] = "Ingredients:" then ReadingIngredientsList else SeekingIngredientsHeading | ||
| parseLines lines[1..] nextState recipe | ||
|
|
||
| and parseIngredientsList lines recipe = | ||
| if recipe.Ingredients.Length = 0 && (lines.Length = 0 || lines[0].Length = 0) then | ||
| Error MissingIngredients | ||
| elif lines.Length = 0 then | ||
| Error MissingInstructions | ||
| elif lines[0].Length = 0 then | ||
| parseLines lines[1..] SeekingInstructionsHeading recipe | ||
| else | ||
| match parseIngredient lines[0] with | ||
| | Ok ingredient -> parseLines lines[1..] ReadingIngredientsList { | ||
| recipe with Ingredients = recipe.Ingredients @ [ingredient] | ||
| } | ||
| | Error e -> Error e | ||
|
|
||
| and parseInstructionsHeading lines recipe = | ||
| if lines.Length = 0 then | ||
| Error MissingInstructions | ||
| else | ||
| let nextState = if lines[0] = "Instructions:" then ReadingInstructions else SeekingInstructionsHeading | ||
| parseLines lines[1..] nextState recipe | ||
|
|
||
| and parseReadingInstructions lines recipe = | ||
| if lines.Length = 0 then | ||
| if recipe.Instructions.Length = 0 then | ||
| Error MissingInstructions | ||
| else | ||
| Ok { recipe with Instructions = recipe.Instructions } | ||
| else | ||
| let separator = if recipe.Instructions.Length = 0 then "" else "\n" | ||
| parseLines lines[1..] ReadingInstructions { | ||
| recipe with Instructions = recipe.Instructions + separator + lines[0] | ||
| } | ||
|
|
||
| let parse (input: string) : Result<Recipe, ParseError> = | ||
| parseLines (input.Split('\n')) ReadingTitle { Title = ""; Ingredients = []; Instructions = "" } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| module FamilyRecipes | ||
|
|
||
| type ParseError = | ||
| | MissingTitle | ||
| | MissingIngredients | ||
| | MissingInstructions | ||
| | InvalidIngredientQuantity | ||
| | MissingIngredientItem | ||
|
|
||
| type Ingredient = { | ||
| Quantity: int | ||
| Item: string | ||
| } | ||
|
|
||
| type Recipe = { | ||
| Title: string | ||
| Ingredients: Ingredient list | ||
| Instructions: string | ||
| } | ||
|
|
||
| let parse input = | ||
| failwith "Please implement this function" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net9.0</TargetFramework> | ||
|
|
||
| <IsPackable>false</IsPackable> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <Compile Include="FamilyRecipes.fs" /> | ||
| <Compile Include="FamilyRecipesTests.fs" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> | ||
| <PackageReference Include="xunit" Version="2.4.1" /> | ||
| <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> | ||
| <PackageReference Include="FsUnit.xUnit" Version="4.0.4" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| module Tests | ||
|
|
||
| open FsUnit.Xunit | ||
| open Xunit | ||
|
|
||
| open FamilyRecipes | ||
|
|
||
| [<Fact>] | ||
| let ``Error on blank recipe`` () = | ||
| let expected: Result<Recipe, ParseError> = Error MissingTitle | ||
| parse "" |> should equal expected | ||
|
|
||
| [<Fact>] | ||
| let ``Error on title without ingredients or instructions`` () = | ||
| let expected: Result<Recipe, ParseError> = Error MissingIngredients | ||
| parse "foo" |> should equal expected | ||
|
|
||
| [<Fact>] | ||
| let ``Error on title and ingredients heading without ingredients list`` () = | ||
| let expected: Result<Recipe, ParseError> = Error MissingIngredients | ||
| parse "A Title\n\nIngredients:" |> should equal expected | ||
|
|
||
| [<Fact>] | ||
| let ``Error on title and ingredients without instructions`` () = | ||
| let expected: Result<Recipe, ParseError> = Error MissingInstructions | ||
| parse "A Title\n\nIngredients:\n1 ingredient" |> should equal expected | ||
|
|
||
| [<Fact>] | ||
| let ``Error on all required sections but with empty ingredients list`` () = | ||
| let expected: Result<Recipe, ParseError> = Error MissingIngredients | ||
| parse "A Title\n\nIngredients:\n\nInstructions:\nSome instructions" |> should equal expected | ||
|
|
||
| [<Fact>] | ||
| let ``Error on all required sections but missing instructions`` () = | ||
| let expected: Result<Recipe, ParseError> = Error MissingInstructions | ||
| parse "A Title\n\nIngredients:\n1 ingredient\n\nInstructions:" |> should equal expected | ||
|
|
||
| [<Fact>] | ||
| let ``Error on non-numeric ingredient quantity`` () = | ||
| let expected: Result<Recipe, ParseError> = Error InvalidIngredientQuantity | ||
| parse "A Title\n\nIngredients:\nfoo bar\n\nInstructions:\nbuzz" |> should equal expected | ||
|
|
||
| [<Fact>] | ||
| let ``Error on missing ingredient item`` () = | ||
| let expected: Result<Recipe, ParseError> = Error MissingIngredientItem | ||
| parse "A Title\n\nIngredients:\n24\n\nInstructions:\nbuzz" |> should equal expected | ||
|
|
||
|
|
||
| [<Fact>] | ||
| let ``Minimal valid recipe`` () = | ||
| let expected: Result<Recipe, ParseError> = Ok { | ||
| Title = "Glass of Wine" | ||
| Ingredients = [ | ||
| { Quantity = 1; Item = "cup of wine" } | ||
| ] | ||
| Instructions = "Pour wine into wine glass.\n" | ||
| } | ||
| let input = """Glass of Wine | ||
|
|
||
| Ingredients: | ||
| 1 cup of wine | ||
|
|
||
| Instructions: | ||
| Pour wine into wine glass. | ||
| """ | ||
| parse input |> should equal expected | ||
|
|
||
| [<Fact>] | ||
| let ``Valid recipe with multiple ingredients, integer quantities and varying units`` () = | ||
| let expected: Result<Recipe, ParseError> = Ok { | ||
| Title = "Gin and Tonic" | ||
| Ingredients = [ | ||
| { Quantity = 1; Item = "cup tonic water" }; | ||
| { Quantity = 2; Item = "shots of gin" }; | ||
| { Quantity = 5; Item = "cubes of ice" }; | ||
| ] | ||
| Instructions = """Put ice cubes into a glass. | ||
| Stir tonic water and gin in another glass. | ||
| Pour tonic water and gin mixture into glass with ice. | ||
| """ | ||
| } | ||
| let input = """Gin and Tonic | ||
|
|
||
| Ingredients: | ||
| 1 cup tonic water | ||
| 2 shots of gin | ||
| 5 cubes of ice | ||
|
|
||
| Instructions: | ||
| Put ice cubes into a glass. | ||
| Stir tonic water and gin in another glass. | ||
| Pour tonic water and gin mixture into glass with ice. | ||
| """ | ||
| parse input |> should equal expected | ||
|
|
||
| // TODO: (maybe) Test the ability to parse fractional quantities | ||
| // TODO: Organize into tasks |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.