From 4bd3bedaa6b0b543161e10c89cefa16c7d47335d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nenad=20Misi=C4=87?= Date: Tue, 2 Jul 2024 12:10:53 +0200 Subject: [PATCH] Add "Anagram" exercise (#52) * add anagram exercise * add rational-numbers & complex-numbers to foregone * add missing author * Unimplement SetEq::ne * include=false for last 2 tests in tests.toml * remove rational-numbers from foregone * add comments to excluded test cases --- config.json | 15 +- .../practice/anagram/.docs/instructions.md | 13 ++ .../practice/anagram/.docs/introduction.md | 12 ++ exercises/practice/anagram/.meta/config.json | 20 +++ .../practice/anagram/.meta/example.cairo | 138 ++++++++++++++++ exercises/practice/anagram/.meta/tests.toml | 90 +++++++++++ exercises/practice/anagram/Scarb.toml | 7 + exercises/practice/anagram/src/lib.cairo | 26 ++++ exercises/practice/anagram/src/tests.cairo | 147 ++++++++++++++++++ 9 files changed, 467 insertions(+), 1 deletion(-) create mode 100644 exercises/practice/anagram/.docs/instructions.md create mode 100644 exercises/practice/anagram/.docs/introduction.md create mode 100644 exercises/practice/anagram/.meta/config.json create mode 100644 exercises/practice/anagram/.meta/example.cairo create mode 100644 exercises/practice/anagram/.meta/tests.toml create mode 100644 exercises/practice/anagram/Scarb.toml create mode 100644 exercises/practice/anagram/src/lib.cairo create mode 100644 exercises/practice/anagram/src/tests.cairo diff --git a/config.json b/config.json index 324306ce..7f49d225 100644 --- a/config.json +++ b/config.json @@ -182,10 +182,23 @@ ], "prerequisites": [], "difficulty": 3 + }, + { + "slug": "anagram", + "name": "Anagram", + "uuid": "ffce515d-69e9-48a6-91ec-8593606b15f9", + "practices": [ + "traits", + "strings", + "arrays" + ], + "prerequisites": [], + "difficulty": 5 } ], "foregone": [ - "hangman" + "hangman", + "complex-numbers" ] }, "concepts": [ diff --git a/exercises/practice/anagram/.docs/instructions.md b/exercises/practice/anagram/.docs/instructions.md new file mode 100644 index 00000000..a7298485 --- /dev/null +++ b/exercises/practice/anagram/.docs/instructions.md @@ -0,0 +1,13 @@ +# Instructions + +Your task is to, given a target word and a set of candidate words, to find the subset of the candidates that are anagrams of the target. + +An anagram is a rearrangement of letters to form a new word: for example `"owns"` is an anagram of `"snow"`. +A word is _not_ its own anagram: for example, `"stop"` is not an anagram of `"stop"`. + +The target and candidates are words of one or more ASCII alphabetic characters (`A`-`Z` and `a`-`z`). +Lowercase and uppercase characters are equivalent: for example, `"PoTS"` is an anagram of `"sTOp"`, but `StoP` is not an anagram of `sTOp`. +The anagram set is the subset of the candidate set that are anagrams of the target (in any order). +Words in the anagram set should have the same letter case as in the candidate set. + +Given the target `"stone"` and candidates `"stone"`, `"tones"`, `"banana"`, `"tons"`, `"notes"`, `"Seton"`, the anagram set is `"tones"`, `"notes"`, `"Seton"`. diff --git a/exercises/practice/anagram/.docs/introduction.md b/exercises/practice/anagram/.docs/introduction.md new file mode 100644 index 00000000..1acbdf00 --- /dev/null +++ b/exercises/practice/anagram/.docs/introduction.md @@ -0,0 +1,12 @@ +# Introduction + +At a garage sale, you find a lovely vintage typewriter at a bargain price! +Excitedly, you rush home, insert a sheet of paper, and start typing away. +However, your excitement wanes when you examine the output: all words are garbled! +For example, it prints "stop" instead of "post" and "least" instead of "stale." +Carefully, you try again, but now it prints "spot" and "slate." +After some experimentation, you find there is a random delay before each letter is printed, which messes up the order. +You now understand why they sold it for so little money! + +You realize this quirk allows you to generate anagrams, which are words formed by rearranging the letters of another word. +Pleased with your finding, you spend the rest of the day generating hundreds of anagrams. diff --git a/exercises/practice/anagram/.meta/config.json b/exercises/practice/anagram/.meta/config.json new file mode 100644 index 00000000..2756c19f --- /dev/null +++ b/exercises/practice/anagram/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "misicnenad" + ], + "files": { + "solution": [ + "src/lib.cairo", + "Scarb.toml" + ], + "test": [ + "src/tests.cairo" + ], + "example": [ + ".meta/example.cairo" + ] + }, + "blurb": "Given a word and a list of possible anagrams, select the correct sublist.", + "source": "Inspired by the Extreme Startup game", + "source_url": "https://github.com/rchatley/extreme_startup" +} diff --git a/exercises/practice/anagram/.meta/example.cairo b/exercises/practice/anagram/.meta/example.cairo new file mode 100644 index 00000000..daaf4c03 --- /dev/null +++ b/exercises/practice/anagram/.meta/example.cairo @@ -0,0 +1,138 @@ +use alexandria_sorting::MergeSort; + +#[derive(Drop, Debug)] +struct Set { + values: Array +} + +#[generate_trait] +impl SetImpl of SetTrait { + fn new(values: Array) -> Set { + Set { values } + } +} + +impl SetEq of PartialEq { + fn eq(lhs: @Set, rhs: @Set) -> bool { + let len = lhs.values.len(); + if len != rhs.values.len() { + return false; + } + let mut i = 0; + loop { + if i == len { + break true; + } + let l_item = lhs.values.at(i); + let mut contained = false; + let mut j = 0; + while j != len { + if IgnoreCase::eq(l_item, rhs.values.at(j)) { + contained = true; + break; + } + j += 1; + }; + if !contained { + break false; + } + i += 1; + } + } + + fn ne(lhs: @Set, rhs: @Set) -> bool { + !(lhs == rhs) + } +} + +pub fn anagrams_for(word: @ByteArray, inputs: @Set) -> Set { + let mut word_sorted = @sort(word); + let mut anagrams = Set { values: array![] }; + let mut i = inputs.values.len(); + + while i != 0 { + i -= 1; + let candidate = inputs.values[i]; + let mut candidate_sorted = @sort(candidate); + + let is_anagram = word.len() == candidate.len() + && IgnoreCase::ne(word, candidate) + && IgnoreCaseArray::eq(word_sorted, candidate_sorted); + + if is_anagram { + anagrams.values.append(format!("{candidate}")); + } + }; + + anagrams +} + +impl IgnoreCase of PartialEq { + fn eq(lhs: @ByteArray, rhs: @ByteArray) -> bool { + let len = lhs.len(); + if len != rhs.len() { + return false; + } + let mut i = 0; + loop { + if i == len { + break true; + } + if lowercase(@lhs[i]) != lowercase(@rhs[i]) { + break false; + } + i += 1; + } + } + + fn ne(lhs: @ByteArray, rhs: @ByteArray) -> bool { + !IgnoreCase::eq(lhs, rhs) + } +} + +fn sort(word: @ByteArray) -> Array { + MergeSort::sort(to_char_array(word).span()) +} + +fn to_char_array(word: @ByteArray) -> Array { + let mut chars: Array = array![]; + let mut i = word.len(); + while i != 0 { + i -= 1; + chars.append(lowercase(@word[i])); + }; + chars +} + +impl IgnoreCaseArray of PartialEq> { + fn eq(lhs: @Array, rhs: @Array) -> bool { + if lhs.len() != rhs.len() { + return false; + } + let mut i = lhs.len(); + loop { + if i == 0 { + break true; + } + i -= 1; + if lowercase(lhs.at(i)) != lowercase(rhs.at(i)) { + break false; + } + } + } + + fn ne(lhs: @Array, rhs: @Array) -> bool { + !IgnoreCaseArray::eq(lhs, rhs) + } +} + +fn lowercase(char: @u8) -> u8 { + if *char < 97 { + *char + 32 + } else { + *char + } +} + +#[cfg(test)] +mod tests; diff --git a/exercises/practice/anagram/.meta/tests.toml b/exercises/practice/anagram/.meta/tests.toml new file mode 100644 index 00000000..c8859e3b --- /dev/null +++ b/exercises/practice/anagram/.meta/tests.toml @@ -0,0 +1,90 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[dd40c4d2-3c8b-44e5-992a-f42b393ec373] +description = "no matches" + +[b3cca662-f50a-489e-ae10-ab8290a09bdc] +description = "detects two anagrams" +include = false + +[03eb9bbe-8906-4ea0-84fa-ffe711b52c8b] +description = "detects two anagrams" +reimplements = "b3cca662-f50a-489e-ae10-ab8290a09bdc" + +[a27558ee-9ba0-4552-96b1-ecf665b06556] +description = "does not detect anagram subsets" + +[64cd4584-fc15-4781-b633-3d814c4941a4] +description = "detects anagram" + +[99c91beb-838f-4ccd-b123-935139917283] +description = "detects three anagrams" + +[78487770-e258-4e1f-a646-8ece10950d90] +description = "detects multiple anagrams with different case" + +[1d0ab8aa-362f-49b7-9902-3d0c668d557b] +description = "does not detect non-anagrams with identical checksum" + +[9e632c0b-c0b1-4804-8cc1-e295dea6d8a8] +description = "detects anagrams case-insensitively" + +[b248e49f-0905-48d2-9c8d-bd02d8c3e392] +description = "detects anagrams using case-insensitive subject" + +[f367325c-78ec-411c-be76-e79047f4bd54] +description = "detects anagrams using case-insensitive possible matches" + +[7cc195ad-e3c7-44ee-9fd2-d3c344806a2c] +description = "does not detect an anagram if the original word is repeated" +include = false + +[630abb71-a94e-4715-8395-179ec1df9f91] +description = "does not detect an anagram if the original word is repeated" +reimplements = "7cc195ad-e3c7-44ee-9fd2-d3c344806a2c" + +[9878a1c9-d6ea-4235-ae51-3ea2befd6842] +description = "anagrams must use all letters exactly once" + +[85757361-4535-45fd-ac0e-3810d40debc1] +description = "words are not anagrams of themselves (case-insensitive)" +include = false + +[68934ed0-010b-4ef9-857a-20c9012d1ebf] +description = "words are not anagrams of themselves" +reimplements = "85757361-4535-45fd-ac0e-3810d40debc1" + +[589384f3-4c8a-4e7d-9edc-51c3e5f0c90e] +description = "words are not anagrams of themselves even if letter case is partially different" +reimplements = "85757361-4535-45fd-ac0e-3810d40debc1" + +[ba53e423-7e02-41ee-9ae2-71f91e6d18e6] +description = "words are not anagrams of themselves even if letter case is completely different" +reimplements = "85757361-4535-45fd-ac0e-3810d40debc1" + +[a0705568-628c-4b55-9798-82e4acde51ca] +description = "words other than themselves can be anagrams" +include = false + +[33d3f67e-fbb9-49d3-a90e-0beb00861da7] +description = "words other than themselves can be anagrams" +reimplements = "a0705568-628c-4b55-9798-82e4acde51ca" + +[a6854f66-eec1-4afd-a137-62ef2870c051] +description = "handles case of greek letters" +# Cairo supports only ASCII +include = false + +[fd3509e5-e3ba-409d-ac3d-a9ac84d13296] +description = "different characters may have the same bytes" +# Cairo supports only ASCII +include = false diff --git a/exercises/practice/anagram/Scarb.toml b/exercises/practice/anagram/Scarb.toml new file mode 100644 index 00000000..ecf80e9e --- /dev/null +++ b/exercises/practice/anagram/Scarb.toml @@ -0,0 +1,7 @@ +[package] +name = "anagram" +version = "0.1.0" +edition = "2023_11" + +[dependencies] +alexandria_sorting = { git = "https://github.com/keep-starknet-strange/alexandria.git" } diff --git a/exercises/practice/anagram/src/lib.cairo b/exercises/practice/anagram/src/lib.cairo new file mode 100644 index 00000000..fdaf3a18 --- /dev/null +++ b/exercises/practice/anagram/src/lib.cairo @@ -0,0 +1,26 @@ +#[derive(Drop, Debug)] +struct Set {} + +#[generate_trait] +impl SetImpl of SetTrait { + fn new(values: Array) -> Set { + panic!() + } +} + +impl SetEq of PartialEq { + fn eq(lhs: @Set, rhs: @Set) -> bool { + panic!() + } + + fn ne(lhs: @Set, rhs: @Set) -> bool { + panic!() + } +} + +pub fn anagrams_for(word: @ByteArray, inputs: @Set) -> Set { + panic!() +} + +#[cfg(test)] +mod tests; diff --git a/exercises/practice/anagram/src/tests.cairo b/exercises/practice/anagram/src/tests.cairo new file mode 100644 index 00000000..2d167ec3 --- /dev/null +++ b/exercises/practice/anagram/src/tests.cairo @@ -0,0 +1,147 @@ +use anagram::{anagrams_for, SetTrait as Set}; + +#[test] +fn no_matches() { + let word: ByteArray = "diaper"; + let inputs = Set::new(array!["hello", "world", "zombies", "pants"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array![]); + assert_eq!(output, expected); +} + +#[test] +fn detects_two_anagrams() { + let word: ByteArray = "solemn"; + let inputs = Set::new(array!["lemons", "cherry", "melons"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array!["lemons", "melons"]); + assert_eq!(output, expected); +} + +#[test] +fn does_not_detect_anagram_subsets() { + let word: ByteArray = "good"; + let inputs = Set::new(array!["dog", "goody"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array![]); + assert_eq!(output, expected); +} + +#[test] +fn detects_anagram() { + let word: ByteArray = "listen"; + let inputs = Set::new(array!["enlists", "google", "inlets", "banana"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array!["inlets"]); + assert_eq!(output, expected); +} + +#[test] +fn detects_three_anagrams() { + let word: ByteArray = "allergy"; + let inputs = Set::new( + array!["gallery", "ballerina", "regally", "clergy", "largely", "leading",] + ); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array!["gallery", "regally", "largely"]); + assert_eq!(output, expected); +} + +#[test] +fn detects_multiple_anagrams_with_different_case() { + let word: ByteArray = "nose"; + let inputs = Set::new(array!["Eons", "ONES"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array!["Eons", "ONES"]); + assert_eq!(output, expected); +} + +#[test] +fn does_not_detect_non_anagrams_with_identical_checksum() { + let word: ByteArray = "mass"; + let inputs = Set::new(array!["last"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array![]); + assert_eq!(output, expected); +} + +#[test] +fn detects_anagrams_case_insensitively() { + let word: ByteArray = "Orchestra"; + let inputs = Set::new(array!["cashregister", "Carthorse", "radishes"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array!["Carthorse"]); + assert_eq!(output, expected); +} + +#[test] +fn detects_anagrams_using_case_insensitive_subject() { + let word: ByteArray = "Orchestra"; + let inputs = Set::new(array!["cashregister", "carthorse", "radishes"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array!["carthorse"]); + assert_eq!(output, expected); +} + +#[test] +fn detects_anagrams_using_case_insensitive_possible_matches() { + let word: ByteArray = "orchestra"; + let inputs = Set::new(array!["cashregister", "Carthorse", "radishes"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array!["Carthorse"]); + assert_eq!(output, expected); +} + +#[test] +fn does_not_detect_an_anagram_if_the_original_word_is_repeated() { + let word: ByteArray = "go"; + let inputs = Set::new(array!["goGoGO"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array![]); + assert_eq!(output, expected); +} + +#[test] +fn anagrams_must_use_all_letters_exactly_once() { + let word: ByteArray = "tapper"; + let inputs = Set::new(array!["patter"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array![]); + assert_eq!(output, expected); +} + +#[test] +fn words_are_not_anagrams_of_themselves() { + let word: ByteArray = "BANANA"; + let inputs = Set::new(array!["BANANA"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array![]); + assert_eq!(output, expected); +} + +#[test] +fn words_are_not_anagrams_of_themselves_even_if_letter_case_is_partially_different() { + let word: ByteArray = "BANANA"; + let inputs = Set::new(array!["Banana"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array![]); + assert_eq!(output, expected); +} + +#[test] +fn words_are_not_anagrams_of_themselves_even_if_letter_case_is_completely_different() { + let word: ByteArray = "BANANA"; + let inputs = Set::new(array!["banana"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array![]); + assert_eq!(output, expected); +} + +#[test] +fn words_other_than_themselves_can_be_anagrams() { + let word: ByteArray = "LISTEN"; + let inputs = Set::new(array!["LISTEN", "Silent"]); + let output = anagrams_for(@word, @inputs); + let expected = Set::new(array!["Silent"]); + assert_eq!(output, expected); +}