Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
41 changes: 41 additions & 0 deletions exercises/concept/family-recipes/.meta/Exemplar.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module FamilyRecipes

type ValidationError =
| EmptyList
| InvalidIngredientQuantity
| MissingIngredientItem

let parseInt (text: string) : Result<int, unit> =
let success, value = System.Int32.TryParse text
if success then Ok value else Error ()

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 validateIngredient (text: string): Result<unit, ValidationError> =
match splitOnce text ' ' with
| Some (quantity, item) ->
match parseInt quantity with
| Ok _ ->
if item.Length = 0 then
Error MissingIngredientItem
else
Ok ()
| Error _ -> Error InvalidIngredientQuantity
| _ -> Error MissingIngredientItem

let validate (input: string) : Result<string, ValidationError> =
let nonblankLines =
input.Split('\n')
|> Array.filter (fun l -> l.Length > 0)
if nonblankLines.Length = 0 then Error EmptyList
else
let firstError =
nonblankLines
|> Array.map validateIngredient
|> Array.tryFind (fun r -> r.IsError)
match firstError with
| Some (Error error) -> Error error
| _ -> Ok input
13 changes: 13 additions & 0 deletions exercises/concept/family-recipes/FamilyRecipes.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module FamilyRecipes

type ValidationError =
| EmptyList
| InvalidIngredientQuantity
| MissingIngredientItem

let parseInt (text: string) : Result<int, unit> =
let success, value = System.Int32.TryParse text
if success then Ok value else Error ()
Comment on lines +8 to +10
Copy link
Contributor Author

@blackk-foxx blackk-foxx Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of this pre-written function is to provide the student an opportunity to practice consuming a Result, since the standard library doesn't really provide any. I'm not really happy with it, though -- the student could decide to ignore it or remove it altogether. I guess it could be moved to a separate module that is read-only for the user, and maybe the tests could be designed (somehow) to ensure that it is called. But that seems overly pedantic to me.

Another idea is to introduce a mock readFile API that returns a Result, and the tests could be structured so that the given input is a fake filename, and the readFile API would return either an Error, or Ok with the ingredients text for the given test case.

Other thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ehm, I'm not entirely. I'll try and find some time this week to do a proposal for some further changes, okay?


let validate 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>
46 changes: 46 additions & 0 deletions exercises/concept/family-recipes/FamilyRecipesTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module Tests

open FsUnit.Xunit
open Xunit

open FamilyRecipes

[<Fact>]
let ``Error on blank list`` () =
let expected: Result<string, ValidationError> = Error EmptyList
validate "" |> should equal expected

[<Fact>]
let ``Error on blank line`` () =
let expected: Result<string, ValidationError> = Error EmptyList
validate "\n" |> should equal expected

[<Fact>]
let ``Error on non-numeric ingredient quantity`` () =
let expected: Result<string, ValidationError> = Error InvalidIngredientQuantity
validate "foo bar" |> should equal expected

[<Fact>]
let ``Error on missing ingredient item`` () =
let expected: Result<string, ValidationError> = Error MissingIngredientItem
validate "24 " |> should equal expected

[<Fact>]
let ``Minimal valid list`` () =
let input = """1 cup rice"""
let expected: Result<string, ValidationError> = Ok input
validate input |> should equal expected

[<Fact>]
let ``Valid list with multiple ingredients`` () =
let input = """4 ounces of oatmeal
12 ounces of water
1 tablespoon of honey"""
let expected: Result<string, ValidationError> = Ok input
validate input |> should equal expected

[<Fact>]
let ``Blank lines within valid list are OK`` () =
let input = "1 foo\n2 bar"
let expected: Result<string, ValidationError> = Ok input
validate input |> should equal expected
Loading