diff --git a/concepts/flag-discriminated-unions/.meta/config.json b/concepts/flag-discriminated-unions/.meta/config.json new file mode 100644 index 000000000..680811818 --- /dev/null +++ b/concepts/flag-discriminated-unions/.meta/config.json @@ -0,0 +1,5 @@ +{ + "blurb": "The Flags attribute allows the values defined in a discriminated union to be represented as bit positions (i.e. flags).", + "authors": ["blackk-foxx"], + "contributors": ["ErikSchierboom"] +} diff --git a/concepts/flag-discriminated-unions/about.md b/concepts/flag-discriminated-unions/about.md new file mode 100644 index 000000000..e79b026cd --- /dev/null +++ b/concepts/flag-discriminated-unions/about.md @@ -0,0 +1,41 @@ +# About + +The Flags attribute allows the values defined in a discriminated union to be represented as bit positions (i.e. flags). +As flags, such values can be combined, making it possible for multiple boolean conditions to be represented in a single value. + +```fsharp +[] +type PhoneFeatures = +| Call = 1 +| Text = 2 +``` + +```fsharp +[] +type PhoneFeaturesBinary = +| Call = 0b00000001 +| Text = 0b00000010 +``` + +Setting a flag can be done with the bitwise OR operator (`|||`); unsetting a flag can be done with a combination of the bitwise AND operator (`&&&`) and the bitwise negation operator (`~~~`). +While checking flag's state can be done with the bitwise AND operator, one can also use the HasFlag() method. + +```fsharp +let features = PhoneFeatures.Call + +// Set the Text flag +let moreFeatures = features ||| PhoneFeatures.Text + +moreFeatures.HasFlag(PhoneFeatures.Call) // => true +moreFeatures.HasFlag(PhoneFeatures.Text) // => true + +// Unset the Call flag +let lessFeatures = features &&& ~~~PhoneFeatures.Call + +lessFeatures.HasFlag(PhoneFeatures.Call) // => false +lessFeatures.HasFlag(PhoneFeatures.Text) // => true +``` + +See [Summary of Bitwise Operators][bitwise-operators] for a complete list of the bitwise operators available in the F# language. + +[bitwise-operators]: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/symbol-and-operator-reference/bitwise-operators#summary-of-bitwise-operators diff --git a/concepts/flag-discriminated-unions/introduction.md b/concepts/flag-discriminated-unions/introduction.md new file mode 100644 index 000000000..78240d3b3 --- /dev/null +++ b/concepts/flag-discriminated-unions/introduction.md @@ -0,0 +1,18 @@ +# Introduction + +A common way to use discriminated union type in F# is to represent a fixed set of named constants -- a structure called an "enum" in other languages. + +Normally, in such a discriminated union, each case can only refer to exactly one of those named constants. +However, sometimes it is useful to refer to more than one constant. +To do so, one can annotate the discriminated union with the Flags attribute. + +A discriminated union with the Flags attribute can be defined as follows (using binary integer notation 0b): + +```fsharp +[] +type PhoneFeatures = +| Call = 0b00000001 +| Text = 0b00000010 +``` + +A `PhoneFeatures` instance with the value `0b00000011` has both its `Call` and `Text` flags set. diff --git a/concepts/flag-discriminated-unions/links.json b/concepts/flag-discriminated-unions/links.json new file mode 100644 index 000000000..501b7a945 --- /dev/null +++ b/concepts/flag-discriminated-unions/links.json @@ -0,0 +1,6 @@ +[ + { + "url": "https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-flagsattribute", + "description": "Flags attribute" + } +] diff --git a/config.json b/config.json index 05193524a..722f4d6f0 100644 --- a/config.json +++ b/config.json @@ -225,6 +225,19 @@ "basics", "pattern-matching" ] + }, + { + "slug": "improved-password-checker", + "name": "Improved Password Checker", + "uuid": "094e458d-14bc-43db-af9c-3fd951a6c635", + "concepts": [ + "flag-discriminated-unions" + ], + "prerequisites": [ + "basics", + "pattern-matching", + "results" + ] } ], "practice": [ @@ -2360,6 +2373,11 @@ "slug": "discriminated-unions", "name": "Discriminated Unions" }, + { + "uuid": "174f8c5e-e947-4e92-a0a5-1530f32b90ce", + "slug": "flag-discriminated-unions", + "name": "Flag Discriminated Unions" + }, { "uuid": "1327d780-3263-4a22-b363-684d775241b8", "slug": "floating-point-numbers", diff --git a/exercises/Exercises.slnx b/exercises/Exercises.slnx index c25398833..12e80692e 100644 --- a/exercises/Exercises.slnx +++ b/exercises/Exercises.slnx @@ -6,6 +6,7 @@ + diff --git a/exercises/concept/improved-password-checker/.docs/hints.md b/exercises/concept/improved-password-checker/.docs/hints.md new file mode 100644 index 000000000..e69de29bb diff --git a/exercises/concept/improved-password-checker/.docs/instructions.md b/exercises/concept/improved-password-checker/.docs/instructions.md new file mode 100644 index 000000000..48cb62479 --- /dev/null +++ b/exercises/concept/improved-password-checker/.docs/instructions.md @@ -0,0 +1,35 @@ +The goal of this exercise is to improve upon the Password Checker exercise. +Since a given password will likely violate more than one rule at a time, a useful password checker ought to communicate to the user all the rules that are violated, instead of just the first one that happens to be discovered. +The improved password checker should indicate all of the rules being violated by a given password in one go. + +The rules for this password checker are the same as in the previous Password Checker exercise: + +- 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 !@#$%^&\* + +## 1. Modify the `PasswordError` discriminated union to allow the individual values to be treated as flags + +Note that the tests will not compile until this essential step is complete. + +## 2. Implement the `checkPassword` function + +The `checkPassword` function checks the given password against the aforementioned rules. +The function should return a `Result` value, where `Ok` is returned when the password satisfies all rules, and an `Error` value when it fails one or more rules. +If the given password fails multiple rules, the `PasswordError` value should represent all of the failing rules. + +```fsharp +checkPassword "abcdefghijk5" +// => Error (PasswordError.MissingUppercaseLetter ||| PasswordError.MissingSymbol) +``` + +## 3. Implement the ``getStatusPhrases` function + +The `getStatusPhrases` function returns a set of strings each containing a human-readable phrase corresponding to one of the erorrs in the result returned from `checkPassword`. + +```fsharp +getStatusPhrases (Error PasswordError.MissingDigit ||| PasswordError.LessThan12Characters) +// => Set ["12 characters"; "digit"] +``` diff --git a/exercises/concept/improved-password-checker/.docs/introduction.md b/exercises/concept/improved-password-checker/.docs/introduction.md new file mode 100644 index 000000000..c3c9c8bdf --- /dev/null +++ b/exercises/concept/improved-password-checker/.docs/introduction.md @@ -0,0 +1,35 @@ +# Introduction + +The Flags attribute allows the values defined in a discriminated union to be represented as bit positions (i.e. flags). +As flags, such values can be combined, making it possible for multiple boolean conditions to be represented in a single value. + +```fsharp +[] +type PhoneFeatures = +| Call = 1 +| Text = 2 +``` + +```fsharp +[] +type PhoneFeaturesBinary = +| Call = 0b00000001 +| Text = 0b00000010 +``` + +Setting a flag can be done with the bitwise OR operator (`|||`); unsetting a flag can be done with a combination of the bitwise AND operator (`&&&`) and the bitwise negation operator (`~~~`). +While checking a flag's state can be done with the bitwise AND operator, one can also use the `HasFlag()` method. + +```fsharp +let features = PhoneFeatures.Call + +// Set the Text flag +let moreFeatures = features ||| PhoneFeatures.Text + +moreFeatures.HasFlag(PhoneFeatures.Call) // => true +moreFeatures.HasFlag(PhoneFeatures.Text) // => true +``` + +See [Summary of Bitwise Operators][bitwise-operators] for a complete list of the bitwise operators available in the F# language. + +[bitwise-operators]: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/symbol-and-operator-reference/bitwise-operators#summary-of-bitwise-operators diff --git a/exercises/concept/improved-password-checker/.meta/Exemplar.fs b/exercises/concept/improved-password-checker/.meta/Exemplar.fs new file mode 100644 index 000000000..f9aa1f2dc --- /dev/null +++ b/exercises/concept/improved-password-checker/.meta/Exemplar.fs @@ -0,0 +1,49 @@ +module ImprovedPasswordChecker + +open System + +[] +type PasswordError = + | None = 0 + | LessThan12Characters = 1 + | MissingUppercaseLetter = 2 + | MissingLowercaseLetter = 4 + | MissingDigit = 8 + | MissingSymbol = 16 + +/// 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 with an error value indicating all of the rules that were violated. +let checkPassword (password: string) : Result = + let errors = ( + [ + ((fun (s: string) -> s.Length >= 12), PasswordError.LessThan12Characters); + (String.exists System.Char.IsDigit, PasswordError.MissingDigit); + (String.exists System.Char.IsLower, PasswordError.MissingLowercaseLetter); + (String.exists System.Char.IsUpper, PasswordError.MissingUppercaseLetter); + (String.exists (fun c -> "!@#$%^&*".Contains c), PasswordError.MissingSymbol) + ] + |> List.filter (fun (test, _) -> password |> test |> not) + |> List.map snd + |> List.fold (|||) PasswordError.None + ) + if errors = PasswordError.None then + Ok password + else + Error errors + +/// Return a list of human-readable phrases indicating the meaning of the given result value. +let getStatusPhrases (result: Result) : string list = + match result with + | Error e -> ( + [ + (PasswordError.LessThan12Characters, "12 characters"); + (PasswordError.MissingUppercaseLetter, "uppercase letter"); + (PasswordError.MissingLowercaseLetter, "lowercase letter"); + (PasswordError.MissingDigit, "digit"); + (PasswordError.MissingSymbol, "symbol") + ] + |> List.filter (fun (flag, _) -> e.HasFlag(flag)) + |> List.map snd + ) + | _ -> [] diff --git a/exercises/concept/improved-password-checker/.meta/config.json b/exercises/concept/improved-password-checker/.meta/config.json new file mode 100644 index 000000000..643b57e4a --- /dev/null +++ b/exercises/concept/improved-password-checker/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "blackk-foxx" + ], + "contributors": [ + "ErikSchierboom" + ], + "files": { + "solution": [ + "ImprovedPasswordChecker.fs" + ], + "test": [ + "ImprovedPasswordCheckerTests.fs" + ], + "exemplar": [ + ".meta/Exemplar.fs" + ] + }, + "blurb": "Learn how to use the Flags attribute to combine values within a discriminated union" +} diff --git a/exercises/concept/improved-password-checker/ImprovedPasswordChecker.fs b/exercises/concept/improved-password-checker/ImprovedPasswordChecker.fs new file mode 100644 index 000000000..6e2ad766e --- /dev/null +++ b/exercises/concept/improved-password-checker/ImprovedPasswordChecker.fs @@ -0,0 +1,18 @@ +module ImprovedPasswordChecker + +type PasswordError = + | LessThan12Characters + | MissingUppercaseLetter + | MissingLowercaseLetter + | MissingDigit + | MissingSymbol + +/// 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 with an error value indicating all of the rules that were violated. +let checkPassword (password: string) : Result = + failwith "Please implement this function" + +/// Return a list of human-readable phrases indicating the meaning of the given result value. +let getStatusPhrases (result: Result) : string list = + failwith "Please implement this function" diff --git a/exercises/concept/improved-password-checker/ImprovedPasswordChecker.fsproj b/exercises/concept/improved-password-checker/ImprovedPasswordChecker.fsproj new file mode 100644 index 000000000..2ef9cc891 --- /dev/null +++ b/exercises/concept/improved-password-checker/ImprovedPasswordChecker.fsproj @@ -0,0 +1,24 @@ + + + + net10.0 + Exercism + + false + + + + + + + + + + + + + + + + + diff --git a/exercises/concept/improved-password-checker/ImprovedPasswordCheckerTests.fs b/exercises/concept/improved-password-checker/ImprovedPasswordCheckerTests.fs new file mode 100644 index 000000000..4ff0cbb46 --- /dev/null +++ b/exercises/concept/improved-password-checker/ImprovedPasswordCheckerTests.fs @@ -0,0 +1,133 @@ +module ImprovedPasswordCheckerTests + +open FsUnit.Xunit +open Xunit +open Exercism.Tests + +open ImprovedPasswordChecker + +[] +[] +let ``Ok with valid password`` () = + let password = "ABCdef123@&$" + let expected: Result = Ok password + checkPassword password |> should equal expected + +[] +[] +let ``Missing digit error with twelve mixed-case letters and symbols`` () = + let expected: Result = Error PasswordError.MissingDigit + checkPassword "ABCDEF$&*ghi" |> should equal expected + +[] +[] +let ``Missing symbol error with twelve mixed-case letters and digits`` () = + let expected: Result = Error PasswordError.MissingSymbol + checkPassword "ABCDEF123ghi" |> should equal expected + +[] +[] +let ``Error on two rules with thirteen mixed-case letters`` () = + let expected: Result = Error ( + PasswordError.MissingDigit ||| + PasswordError.MissingSymbol + ) + checkPassword "AbCdEfGhIjKlM" |> should equal expected + +[] +[] +let ``Error on some of the rules with twelve uppercase letters`` () = + let expected: Result = Error ( + PasswordError.MissingLowercaseLetter ||| + PasswordError.MissingDigit ||| + PasswordError.MissingSymbol + ) + checkPassword "ABCDEFGHIJKL" |> should equal expected + +[] +[] +let ``Error on most of the rules with eleven uppercase letters`` () = + let expected: Result = Error ( + PasswordError.LessThan12Characters ||| + PasswordError.MissingLowercaseLetter ||| + PasswordError.MissingDigit ||| + PasswordError.MissingSymbol + ) + checkPassword "ABCDEFGHIJK" |> should equal expected + +[] +[] +let ``Error on most of the rules with single uppercase letter`` () = + let expected: Result = Error ( + PasswordError.LessThan12Characters ||| + PasswordError.MissingLowercaseLetter ||| + PasswordError.MissingDigit ||| + PasswordError.MissingSymbol + ) + checkPassword "A" |> should equal expected + +[] +[] +let ``Error on all rules with blank password`` () = + let expected: Result = Error ( + PasswordError.LessThan12Characters ||| + PasswordError.MissingUppercaseLetter ||| + PasswordError.MissingLowercaseLetter ||| + PasswordError.MissingDigit ||| + PasswordError.MissingSymbol + ) + checkPassword "" |> should equal expected + +[] +[] +let ``No phrases for Ok result`` () = + getStatusPhrases (Ok "") |> should equal ([]: string list) + +[] +[] +let ``One phrase for insufficient length`` () = + let actual = getStatusPhrases (Error PasswordError.LessThan12Characters) + set actual |> should equal (set ["12 characters"]) + +[] +[] +let ``Two phrases for insufficient length + missing uppercase letter`` () = + let givenResult = Error (PasswordError.LessThan12Characters ||| PasswordError.MissingUppercaseLetter) + let expected = ["12 characters"; "uppercase letter"] + set (getStatusPhrases givenResult) |> should equal (set expected) + +[] +[] +let ``Three phrases for insufficient length + missing uppercase letter + missing lowercase letter`` () = + let givenResult = Error ( + PasswordError.LessThan12Characters ||| + PasswordError.MissingUppercaseLetter ||| + PasswordError.MissingLowercaseLetter + ) + let expected = ["12 characters"; "uppercase letter"; "lowercase letter"] + set (getStatusPhrases givenResult) |> should equal (set expected) + +[] +[] +let ``Four phrases for all errors except missing symbol`` () = + let givenResult = Error ( + PasswordError.LessThan12Characters ||| + PasswordError.MissingUppercaseLetter ||| + PasswordError.MissingLowercaseLetter ||| + PasswordError.MissingDigit + ) + let expected = ["12 characters"; "uppercase letter"; "lowercase letter"; "digit"] + set (getStatusPhrases givenResult) |> should equal (set expected) + +[] +[] +let ``All phrases for all errors`` () = + let givenResult = Error ( + PasswordError.LessThan12Characters ||| + PasswordError.MissingUppercaseLetter ||| + PasswordError.MissingLowercaseLetter ||| + PasswordError.MissingDigit ||| + PasswordError.MissingSymbol + ) + let expected = Set ["12 characters"; "uppercase letter"; "lowercase letter"; "digit"; "symbol"] + set (getStatusPhrases givenResult) |> should equal (set expected)