Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions exercises/concept/family-recipes/.meta/exemplar.fs
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 = "" }
22 changes: 22 additions & 0 deletions exercises/concept/family-recipes/FamilyRecipes.fs
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"
21 changes: 21 additions & 0 deletions exercises/concept/family-recipes/FamilyRecipes.fsproj
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>
97 changes: 97 additions & 0 deletions exercises/concept/family-recipes/FamilyRecipesTests.fs
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
Loading