From bb5e4eb5959bc0b20dd9e7701cb70a9b1e8c5051 Mon Sep 17 00:00:00 2001 From: Todd Schwartz Date: Wed, 4 Feb 2026 16:27:47 -0800 Subject: [PATCH 1/6] Initial version --- .../password-checker/.meta/Exemplar.fs | 31 +++++++++ .../password-checker/PasswordChecker.fs | 14 ++++ .../password-checker/PasswordChecker.fsproj | 21 ++++++ .../password-checker/PasswordCheckerTests.fs | 69 +++++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 exercises/concept/password-checker/.meta/Exemplar.fs create mode 100644 exercises/concept/password-checker/PasswordChecker.fs create mode 100644 exercises/concept/password-checker/PasswordChecker.fsproj create mode 100644 exercises/concept/password-checker/PasswordCheckerTests.fs diff --git a/exercises/concept/password-checker/.meta/Exemplar.fs b/exercises/concept/password-checker/.meta/Exemplar.fs new file mode 100644 index 000000000..38eb47dde --- /dev/null +++ b/exercises/concept/password-checker/.meta/Exemplar.fs @@ -0,0 +1,31 @@ +module PasswordChecker + +type PasswordRule = + | AtLeast12Characters + | AtLeastOneUppercaseLetter + | AtLeastOneLowercaseLetter + | AtLeastOneDigit + | AtLeastOneSymbol + +let checkPassword (password: string) : Result = + if password.Length < 12 then + Error AtLeast12Characters + elif password |> String.exists System.Char.IsUpper |> not then + Error AtLeastOneUppercaseLetter + elif password |> String.exists System.Char.IsLower |> not then + Error AtLeastOneLowercaseLetter + elif password |> String.exists System.Char.IsDigit |> not then + Error AtLeastOneDigit + elif password |> String.exists (fun c -> "!@#$%^&*".Contains c) |> not then + Error AtLeastOneSymbol + else Ok password + +let getStatusMessage (result: Result) : string = + let preamble = "Error: does not have at least " + match result with + | Error AtLeast12Characters -> preamble + "12 characters" + | Error AtLeastOneUppercaseLetter -> preamble + "one uppercase letter" + | Error AtLeastOneLowercaseLetter -> preamble + "one lowercase letter" + | Error AtLeastOneDigit -> preamble + "one digit" + | Error AtLeastOneSymbol -> preamble + "one symbol" + | Ok _ -> "OK" diff --git a/exercises/concept/password-checker/PasswordChecker.fs b/exercises/concept/password-checker/PasswordChecker.fs new file mode 100644 index 000000000..11e95cc21 --- /dev/null +++ b/exercises/concept/password-checker/PasswordChecker.fs @@ -0,0 +1,14 @@ +module PasswordChecker + +type PasswordRule = + | AtLeast12Characters + | AtLeastOneUppercaseLetter + | AtLeastOneLowercaseLetter + | AtLeastOneDigit + | AtLeastOneSymbol + +let checkPassword (password: string) : Result = + failwith "Please implement this function" + +let getStatusMessage (result: Result) : string = + failwith "Please implement this function" diff --git a/exercises/concept/password-checker/PasswordChecker.fsproj b/exercises/concept/password-checker/PasswordChecker.fsproj new file mode 100644 index 000000000..31900f329 --- /dev/null +++ b/exercises/concept/password-checker/PasswordChecker.fsproj @@ -0,0 +1,21 @@ + + + + net9.0 + + false + + + + + + + + + + + + + + + diff --git a/exercises/concept/password-checker/PasswordCheckerTests.fs b/exercises/concept/password-checker/PasswordCheckerTests.fs new file mode 100644 index 000000000..b9f6f0d9e --- /dev/null +++ b/exercises/concept/password-checker/PasswordCheckerTests.fs @@ -0,0 +1,69 @@ +module Tests + +open FsUnit.Xunit +open Xunit + +open PasswordChecker + +[] +let ``Error on blank string`` () = + let expected: Result = Error AtLeast12Characters + checkPassword "" |> should equal expected + +[] +let ``Error when password too short`` () = + let expected: Result = Error AtLeast12Characters + checkPassword "@bcd3fghijK" |> should equal expected + +[] +let ``Error when password has no uppercase letters`` () = + let expected: Result = Error AtLeastOneUppercaseLetter + checkPassword "@bcd3fghijkl" |> should equal expected + +[] +let ``Error when password has no lowercase letters`` () = + let expected: Result = Error AtLeastOneLowercaseLetter + checkPassword "@BCD3FGHIJKL" |> should equal expected + +[] +let ``Error when password has no digits`` () = + let expected: Result = Error AtLeastOneDigit + checkPassword "@bcdefghijkL" |> should equal expected + +[] +let ``Error when password has no symbols`` () = + let expected: Result = Error AtLeastOneSymbol + checkPassword "abcd3fghijkL" |> should equal expected + +[] +[] +[] +[] +[] +let ``Ok when password is good`` (password: string) = + let expected: Result = Ok password + checkPassword password |> should equal expected + +[] +let ``Insufficient length error message`` () = + getStatusMessage (Error AtLeast12Characters) |> should equal "Error: does not have at least 12 characters" + +[] +let ``Missing uppercase error message`` () = + getStatusMessage (Error AtLeastOneUppercaseLetter) |> should equal "Error: does not have at least one uppercase letter" + +[] +let ``Missing lowercase error message`` () = + getStatusMessage (Error AtLeastOneLowercaseLetter) |> should equal "Error: does not have at least one lowercase letter" + +[] +let ``Missing digit error message`` () = + getStatusMessage (Error AtLeastOneDigit) |> should equal "Error: does not have at least one digit" + +[] +let ``Missing symbol error message`` () = + getStatusMessage (Error AtLeastOneSymbol) |> should equal "Error: does not have at least one symbol" + +[] +let ``OK message`` () = + getStatusMessage (Ok "foo") |> should equal "OK" From db1f99a984eb370be171a32284f4c598e9593c78 Mon Sep 17 00:00:00 2001 From: Todd Schwartz Date: Tue, 10 Feb 2026 15:36:13 -0800 Subject: [PATCH 2/6] Add missing docs & infrastructure --- concepts/results/.meta/config.json | 8 +++ concepts/results/about.md | 68 +++++++++++++++++++ concepts/results/introduction.md | 47 +++++++++++++ concepts/results/links.json | 6 ++ config.json | 12 ++++ .../concept/password-checker/.docs/hints.md | 0 .../password-checker/.docs/instructions.md | 28 ++++++++ .../password-checker/.docs/introduction.md | 47 +++++++++++++ .../password-checker/.meta/Exemplar.fs | 4 ++ .../password-checker/.meta/config.json | 20 ++++++ .../concept/password-checker/.meta/design.md | 19 ++++++ .../password-checker/PasswordChecker.fs | 4 ++ .../password-checker/PasswordCheckerTests.fs | 17 +++-- 13 files changed, 275 insertions(+), 5 deletions(-) create mode 100644 concepts/results/.meta/config.json create mode 100644 concepts/results/about.md create mode 100644 concepts/results/introduction.md create mode 100644 concepts/results/links.json create mode 100644 exercises/concept/password-checker/.docs/hints.md create mode 100644 exercises/concept/password-checker/.docs/instructions.md create mode 100644 exercises/concept/password-checker/.docs/introduction.md create mode 100644 exercises/concept/password-checker/.meta/config.json create mode 100644 exercises/concept/password-checker/.meta/design.md diff --git a/concepts/results/.meta/config.json b/concepts/results/.meta/config.json new file mode 100644 index 000000000..b6ed7688a --- /dev/null +++ b/concepts/results/.meta/config.json @@ -0,0 +1,8 @@ +{ + "blurb": "Learn how to handle errors in a type-safe manner with the Result type", + "authors": [ + "blackk-foxx" + ], + "contributors": [ + ] +} \ No newline at end of file diff --git a/concepts/results/about.md b/concepts/results/about.md new file mode 100644 index 000000000..c06af92a2 --- /dev/null +++ b/concepts/results/about.md @@ -0,0 +1,68 @@ +# About + +The `Result` type makes it possible for a function to return a single value indicating all of the following things: +- Whether the operation succeeded or failed +- On success, the resulting value of the operation +- On failure, the reason for the failure + +With other programming languages that don't support something like a `Result` type, it is common to find the following patterns to accomplish the aforementioned goals: +- A function returning a numeric code indicating success or the reason for the failure and requiring an output parameter to accept the value on success. +- A function returning the value on success or NULL on error, and then a different function to get the error code indicating the reason for the failure. + +Another common error-handling mechanism is the exception, which abruptly breaks out of the current function and +transfers control to the first handler in the call stack when a failure occurs. +If no handler "catches" the exception, then the program aborts. + +## Benefits of using the `Result` type + +* __Compile-time safety__: By making the success or failure of a function call explicit in the type system, the compiler can ensure that calling code handles all of the failure cases, preventing a large category of bugs that could occur with nulls. + +* __Run-time safety__: Instead of using `null`, the `Result` type uses `Error` represent a failure, making it impossible for a `NullReferenceException` to occur. + +* __Explicit error handling__: Code that calls a function returning a `Result` must acknowledge that the +call can fail; calling code must handle such failures intentionally. There are oo silent failures and no surprise exceptions. + +* __Predictable control flow__: A `Result` is returned just like any other value; it does not jump out of the call stack like an exception. You always know where errors originate and how they propagate. + +* __Improved readability and maintainability__: By returning a `Result`, a function's signature clearly indicates the expected behavior on success and failure. + +## Usage + +The `Result` type is a generic type containing two underlying types: +- The type of the result on a successful operation +- The type of error on a failure + +A `Result` value is either `Ok ` representing a successful result, or `Error ` representing a failure. + +The following function demonstrates how to create a `Result` value: + +```fsharp +let validateName (name: string) : Result = + match name with + | null -> Error "Name not found." + | "" -> Error "Name is empty." + | _ -> Ok name +``` + +## Reading the content of a `Result` value + +Consider the following function signature: + +```fsharp +type FileOpenError = +| NotFound +| AccessDenied +| FileLocked + +let openFile (filename: string) : Result +``` + +Code that calls the `openFile` function can use pattern matching to handle the success and failure cases, as in the following example: + +```fsharp +match openFile(filename) with +| Ok handle -> doSomethingWithFile(handle) +| Error NotFound -> printfn $"Error: file {filename} was not found." +| Error AccessDenied -> printfn $"Error: you do not have permission to open the file {filename}." +| Error FileLocked -> printfn $"Error: file {filename} is already in use." +``` diff --git a/concepts/results/introduction.md b/concepts/results/introduction.md new file mode 100644 index 000000000..061d62167 --- /dev/null +++ b/concepts/results/introduction.md @@ -0,0 +1,47 @@ +# Introduction + +The `Result` type makes it possible for a function to return a single value indicating all of the following things: +- Whether the operation succeeded or failed +- On success, the resulting value of the operation +- On failure, the reason for the failure + +## Usage + +The `Result` type is a generic type containing two underlying types: +- The type of the result on a successful operation +- The type of error on a failure + +A `Result` value is either `Ok ` representing a successful result, or `Error ` representing a failure. + +The following function demonstrates how to create a `Result` value: + +```fsharp +let validateName (name: string) : Result = + match name with + | null -> Error "Name not found." + | "" -> Error "Name is empty." + | _ -> Ok name +``` + +## Reading the content of a `Result` value + +Consider the following function signature: + +```fsharp +type FileOpenError = +| NotFound +| AccessDenied +| FileLocked + +let openFile (filename: string) : Result +``` + +Code that calls the `openFile` function can use pattern matching to handle the success and failure cases, as in the following example: + +```fsharp +match openFile(filename) with +| Ok handle -> doSomethingWithFile(handle) +| Error NotFound -> printfn $"Error: file {filename} was not found." +| Error AccessDenied -> printfn $"Error: you do not have permission to open the file {filename}." +| Error FileLocked -> printfn $"Error: file {filename} is already in use." +``` diff --git a/concepts/results/links.json b/concepts/results/links.json new file mode 100644 index 000000000..892487c5b --- /dev/null +++ b/concepts/results/links.json @@ -0,0 +1,6 @@ +[ + { + "url": "https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/results", + "description": "F# language reference on the Result type" + } +] diff --git a/config.json b/config.json index c95b77ec4..1e5347ee7 100644 --- a/config.json +++ b/config.json @@ -213,6 +213,18 @@ "records", "tuples" ] + }, + { + "slug": "password-checker", + "name": "Password Checker", + "uuid": "a5a67ac7-df9f-48ea-aeb8-2af3738f0577", + "concepts": [ + "results" + ], + "prerequisites": [ + "basics", + "pattern-matching" + ] } ], "practice": [ diff --git a/exercises/concept/password-checker/.docs/hints.md b/exercises/concept/password-checker/.docs/hints.md new file mode 100644 index 000000000..e69de29bb diff --git a/exercises/concept/password-checker/.docs/instructions.md b/exercises/concept/password-checker/.docs/instructions.md new file mode 100644 index 000000000..ee368d758 --- /dev/null +++ b/exercises/concept/password-checker/.docs/instructions.md @@ -0,0 +1,28 @@ +# Instructions + +Your task is to create a password checker. +A password checker validates a user's proposed password to ensure that it meets a set of requirements defined by the organization that controls access to the given resource. + +For this exercise, the password requirements are: +* Must have 12 or more characters +* Must have at least one uppercase letter +* Must have at least one lowercase letter +* Must have at least one digit +* Must have at least one symbol in the set !@#$%^&* + +Your solution must use a `Result` to encapsulate the success or failure status. +For the success case, the `Result` must convey the validated password as a string. +For the failure case, the `Result` must convey the rule that was violated in the failure case. + +~~~~exercism/note +For this exercise, the password checker will be simplistic -- it will indicate only when a single rule has been violated. +A subsequent exercise will explore a more realistic password checker that can indicate when multiple rules have been violated at the same time. +~~~~ + +## 1. Implement the `checkPassword` function + +The `checkPassword` function checks the given password against the aforementioned rules. On failure, it indicates the rule that was violated by encapsulating one of the `PasswordRule` values within the result value. + +## 2. Implement the ``getStatusMessage` function + +The `getStatusMessage` function returns a string containing a human-readable message indicating the meaning of the result returned from `checkPassword`. diff --git a/exercises/concept/password-checker/.docs/introduction.md b/exercises/concept/password-checker/.docs/introduction.md new file mode 100644 index 000000000..061d62167 --- /dev/null +++ b/exercises/concept/password-checker/.docs/introduction.md @@ -0,0 +1,47 @@ +# Introduction + +The `Result` type makes it possible for a function to return a single value indicating all of the following things: +- Whether the operation succeeded or failed +- On success, the resulting value of the operation +- On failure, the reason for the failure + +## Usage + +The `Result` type is a generic type containing two underlying types: +- The type of the result on a successful operation +- The type of error on a failure + +A `Result` value is either `Ok ` representing a successful result, or `Error ` representing a failure. + +The following function demonstrates how to create a `Result` value: + +```fsharp +let validateName (name: string) : Result = + match name with + | null -> Error "Name not found." + | "" -> Error "Name is empty." + | _ -> Ok name +``` + +## Reading the content of a `Result` value + +Consider the following function signature: + +```fsharp +type FileOpenError = +| NotFound +| AccessDenied +| FileLocked + +let openFile (filename: string) : Result +``` + +Code that calls the `openFile` function can use pattern matching to handle the success and failure cases, as in the following example: + +```fsharp +match openFile(filename) with +| Ok handle -> doSomethingWithFile(handle) +| Error NotFound -> printfn $"Error: file {filename} was not found." +| Error AccessDenied -> printfn $"Error: you do not have permission to open the file {filename}." +| Error FileLocked -> printfn $"Error: file {filename} is already in use." +``` diff --git a/exercises/concept/password-checker/.meta/Exemplar.fs b/exercises/concept/password-checker/.meta/Exemplar.fs index 38eb47dde..3c4373d7c 100644 --- a/exercises/concept/password-checker/.meta/Exemplar.fs +++ b/exercises/concept/password-checker/.meta/Exemplar.fs @@ -7,6 +7,9 @@ type PasswordRule = | AtLeastOneDigit | AtLeastOneSymbol +/// Validate the given password against the rules defined in `PasswordRule`. If it meets all +/// of the rules, return a result indicating success; otherwise return a result indicating +/// failure and the rule that was violated. let checkPassword (password: string) : Result = if password.Length < 12 then Error AtLeast12Characters @@ -20,6 +23,7 @@ let checkPassword (password: string) : Result = Error AtLeastOneSymbol else Ok password +/// Return a human-readable message indicating the meaning of the given result value. let getStatusMessage (result: Result) : string = let preamble = "Error: does not have at least " match result with diff --git a/exercises/concept/password-checker/.meta/config.json b/exercises/concept/password-checker/.meta/config.json new file mode 100644 index 000000000..a13a06700 --- /dev/null +++ b/exercises/concept/password-checker/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "blackk-foxx" + ], + "files": { + "solution": [ + "PasswordChecker.fs" + ], + "test": [ + "PasswordCheckerTests.fs" + ], + "exemplar": [ + ".meta/Exemplar.fs" + ], + "invalidator": [ + "PasswordChecker.fsproj" + ] + }, + "blurb": "Learn how to use the Result type to convey success/failure results" +} diff --git a/exercises/concept/password-checker/.meta/design.md b/exercises/concept/password-checker/.meta/design.md new file mode 100644 index 000000000..c6b1281ce --- /dev/null +++ b/exercises/concept/password-checker/.meta/design.md @@ -0,0 +1,19 @@ +# Design + +## Goal + +The goal of this exercise is to teach students about success/failure values enabled by the `Result` type and what you can do with them. + +## Learning objectives + +- Know of the existence of the `Result` type. +- Know how to create a `Result` value. +- Know how to pattern match on the success and failure cases conveyed in a `Result` value. + +## Out of scope + +- The generic concept of pattern matching; this exercise focuses pattern matching with `Result` patterns. + +## Concepts + +- `results` diff --git a/exercises/concept/password-checker/PasswordChecker.fs b/exercises/concept/password-checker/PasswordChecker.fs index 11e95cc21..32f473140 100644 --- a/exercises/concept/password-checker/PasswordChecker.fs +++ b/exercises/concept/password-checker/PasswordChecker.fs @@ -7,8 +7,12 @@ type PasswordRule = | AtLeastOneDigit | AtLeastOneSymbol +/// Validate the given password against the rules defined in `PasswordRule`. If it meets all +/// of the rules, return a result indicating success; otherwise return a result indicating +/// failure and the rule that was violated. let checkPassword (password: string) : Result = failwith "Please implement this function" +/// Return a human-readable message indicating the meaning of the given result value. let getStatusMessage (result: Result) : string = failwith "Please implement this function" diff --git a/exercises/concept/password-checker/PasswordCheckerTests.fs b/exercises/concept/password-checker/PasswordCheckerTests.fs index b9f6f0d9e..c12157bbf 100644 --- a/exercises/concept/password-checker/PasswordCheckerTests.fs +++ b/exercises/concept/password-checker/PasswordCheckerTests.fs @@ -6,31 +6,31 @@ open Xunit open PasswordChecker [] -let ``Error on blank string`` () = - let expected: Result = Error AtLeast12Characters - checkPassword "" |> should equal expected - -[] +[] let ``Error when password too short`` () = let expected: Result = Error AtLeast12Characters checkPassword "@bcd3fghijK" |> should equal expected [] +[] let ``Error when password has no uppercase letters`` () = let expected: Result = Error AtLeastOneUppercaseLetter checkPassword "@bcd3fghijkl" |> should equal expected [] +[] let ``Error when password has no lowercase letters`` () = let expected: Result = Error AtLeastOneLowercaseLetter checkPassword "@BCD3FGHIJKL" |> should equal expected [] +[] let ``Error when password has no digits`` () = let expected: Result = Error AtLeastOneDigit checkPassword "@bcdefghijkL" |> should equal expected [] +[] let ``Error when password has no symbols`` () = let expected: Result = Error AtLeastOneSymbol checkPassword "abcd3fghijkL" |> should equal expected @@ -40,30 +40,37 @@ let ``Error when password has no symbols`` () = [] [] [] +[] let ``Ok when password is good`` (password: string) = let expected: Result = Ok password checkPassword password |> should equal expected [] +[] let ``Insufficient length error message`` () = getStatusMessage (Error AtLeast12Characters) |> should equal "Error: does not have at least 12 characters" [] +[] let ``Missing uppercase error message`` () = getStatusMessage (Error AtLeastOneUppercaseLetter) |> should equal "Error: does not have at least one uppercase letter" [] +[] let ``Missing lowercase error message`` () = getStatusMessage (Error AtLeastOneLowercaseLetter) |> should equal "Error: does not have at least one lowercase letter" [] +[] let ``Missing digit error message`` () = getStatusMessage (Error AtLeastOneDigit) |> should equal "Error: does not have at least one digit" [] +[] let ``Missing symbol error message`` () = getStatusMessage (Error AtLeastOneSymbol) |> should equal "Error: does not have at least one symbol" [] +[] let ``OK message`` () = getStatusMessage (Ok "foo") |> should equal "OK" From 6c3630222249b2d75f6b35fbcd8ca6c5cde6d196 Mon Sep 17 00:00:00 2001 From: Todd Schwartz Date: Tue, 10 Feb 2026 15:52:31 -0800 Subject: [PATCH 3/6] Update results array; correct code snippets --- concepts/results/about.md | 4 ++-- concepts/results/introduction.md | 4 ++-- config.json | 5 +++++ exercises/concept/password-checker/.docs/introduction.md | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/concepts/results/about.md b/concepts/results/about.md index c06af92a2..5918eb204 100644 --- a/concepts/results/about.md +++ b/concepts/results/about.md @@ -46,7 +46,7 @@ let validateName (name: string) : Result = ## Reading the content of a `Result` value -Consider the following function signature: +Consider the following type definition and function signature: ```fsharp type FileOpenError = @@ -54,7 +54,7 @@ type FileOpenError = | AccessDenied | FileLocked -let openFile (filename: string) : Result +let openFile (filename: string) : Result = ``` Code that calls the `openFile` function can use pattern matching to handle the success and failure cases, as in the following example: diff --git a/concepts/results/introduction.md b/concepts/results/introduction.md index 061d62167..4e645b905 100644 --- a/concepts/results/introduction.md +++ b/concepts/results/introduction.md @@ -25,7 +25,7 @@ let validateName (name: string) : Result = ## Reading the content of a `Result` value -Consider the following function signature: +Consider the following type definition and function signature: ```fsharp type FileOpenError = @@ -33,7 +33,7 @@ type FileOpenError = | AccessDenied | FileLocked -let openFile (filename: string) : Result +let openFile (filename: string) : Result = ``` Code that calls the `openFile` function can use pattern matching to handle the success and failure cases, as in the following example: diff --git a/config.json b/config.json index 1e5347ee7..645771f06 100644 --- a/config.json +++ b/config.json @@ -2378,6 +2378,11 @@ "slug": "recursion", "name": "Recursion" }, + { + "uuid": "c25353f4-9fc1-4fd9-978b-a7449af4d584", + "slug": "results", + "name": "Results" + }, { "uuid": "8a3e23fd-aa42-42c3-9dbd-c26159fd6774", "slug": "strings", diff --git a/exercises/concept/password-checker/.docs/introduction.md b/exercises/concept/password-checker/.docs/introduction.md index 061d62167..4e645b905 100644 --- a/exercises/concept/password-checker/.docs/introduction.md +++ b/exercises/concept/password-checker/.docs/introduction.md @@ -25,7 +25,7 @@ let validateName (name: string) : Result = ## Reading the content of a `Result` value -Consider the following function signature: +Consider the following type definition and function signature: ```fsharp type FileOpenError = @@ -33,7 +33,7 @@ type FileOpenError = | AccessDenied | FileLocked -let openFile (filename: string) : Result +let openFile (filename: string) : Result = ``` Code that calls the `openFile` function can use pattern matching to handle the success and failure cases, as in the following example: From 578eb926f2bcb444d05780d53779a246e4024e1b Mon Sep 17 00:00:00 2001 From: Todd Schwartz Date: Tue, 10 Feb 2026 16:05:42 -0800 Subject: [PATCH 4/6] Correct formatting --- concepts/results/.meta/config.json | 2 +- config.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/concepts/results/.meta/config.json b/concepts/results/.meta/config.json index b6ed7688a..60cce2b46 100644 --- a/concepts/results/.meta/config.json +++ b/concepts/results/.meta/config.json @@ -5,4 +5,4 @@ ], "contributors": [ ] -} \ No newline at end of file +} diff --git a/config.json b/config.json index 645771f06..27a7be4f2 100644 --- a/config.json +++ b/config.json @@ -2379,9 +2379,9 @@ "name": "Recursion" }, { - "uuid": "c25353f4-9fc1-4fd9-978b-a7449af4d584", - "slug": "results", - "name": "Results" + "uuid": "c25353f4-9fc1-4fd9-978b-a7449af4d584", + "slug": "results", + "name": "Results" }, { "uuid": "8a3e23fd-aa42-42c3-9dbd-c26159fd6774", From 5d2949b61791e57ea572ca241177035054dcad30 Mon Sep 17 00:00:00 2001 From: Todd Schwartz Date: Tue, 10 Feb 2026 16:07:15 -0800 Subject: [PATCH 5/6] Correct formatting --- config.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config.json b/config.json index 27a7be4f2..49a0de676 100644 --- a/config.json +++ b/config.json @@ -219,11 +219,11 @@ "name": "Password Checker", "uuid": "a5a67ac7-df9f-48ea-aeb8-2af3738f0577", "concepts": [ - "results" + "results" ], "prerequisites": [ - "basics", - "pattern-matching" + "basics", + "pattern-matching" ] } ], From 07b001bf659972e3d52099f157261429e4d896c6 Mon Sep 17 00:00:00 2001 From: Todd Schwartz Date: Fri, 13 Feb 2026 16:02:17 -0800 Subject: [PATCH 6/6] Address review feedback --- concepts/results/about.md | 14 +++++-- concepts/results/introduction.md | 12 ++++-- .../password-checker/.docs/instructions.md | 10 +++++ .../password-checker/.docs/introduction.md | 10 +++-- .../password-checker/.meta/Exemplar.fs | 40 +++++++++---------- .../password-checker/PasswordChecker.fs | 20 +++++----- .../password-checker/PasswordChecker.fsproj | 1 + .../password-checker/PasswordCheckerTests.fs | 25 ++++++------ 8 files changed, 79 insertions(+), 53 deletions(-) diff --git a/concepts/results/about.md b/concepts/results/about.md index 5918eb204..e001b3574 100644 --- a/concepts/results/about.md +++ b/concepts/results/about.md @@ -29,10 +29,12 @@ call can fail; calling code must handle such failures intentionally. There are ## Usage The `Result` type is a generic type containing two underlying types: -- The type of the result on a successful operation -- The type of error on a failure +- The type of the resultant value on a successful operation +- The type representing the reason for the failure on a failure -A `Result` value is either `Ok ` representing a successful result, or `Error ` representing a failure. +The `Result` type is also a [discriminated union][discriminated-union] with the following possible cases: +* `Ok ` representing a successful result +* `Error ` representing a failure The following function demonstrates how to create a `Result` value: @@ -44,11 +46,13 @@ let validateName (name: string) : Result = | _ -> Ok name ``` +In this example, the `Ok` value is a string (the given name), and the `Error` value is also a string (the cause of the error, in human-readable form). + ## Reading the content of a `Result` value Consider the following type definition and function signature: -```fsharp +``` type FileOpenError = | NotFound | AccessDenied @@ -66,3 +70,5 @@ match openFile(filename) with | Error AccessDenied -> printfn $"Error: you do not have permission to open the file {filename}." | Error FileLocked -> printfn $"Error: file {filename} is already in use." ``` + +[discriminated-union]: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/discriminated-unions diff --git a/concepts/results/introduction.md b/concepts/results/introduction.md index 4e645b905..00c2839ea 100644 --- a/concepts/results/introduction.md +++ b/concepts/results/introduction.md @@ -8,10 +8,12 @@ The `Result` type makes it possible for a function to return a single value indi ## Usage The `Result` type is a generic type containing two underlying types: -- The type of the result on a successful operation -- The type of error on a failure +- The type of the resultant value on a successful operation +- The type representing the reason for the failure on a failure -A `Result` value is either `Ok ` representing a successful result, or `Error ` representing a failure. +The `Result` type is also a [discriminated union][discriminated-union] with the following possible cases: +* `Ok ` representing a successful result +* `Error ` representing a failure The following function demonstrates how to create a `Result` value: @@ -23,11 +25,13 @@ let validateName (name: string) : Result = | _ -> Ok name ``` +In this example, the `Ok` value is a string (the given name), and the `Error` value is also a string (the cause of the error, in human-readable form). + ## Reading the content of a `Result` value Consider the following type definition and function signature: -```fsharp +``` type FileOpenError = | NotFound | AccessDenied diff --git a/exercises/concept/password-checker/.docs/instructions.md b/exercises/concept/password-checker/.docs/instructions.md index ee368d758..8b6b6802b 100644 --- a/exercises/concept/password-checker/.docs/instructions.md +++ b/exercises/concept/password-checker/.docs/instructions.md @@ -23,6 +23,16 @@ A subsequent exercise will explore a more realistic password checker that can in The `checkPassword` function checks the given password against the aforementioned rules. On failure, it indicates the rule that was violated by encapsulating one of the `PasswordRule` values within the result value. +```fsharp +checkPassword "abcdefghij5#" +// => Error MissingUppercaseLetter +``` + ## 2. Implement the ``getStatusMessage` function The `getStatusMessage` function returns a string containing a human-readable message indicating the meaning of the result returned from `checkPassword`. + +```fsharp +getStatusMessage (Error MissingDigit) +// => "Error: does not have at least one digit" +``` diff --git a/exercises/concept/password-checker/.docs/introduction.md b/exercises/concept/password-checker/.docs/introduction.md index 4e645b905..63b60cf1e 100644 --- a/exercises/concept/password-checker/.docs/introduction.md +++ b/exercises/concept/password-checker/.docs/introduction.md @@ -8,10 +8,12 @@ The `Result` type makes it possible for a function to return a single value indi ## Usage The `Result` type is a generic type containing two underlying types: -- The type of the result on a successful operation +- The type of the resultant value on a successful operation - The type of error on a failure -A `Result` value is either `Ok ` representing a successful result, or `Error ` representing a failure. +The `Result` type is also a [discriminated union][discriminated-union] with the following possible cases: +* `Ok ` representing a successful result +* `Error ` representing a failure The following function demonstrates how to create a `Result` value: @@ -23,11 +25,13 @@ let validateName (name: string) : Result = | _ -> Ok name ``` +In this example, the `Ok` value is a string (the given name), and the `Error` value is also a string (the cause of the error, in human-readable form). + ## Reading the content of a `Result` value Consider the following type definition and function signature: -```fsharp +``` type FileOpenError = | NotFound | AccessDenied diff --git a/exercises/concept/password-checker/.meta/Exemplar.fs b/exercises/concept/password-checker/.meta/Exemplar.fs index 3c4373d7c..1b7915da9 100644 --- a/exercises/concept/password-checker/.meta/Exemplar.fs +++ b/exercises/concept/password-checker/.meta/Exemplar.fs @@ -1,35 +1,35 @@ module PasswordChecker -type PasswordRule = - | AtLeast12Characters - | AtLeastOneUppercaseLetter - | AtLeastOneLowercaseLetter - | AtLeastOneDigit - | AtLeastOneSymbol +type PasswordError = + | LessThan12Characters + | MissingUppercaseLetter + | MissingLowercaseLetter + | MissingDigit + | MissingSymbol -/// Validate the given password against the rules defined in `PasswordRule`. If it meets all +/// Validate the given password against the rules defined in the instructions. If it meets all /// of the rules, return a result indicating success; otherwise return a result indicating -/// failure and the rule that was violated. -let checkPassword (password: string) : Result = +/// failure and an error indicating which rule was violated. +let checkPassword (password: string) : Result = if password.Length < 12 then - Error AtLeast12Characters + Error LessThan12Characters elif password |> String.exists System.Char.IsUpper |> not then - Error AtLeastOneUppercaseLetter + Error MissingUppercaseLetter elif password |> String.exists System.Char.IsLower |> not then - Error AtLeastOneLowercaseLetter + Error MissingLowercaseLetter elif password |> String.exists System.Char.IsDigit |> not then - Error AtLeastOneDigit + Error MissingDigit elif password |> String.exists (fun c -> "!@#$%^&*".Contains c) |> not then - Error AtLeastOneSymbol + Error MissingSymbol else Ok password /// Return a human-readable message indicating the meaning of the given result value. -let getStatusMessage (result: Result) : string = +let getStatusMessage (result: Result) : string = let preamble = "Error: does not have at least " match result with - | Error AtLeast12Characters -> preamble + "12 characters" - | Error AtLeastOneUppercaseLetter -> preamble + "one uppercase letter" - | Error AtLeastOneLowercaseLetter -> preamble + "one lowercase letter" - | Error AtLeastOneDigit -> preamble + "one digit" - | Error AtLeastOneSymbol -> preamble + "one symbol" + | Error LessThan12Characters -> preamble + "12 characters" + | Error MissingUppercaseLetter -> preamble + "one uppercase letter" + | Error MissingLowercaseLetter -> preamble + "one lowercase letter" + | Error MissingDigit -> preamble + "one digit" + | Error MissingSymbol -> preamble + "one symbol" | Ok _ -> "OK" diff --git a/exercises/concept/password-checker/PasswordChecker.fs b/exercises/concept/password-checker/PasswordChecker.fs index 32f473140..033ca82da 100644 --- a/exercises/concept/password-checker/PasswordChecker.fs +++ b/exercises/concept/password-checker/PasswordChecker.fs @@ -1,18 +1,18 @@ module PasswordChecker -type PasswordRule = - | AtLeast12Characters - | AtLeastOneUppercaseLetter - | AtLeastOneLowercaseLetter - | AtLeastOneDigit - | AtLeastOneSymbol +type PasswordError = + | LessThan12Characters + | MissingUppercaseLetter + | MissingLowercaseLetter + | MissingDigit + | MissingSymbol -/// Validate the given password against the rules defined in `PasswordRule`. If it meets all +/// Validate the given password against the rules defined in the instructions. If it meets all /// of the rules, return a result indicating success; otherwise return a result indicating -/// failure and the rule that was violated. -let checkPassword (password: string) : Result = +/// failure and an error indicating which rule was violated. +let checkPassword (password: string) : Result = failwith "Please implement this function" /// Return a human-readable message indicating the meaning of the given result value. -let getStatusMessage (result: Result) : string = +let getStatusMessage (result: Result) : string = failwith "Please implement this function" diff --git a/exercises/concept/password-checker/PasswordChecker.fsproj b/exercises/concept/password-checker/PasswordChecker.fsproj index 31900f329..75f8a5818 100644 --- a/exercises/concept/password-checker/PasswordChecker.fsproj +++ b/exercises/concept/password-checker/PasswordChecker.fsproj @@ -16,6 +16,7 @@ + diff --git a/exercises/concept/password-checker/PasswordCheckerTests.fs b/exercises/concept/password-checker/PasswordCheckerTests.fs index c12157bbf..26abe744d 100644 --- a/exercises/concept/password-checker/PasswordCheckerTests.fs +++ b/exercises/concept/password-checker/PasswordCheckerTests.fs @@ -1,38 +1,39 @@ -module Tests +module PasswordCheckerTests open FsUnit.Xunit open Xunit +open Exercism.Tests open PasswordChecker [] [] let ``Error when password too short`` () = - let expected: Result = Error AtLeast12Characters + let expected: Result = Error LessThan12Characters checkPassword "@bcd3fghijK" |> should equal expected [] [] let ``Error when password has no uppercase letters`` () = - let expected: Result = Error AtLeastOneUppercaseLetter + let expected: Result = Error MissingUppercaseLetter checkPassword "@bcd3fghijkl" |> should equal expected [] [] let ``Error when password has no lowercase letters`` () = - let expected: Result = Error AtLeastOneLowercaseLetter + let expected: Result = Error MissingLowercaseLetter checkPassword "@BCD3FGHIJKL" |> should equal expected [] [] let ``Error when password has no digits`` () = - let expected: Result = Error AtLeastOneDigit + let expected: Result = Error MissingDigit checkPassword "@bcdefghijkL" |> should equal expected [] [] let ``Error when password has no symbols`` () = - let expected: Result = Error AtLeastOneSymbol + let expected: Result = Error MissingSymbol checkPassword "abcd3fghijkL" |> should equal expected [] @@ -42,33 +43,33 @@ let ``Error when password has no symbols`` () = [] [] let ``Ok when password is good`` (password: string) = - let expected: Result = Ok password + let expected: Result = Ok password checkPassword password |> should equal expected [] [] let ``Insufficient length error message`` () = - getStatusMessage (Error AtLeast12Characters) |> should equal "Error: does not have at least 12 characters" + getStatusMessage (Error LessThan12Characters) |> should equal "Error: does not have at least 12 characters" [] [] let ``Missing uppercase error message`` () = - getStatusMessage (Error AtLeastOneUppercaseLetter) |> should equal "Error: does not have at least one uppercase letter" + getStatusMessage (Error MissingUppercaseLetter) |> should equal "Error: does not have at least one uppercase letter" [] [] let ``Missing lowercase error message`` () = - getStatusMessage (Error AtLeastOneLowercaseLetter) |> should equal "Error: does not have at least one lowercase letter" + getStatusMessage (Error MissingLowercaseLetter) |> should equal "Error: does not have at least one lowercase letter" [] [] let ``Missing digit error message`` () = - getStatusMessage (Error AtLeastOneDigit) |> should equal "Error: does not have at least one digit" + getStatusMessage (Error MissingDigit) |> should equal "Error: does not have at least one digit" [] [] let ``Missing symbol error message`` () = - getStatusMessage (Error AtLeastOneSymbol) |> should equal "Error: does not have at least one symbol" + getStatusMessage (Error MissingSymbol) |> should equal "Error: does not have at least one symbol" [] []