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..213f370e6 --- /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..060ddd3f7 --- /dev/null +++ b/exercises/concept/improved-password-checker/.docs/instructions.md @@ -0,0 +1,37 @@ +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 !@#$%^&\* + +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 indicate all of the violated rules. + +## 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. On failure, it indicates the rule(s) that was/were violated by encapsulating one or more of the `PasswordError` values within the result value. + +```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..2a94957b9 --- /dev/null +++ b/exercises/concept/improved-password-checker/.docs/introduction.md @@ -0,0 +1,41 @@ +# 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 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/exercises/concept/improved-password-checker/.meta/Exemplar.fs b/exercises/concept/improved-password-checker/.meta/Exemplar.fs new file mode 100644 index 000000000..937b82167 --- /dev/null +++ b/exercises/concept/improved-password-checker/.meta/Exemplar.fs @@ -0,0 +1,55 @@ +module ImprovedPasswordChecker + +open System + +[] +type PasswordError = + | 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 mutable errors = ( + PasswordError.LessThan12Characters ||| + PasswordError.MissingUppercaseLetter ||| + PasswordError.MissingLowercaseLetter ||| + PasswordError.MissingDigit ||| + PasswordError.MissingSymbol + ) + if password.Length >= 12 then + errors <- errors &&& ~~~PasswordError.LessThan12Characters + for (charTest, flag) in [ + (System.Char.IsUpper, PasswordError.MissingUppercaseLetter); + (System.Char.IsLower, PasswordError.MissingLowercaseLetter); + (System.Char.IsDigit, PasswordError.MissingDigit); + ((fun c -> "!@#$%^&*".Contains c), PasswordError.MissingSymbol) + ] do + if password |> String.exists charTest then + errors <- errors &&& ~~~flag + + if int errors = 0 then + Ok password + else + Error errors + +/// Return a set of human-readable phrases indicating the meaning of the given result value. +let getStatusPhrases (result: Result) : Set = + let mutable phrases: Set = Set [ ] + match result with + | Error errors -> + for (flag, phrase) in [ + (PasswordError.LessThan12Characters, "12 characters"); + (PasswordError.MissingUppercaseLetter, "uppercase letter"); + (PasswordError.MissingLowercaseLetter, "lowercase letter"); + (PasswordError.MissingDigit, "digit"); + (PasswordError.MissingSymbol, "symbol") + ] do + if errors.HasFlag(flag) then + phrases <- phrases.Add(phrase) + | Ok _ -> () + phrases 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..9fe26a8e8 --- /dev/null +++ b/exercises/concept/improved-password-checker/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "blackk-foxx" + ], + "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..f33798dd3 --- /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 set of human-readable phrases indicating the meaning of the given result value. +let getStatusPhrases (result: Result) : Set = + 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..11e7733d7 --- /dev/null +++ b/exercises/concept/improved-password-checker/ImprovedPasswordCheckerTests.fs @@ -0,0 +1,127 @@ +module ImprovedPasswordCheckerTests + +open FsUnit.Xunit +open Xunit +open Exercism.Tests + +open ImprovedPasswordChecker + +[] +[] +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 ``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 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 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 two rules with thirteen mixed-case letters`` () = + let expected: Result = Error ( + PasswordError.MissingDigit ||| + PasswordError.MissingSymbol + ) + checkPassword "AbCdEfGhIjKlM" |> 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 ``Missing digit error with twelve mixed-case letters and symbols`` () = + let expected: Result = Error PasswordError.MissingDigit + checkPassword "ABCDEF$&*ghi" |> should equal expected + +[] +[] +let ``Ok with valid password`` () = + let password = "ABCdef123@&$" + let expected: Result = Ok password + checkPassword password |> should equal expected + +[] +[] +let ``Insufficient length`` () = + getStatusPhrases (Error PasswordError.LessThan12Characters) |> should equal (Set ["12 characters"]) + +[] +[] +let ``Insufficient length + missing uppercase letter`` () = + let givenResult = Error (PasswordError.LessThan12Characters ||| PasswordError.MissingUppercaseLetter) + let expected = Set ["12 characters"; "uppercase letter"] + getStatusPhrases givenResult |> should equal expected + +[] +[] +let ``Insufficient length + missing uppercase letter + missing lowercase letter`` () = + let givenResult = Error ( + PasswordError.LessThan12Characters ||| + PasswordError.MissingUppercaseLetter ||| + PasswordError.MissingLowercaseLetter + ) + let expected = Set ["12 characters"; "uppercase letter"; "lowercase letter"] + getStatusPhrases givenResult |> should equal expected + +[] +[] +let ``All errors except missing symbol`` () = + let givenResult = Error ( + PasswordError.LessThan12Characters ||| + PasswordError.MissingUppercaseLetter ||| + PasswordError.MissingLowercaseLetter ||| + PasswordError.MissingDigit + ) + let expected = Set ["12 characters"; "uppercase letter"; "lowercase letter"; "digit"] + getStatusPhrases givenResult |> should equal expected + +[] +[] +let ``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"] + getStatusPhrases givenResult |> should equal expected