diff --git a/.gitignore b/.gitignore index 51a74af2..4acc8d52 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ pom.xml.asc .clj-kondo .lsp *.calva +*.cpcache diff --git a/exercises/concept/date-parser/.docs/hints.md b/exercises/concept/date-parser/.docs/hints.md index e69de29b..c37f7845 100644 --- a/exercises/concept/date-parser/.docs/hints.md +++ b/exercises/concept/date-parser/.docs/hints.md @@ -0,0 +1,57 @@ +# Hints + +## General + +- Review regular expression patterns from the introduction. Remember, when creating the pattern from a string, you must escape some characters. +- Refer to the full [Java regex spec][java-util-regex-pattern]. +- Check out this website about regular expressions: [Regular-Expressions.info][website-regex-info]. +- Check out this website about regular expressions: [Rex Egg -The world's most tyrannosauical regex tutorial][website-rexegg]. +- Check out this website about regular expressions: [RegexOne - Learn Regular Expressions with simple, interactive exercises.][website-regexone]. +- Check out this website about regular expressions: [Regular Expressions 101 - an online regex sandbox][website-regex-101]. +- Check out this website about regular expressions: [RegExr - an online regex sandbox][website-regexr]. + +## 1. Match the day, month, and year from a date + +- Remember to return a string representing the regular expression pattern. +- Review how to create _character classes_ or use _shorthand character classes_. +- Review _quantifiers_. +- A day is one or two digits. +- A month is one or two digits. +- A year is four digits. +- Create a regex pattern with [`re-pattern`][re-pattern]. +- Return a regex match with [`re-matches`][re-matches]. + +## 2. Match the day of the week and the month of the year + +- Review how to write a pattern to match _string literals_. +- Review _alternations_. +- Wrap the whole expression in a _group_. + +## 3. Capture the day, month, and year + +- Review how to write patterns for captures and named captures. +- Reuse the `day`, `month`, `year`, `day-names`, and `month-names` functions that you already implemented. +- You can use [`re-matcher`][re-matcher] to return an instance of `java.util.regex.Matcher` to use with [`re-find`][re-find]. + +## 4. Combine the captures to capture the whole date + +- Remember, string concatenation may be used to join strings. +- Reuse the `capture-day`, `capture-month`, `capture-year`, `capture-day-name`, and `capture-month-name` functions that you already implemented. + +## 5. Narrow the capture to match only on the date + +- Remember, _anchors_ help to match the pattern to the **whole line**. +- String concatenation may be used in a call to `re-pattern`. +- Reuse the `capture-numeric-date`, `capture-month-name-date`, and `capture-day-month-nam-date` functions that you already implemented. + +[java-util-regex-pattern]: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/regex/Pattern.html +[re-find]: https://clojuredocs.org/clojure.core/re-find +[re-matcher]: https://clojuredocs.org/clojure.core/re-matcher +[re-matches]: https://clojuredocs.org/clojure.core/re-matches +[re-pattern]: https://clojuredocs.org/clojure.core/re-pattern +[website-regex-info]: https://www.regular-expressions.info +[website-rexegg]: https://www.rexegg.com/ +[website-regexone]: https://regexone.com/ +[website-regex-101]: https://regex101.com/ +[website-regexr]: https://regexr.com/ +[website-regex-crossword]: https://regexcrossword.com/ diff --git a/exercises/concept/date-parser/.docs/instructions.md b/exercises/concept/date-parser/.docs/instructions.md index 713c9ebc..6b20578d 100644 --- a/exercises/concept/date-parser/.docs/instructions.md +++ b/exercises/concept/date-parser/.docs/instructions.md @@ -34,7 +34,7 @@ Implement `day-name` and `month-name` to return a string pattern which, when exe ## 3. Capture the day, month, and year -Implement `capture-day`, `capture-month`, `capture-year`, `capture-day-name`, `capture-month-name` to return a map of the respective components to the names: `"day"`, `"month"`, `"year"`, `"day-name"`, `"month-name"` +Implement `capture-day`, `capture-month`, `capture-year`, `capture-day-name`, and `capture-month-name` to return a map of the respective components to the names: `"day"`, `"month"`, `"year"`, `"day-name"`, and `"month-name"`. ```clojure (capture-month-name "December") @@ -49,3 +49,14 @@ Implement `capture-numeric-date`, `capture-month-name-date`, and `capture-day-mo (capture-numeric-date "01/01/1970") ;;=> {:day "01", :month "01", :year "1970"} ``` + +## 5. Narrow the capture to match only on the date + +Implement `match-numeric-date`, `match-month-name-date`, and `match-day-month-name-date` to return a `java.util.regex.Pattern` that only matches the date, and which can also capture the components. + +```clojure +(re-matches date-parser/match-day-month-name-date "Thursday, January 1, 1970 was the day") +;; => nil +(re-matches date-parser/match-day-month-name-date "Thursday, January 1, 1970") +;; => ["Thursday, January 1, 1970" "Thursday" "January" "1" "1970"] +``` \ No newline at end of file diff --git a/exercises/concept/date-parser/.docs/introduction.md b/exercises/concept/date-parser/.docs/introduction.md index e69de29b..af7d586c 100644 --- a/exercises/concept/date-parser/.docs/introduction.md +++ b/exercises/concept/date-parser/.docs/introduction.md @@ -0,0 +1,84 @@ +# Introduction + +## Regular Expressions + +Regular expressions (regex) are a powerful tool for working with strings in Clojure. Regular expressions in Clojure follow the Java specification. String patterns representing the regular expression's meaning are first compiled then used for matching all or part of a string. + +In Clojure, the most concise way to create regular expressions is using the #"pattern" syntax. This provides _syntactic sugar_ as a convenience. To match a _string literal_, we can use this. + +```clojure +#"test" +``` + +If you want to construct a regex pattern dynamically at run time, then you need to use `re-pattern` to convert a string to a pattern that can be used for matching. When doing so, you need to escape every `\` character with another `\`. But if your pattern is one you write into the source code, it is more convenient to use the #"pattern" syntax. + +### Character classes + +Matching a range of characters using square brackets `[]` defines a _character class_. This will match any one character to the characters in the class. You can also specify a range of characters like `a-z`, as long as the start and end represent a contiguous range of code points. + +```clojure +(def regex #"[a-z][ADKZ][0-9][!?]") +(re-matches regex "jZ5!") +;; => "jZ5!" +(re-matches regex "jB5?") +;; => nil +``` + +_Shorthand character classes_ make the pattern more concise. For example: + +- `\d` short for `[0-9]` (any digit) +- `\w` short for `[A-Za-z0-9_]` (any 'word' character) +- `\s` short for `[ \t\n\x0B\f\r]` (any whitespace character) + +When a _shorthand character class_ is used outside of the #"pattern" syntax, it must be escaped: `"\\d"` + +### Alternations + +_Alternations_ use `|` as a special character to denote matching one _or_ another + +```clojure +(def regex #"cat|bat") +(re-matches regex "bat") +;; => "bat" +(re-matches regex "bat") +;; => "cat" +``` + +### Quantifiers + +_Quantifiers_ allow for a repeating pattern in the regex. They affect the group preceding the quantifier. + +- `{N, M}` where `N` is the minimum number of repetitions, and `M` is the maximum +- `{N,}` match `N` or more repetitions +- `{0,}` may also be written as `*`: match zero-or-more repetitions +- `{1,}` may also be written as `+`: match one-or-more repetitions + +### Groups + +Round brackets `()` are used to denote _groups_ and _captures_. The group may also be _captured_ in some instances to be returned for use. In Clojure, these may be named or un-named. Captures are named by appending `?` after the opening parenthesis. Groups function as a single unit, like when followed by _quantifiers_. + +```clojure +(re-find #"(?b)" "blueberry") +;; => ["b" "b"] +``` + +### Anchors + +_Anchors_ are used to tie the regular expression to the beginning or end of the string to be matched: + +- `^` anchors to the beginning of the string +- `$` anchors to the end of the string + +### Concatenation + +Because the `#"pattern"` syntax is a shortcut for `re-pattern`, you may also use string concatenation to dynamically build a regular expression pattern: + +```clojure +(def anchor "$") +(def regex (str "end of the line" anchor)) +(re-matches regex "end of the line?") +;; => nil +(re-matches regex "end of the line") +"end of the line" =~ regex +;; => "end of the line" +``` \ No newline at end of file diff --git a/exercises/concept/date-parser/.meta/exemplar.clj b/exercises/concept/date-parser/.meta/exemplar.clj index 689bdfe1..cfd30a64 100644 --- a/exercises/concept/date-parser/.meta/exemplar.clj +++ b/exercises/concept/date-parser/.meta/exemplar.clj @@ -56,3 +56,12 @@ :month-name (.group matcher "month") :day (.group matcher "day") :year (.group matcher "year")}))) + +(def match-numeric-date + (re-pattern (str "(?" day ")/(?" month ")/(?" year ")"))) + +(def match-month-name-date + (re-pattern (str "(?" months ") (?" day "), (?" year ")"))) + +(def match-day-month-name-date + (re-pattern (str "(?" days "), (?" months ") (?" day "), (?" year ")"))) \ No newline at end of file diff --git a/exercises/concept/date-parser/src/date_parser.clj b/exercises/concept/date-parser/src/date_parser.clj index 447b1a2f..050f842a 100644 --- a/exercises/concept/date-parser/src/date_parser.clj +++ b/exercises/concept/date-parser/src/date_parser.clj @@ -6,34 +6,24 @@ (def days) -(defn day-names [s] - ) +(defn day-names [s]) (def months) -(defn month-names [s] - ) +(defn month-names [s]) -(defn capture-month [s] - ) +(defn capture-month [s]) -(defn capture-day [s] - ) +(defn capture-day [s]) -(defn capture-year [s] - ) +(defn capture-year [s]) -(defn capture-month-name [s] - ) +(defn capture-month-name [s]) -(defn capture-day-name [s] - ) +(defn capture-day-name [s]) -(defn capture-numeric-date [s] - ) +(defn capture-numeric-date [s]) -(defn capture-month-name-date [s] - ) +(defn capture-month-name-date [s]) -(defn capture-day-month-name-date [s] - ) +(defn capture-day-month-name-date [s]) diff --git a/exercises/concept/date-parser/test/date_parser_test.clj b/exercises/concept/date-parser/test/date_parser_test.clj index 19123cca..c3e78f71 100644 --- a/exercises/concept/date-parser/test/date_parser_test.clj +++ b/exercises/concept/date-parser/test/date_parser_test.clj @@ -1,7 +1,7 @@ (ns date-parser-test (:require [clojure.test :refer [deftest testing is]] date-parser)) - + (deftest ^{:task 1} day-test (testing "numeric pattern for day matches" (testing "un-padded 1" @@ -84,7 +84,7 @@ (is (= "08" (re-matches (re-pattern date-parser/day) "08")))) (testing "padded 09" (is (= "09" (re-matches (re-pattern date-parser/day) "09"))))) - (testing "numeric pattern for day doesn't match" + (testing "numeric pattern for a day that doesn't match" (testing "too few digits" (is (nil? (re-matches (re-pattern date-parser/day) "")))) (testing "too many digits" @@ -120,8 +120,8 @@ (is (= "11" (re-matches (re-pattern date-parser/month) "11")))) (testing "un-padded 12" (is (= "12" (re-matches (re-pattern date-parser/month) "12")))) - (testing "un-padded 1" - (is (= "1" (re-matches (re-pattern date-parser/month) "1")))) + (testing "padded 01" + (is (= "01" (re-matches (re-pattern date-parser/month) "01")))) (testing "padded 02" (is (= "02" (re-matches (re-pattern date-parser/month) "02")))) (testing "padded 03" @@ -138,7 +138,7 @@ (is (= "08" (re-matches (re-pattern date-parser/month) "08")))) (testing "padded 09" (is (= "09" (re-matches (re-pattern date-parser/month) "09"))))) - (testing "numeric pattern for month doesn't match" + (testing "numeric pattern for month that doesn't match" (testing "too few digits" (is (nil? (re-matches (re-pattern date-parser/month) "")))) (testing "too many digits" @@ -182,7 +182,10 @@ (testing "numeric day of the week (0-indexed)" (is (nil? (date-parser/day-names "0")))) (testing "numeric day of the week (1-indexed)" - (is (nil? (date-parser/day-names "1")))))) + (is (nil? (date-parser/day-names "1"))))) + (testing "day names don't match with trailing or leading whitespace" + (is (nil? (date-parser/day-names " Sunday "))))) + (deftest ^{:task 2} month-names-test (testing "month names match" @@ -206,7 +209,9 @@ (testing "numeric month of the year (0-indexed)" (is (nil? (date-parser/month-names "0")))) (testing "numeric month of the year (1-indexed)" - (is (nil? (date-parser/month-names "1")))))) + (is (nil? (date-parser/month-names "1"))))) + (testing "month names don't match with trailing or leading whitespace" + (is (nil? (date-parser/day-names " January "))))) (deftest ^{:task 3} capture-test (testing "capture numeric month" @@ -228,3 +233,39 @@ (testing "day and month named date" (is (= {:year "1970", :month-name "January", :day "1", :day-name "Thursday"} (date-parser/capture-day-month-name-date "Thursday, January 1, 1970"))))) + +(deftest match-numeric-date-test + (testing "pattern to match numeric date is a regex" + (is (= java.util.regex.Pattern (type date-parser/match-numeric-date)))) + (testing "numeric date matches" + (is (= "01/02/1970" + (first (re-matches date-parser/match-numeric-date "01/02/1970"))))) + (testing "numeric date has named captures" + (is (= ["01/02/1970" "01" "02" "1970"] + (re-matches date-parser/match-numeric-date "01/02/1970")))) + (testing "numeric date with a prefix doesn't match" + (is (nil? (re-matches date-parser/match-numeric-date "The day was 01/02/1970")))) + (testing "numeric date with a suffix doesn't match" + (is (nil? (re-matches date-parser/match-numeric-date "01/02/1970 was the day")))) + (testing "pattern to match month name date is a regex" + (is (= java.util.regex.Pattern (type date-parser/match-month-name-date)))) + (testing "month named date matches" + (is (= "January 1, 1970" + (first (re-matches date-parser/match-month-name-date "January 1, 1970"))))) + (testing "month named date has named captures" + (is (= ["January 1, 1970" "January" "1" "1970"] + (re-matches date-parser/match-month-name-date "January 1, 1970")))) + (testing "month named date with a prefix doesn't match" + (is (nil? (re-matches date-parser/match-month-name-date "The day was January 1, 1970")))) + (testing "month named date with a suffix doesn't match" + (is (nil? (re-matches date-parser/match-month-name-date "January 1, 1970 was the day")))) + (testing "day and month names date matches" + (is (= "Thursday, January 1, 1970" + (first (re-matches date-parser/match-day-month-name-date "Thursday, January 1, 1970"))))) + (testing "month named date has named captures" + (is (= ["Thursday, January 1, 1970" "Thursday" "January" "1" "1970"] + (re-matches date-parser/match-day-month-name-date "Thursday, January 1, 1970")))) + (testing "day and month names date with a prefix doesn't match" + (is (nil? (re-matches date-parser/match-day-month-name-date "The day way Thursday, January 1, 1970")))) + (testing "day and month names date with a suffix doesn't match" + (is (nil? (re-matches date-parser/match-day-month-name-date "Thursday, January 1, 1970 was the day")))))