From 0c3373b87e3cd3a519edbdc2c2efd89dc8fe0815 Mon Sep 17 00:00:00 2001 From: Danylo Negrienko Date: Sat, 6 Jul 2024 13:16:07 -0400 Subject: [PATCH] isbn-verifier --- elixir/isbn-verifier/.exercism/config.json | 27 ++++ elixir/isbn-verifier/.exercism/metadata.json | 1 + elixir/isbn-verifier/.formatter.exs | 4 + elixir/isbn-verifier/.gitignore | 24 ++++ elixir/isbn-verifier/HELP.md | 75 ++++++++++ elixir/isbn-verifier/README.md | 66 +++++++++ elixir/isbn-verifier/lib/isbn_verifier.ex | 41 ++++++ elixir/isbn-verifier/mix.exs | 28 ++++ .../isbn-verifier/test/isbn_verifier_test.exs | 130 ++++++++++++++++++ elixir/isbn-verifier/test/test_helper.exs | 2 + 10 files changed, 398 insertions(+) create mode 100644 elixir/isbn-verifier/.exercism/config.json create mode 100644 elixir/isbn-verifier/.exercism/metadata.json create mode 100644 elixir/isbn-verifier/.formatter.exs create mode 100644 elixir/isbn-verifier/.gitignore create mode 100644 elixir/isbn-verifier/HELP.md create mode 100644 elixir/isbn-verifier/README.md create mode 100644 elixir/isbn-verifier/lib/isbn_verifier.ex create mode 100644 elixir/isbn-verifier/mix.exs create mode 100644 elixir/isbn-verifier/test/isbn_verifier_test.exs create mode 100644 elixir/isbn-verifier/test/test_helper.exs diff --git a/elixir/isbn-verifier/.exercism/config.json b/elixir/isbn-verifier/.exercism/config.json new file mode 100644 index 0000000..86bf1f0 --- /dev/null +++ b/elixir/isbn-verifier/.exercism/config.json @@ -0,0 +1,27 @@ +{ + "authors": [ + "stfnsr" + ], + "contributors": [ + "angelikatyborska", + "Cohen-Carlisle", + "devonestes", + "herminiotorres", + "neenjaw", + "sotojuan" + ], + "files": { + "solution": [ + "lib/isbn_verifier.ex" + ], + "test": [ + "test/isbn_verifier_test.exs" + ], + "example": [ + ".meta/example.ex" + ] + }, + "blurb": "Check if a given string is a valid ISBN-10 number.", + "source": "Converting a string into a number and some basic processing utilizing a relatable real world example.", + "source_url": "https://en.wikipedia.org/wiki/International_Standard_Book_Number#ISBN-10_check_digit_calculation" +} diff --git a/elixir/isbn-verifier/.exercism/metadata.json b/elixir/isbn-verifier/.exercism/metadata.json new file mode 100644 index 0000000..3b05260 --- /dev/null +++ b/elixir/isbn-verifier/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"elixir","exercise":"isbn-verifier","id":"6c4a3518e48f4c1595cc3d84835d58f8","url":"https://exercism.org/tracks/elixir/exercises/isbn-verifier","handle":"negrienko","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/elixir/isbn-verifier/.formatter.exs b/elixir/isbn-verifier/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/elixir/isbn-verifier/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/elixir/isbn-verifier/.gitignore b/elixir/isbn-verifier/.gitignore new file mode 100644 index 0000000..93c6590 --- /dev/null +++ b/elixir/isbn-verifier/.gitignore @@ -0,0 +1,24 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +isbn_verifier-*.tar + diff --git a/elixir/isbn-verifier/HELP.md b/elixir/isbn-verifier/HELP.md new file mode 100644 index 0000000..83574ba --- /dev/null +++ b/elixir/isbn-verifier/HELP.md @@ -0,0 +1,75 @@ +# Help + +## Running the tests + +From the terminal, change to the base directory of the exercise then execute the tests with: + +```bash +$ mix test +``` + +This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs` + +Documentation: + +* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html) +* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html) + +## Pending tests + +In test suites of practice exercises, all but the first test have been tagged to be skipped. + +Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol. + +For example: + +```elixir +# @tag :pending +test "shouting" do + assert Bob.hey("WATCH OUT!") == "Whoa, chill out!" +end +``` + +If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command: + +```bash +$ mix test --include pending +``` + +Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`. + +```elixir +# ExUnit.configure(exclude: :pending, trace: true) +``` + +## Useful `mix test` options + +* `test/.exs:LINENUM` - runs only a single test, the test from `.exs` whose definition is on line `LINENUM` +* `--failed` - runs only tests that failed the last time they ran +* `--max-failures` - the suite stops evaluating tests when this number of test failures +is reached +* `--seed 0` - disables randomization so the tests in a single file will always be ran +in the same order they were defined in + +## Submitting your solution + +You can submit your solution using the `exercism submit lib/isbn_verifier.ex` command. +This command will upload your solution to the Exercism website and print the solution page's URL. + +It's possible to submit an incomplete solution which allows you to: + +- See how others have completed the exercise +- Request help from a mentor + +## Need to get help? + +If you'd like help solving the exercise, check the following pages: + +- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir) +- The [Elixir track's programming category on the forum](https://forum.exercism.org/c/programming/elixir) +- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5) +- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs) + +Should those resources not suffice, you could submit your (incomplete) solution to request mentoring. + +If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found. \ No newline at end of file diff --git a/elixir/isbn-verifier/README.md b/elixir/isbn-verifier/README.md new file mode 100644 index 0000000..2965ba5 --- /dev/null +++ b/elixir/isbn-verifier/README.md @@ -0,0 +1,66 @@ +# ISBN Verifier + +Welcome to ISBN Verifier on Exercism's Elixir Track. +If you need help running the tests or submitting your code, check out `HELP.md`. + +## Instructions + +The [ISBN-10 verification process][isbn-verification] is used to validate book identification numbers. +These normally contain dashes and look like: `3-598-21508-8` + +## ISBN + +The ISBN-10 format is 9 digits (0 to 9) plus one check character (either a digit or an X only). +In the case the check character is an X, this represents the value '10'. +These may be communicated with or without hyphens, and can be checked for their validity by the following formula: + +```text +(d₁ * 10 + d₂ * 9 + d₃ * 8 + d₄ * 7 + d₅ * 6 + d₆ * 5 + d₇ * 4 + d₈ * 3 + d₉ * 2 + d₁₀ * 1) mod 11 == 0 +``` + +If the result is 0, then it is a valid ISBN-10, otherwise it is invalid. + +## Example + +Let's take the ISBN-10 `3-598-21508-8`. +We plug it in to the formula, and get: + +```text +(3 * 10 + 5 * 9 + 9 * 8 + 8 * 7 + 2 * 6 + 1 * 5 + 5 * 4 + 0 * 3 + 8 * 2 + 8 * 1) mod 11 == 0 +``` + +Since the result is 0, this proves that our ISBN is valid. + +## Task + +Given a string the program should check if the provided string is a valid ISBN-10. +Putting this into place requires some thinking about preprocessing/parsing of the string prior to calculating the check digit for the ISBN. + +The program should be able to verify ISBN-10 both with and without separating dashes. + +## Caveats + +Converting from strings to numbers can be tricky in certain languages. +Now, it's even trickier since the check digit of an ISBN-10 may be 'X' (representing '10'). +For instance `3-598-21507-X` is a valid ISBN-10. + +[isbn-verification]: https://en.wikipedia.org/wiki/International_Standard_Book_Number + +## Source + +### Created by + +- @stfnsr + +### Contributed to by + +- @angelikatyborska +- @Cohen-Carlisle +- @devonestes +- @herminiotorres +- @neenjaw +- @sotojuan + +### Based on + +Converting a string into a number and some basic processing utilizing a relatable real world example. - https://en.wikipedia.org/wiki/International_Standard_Book_Number#ISBN-10_check_digit_calculation \ No newline at end of file diff --git a/elixir/isbn-verifier/lib/isbn_verifier.ex b/elixir/isbn-verifier/lib/isbn_verifier.ex new file mode 100644 index 0000000..2178932 --- /dev/null +++ b/elixir/isbn-verifier/lib/isbn_verifier.ex @@ -0,0 +1,41 @@ +defmodule IsbnVerifier do + @doc """ + Checks if a string is a valid ISBN-10 identifier + + ## Examples + + iex> IsbnVerifier.isbn?("3-598-21507-X") + true + + iex> IsbnVerifier.isbn?("3-598-2K507-0") + false + + """ + + @regex ~r/^(?\d{9})(?[0-9X])$/ + + @spec isbn?(String.t()) :: boolean + def isbn?(isbn) do + with isbn_without_dashes <- String.replace(isbn, "-", ""), + %{"digits" => digits, "check_digit" => check_digit} <- + Regex.named_captures(@regex, isbn_without_dashes) do + rem(check_sum(digits) + check_digit(check_digit), 11) == 0 + else + _ -> false + end + end + + defp check_sum(digits) do + {1, check_sum} = + digits + |> String.graphemes() + |> Enum.reduce({10, 0}, fn digit, {index, acc} -> + {index - 1, acc + String.to_integer(digit) * index} + end) + + check_sum + end + + defp check_digit("X"), do: 10 + defp check_digit(string), do: String.to_integer(string) +end diff --git a/elixir/isbn-verifier/mix.exs b/elixir/isbn-verifier/mix.exs new file mode 100644 index 0000000..fb45f09 --- /dev/null +++ b/elixir/isbn-verifier/mix.exs @@ -0,0 +1,28 @@ +defmodule IsbnVerifier.MixProject do + use Mix.Project + + def project do + [ + app: :isbn_verifier, + version: "0.1.0", + # elixir: "~> 1.8", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/elixir/isbn-verifier/test/isbn_verifier_test.exs b/elixir/isbn-verifier/test/isbn_verifier_test.exs new file mode 100644 index 0000000..8643405 --- /dev/null +++ b/elixir/isbn-verifier/test/isbn_verifier_test.exs @@ -0,0 +1,130 @@ +defmodule IsbnVerifierTest do + use ExUnit.Case + + test "valid isbn number" do + assert IsbnVerifier.isbn?("3-598-21508-8") + end + + test "invalid isbn check digit" do + refute IsbnVerifier.isbn?("3-598-21508-9") + end + + test "valid isbn number with a check digit of 10" do + assert IsbnVerifier.isbn?("3-598-21507-X") + end + + test "check digit is a character other than X" do + refute IsbnVerifier.isbn?("3-598-21507-A") + end + + test "check digit in isbn is not treated as zero" do + refute IsbnVerifier.isbn?("4-598-21507-B") + end + + test "invalid character in isbn" do + refute IsbnVerifier.isbn?("3-598-P1581-X") + end + + test "X is only valid as a check digit" do + refute IsbnVerifier.isbn?("3-598-2X507-9") + end + + test "valid isbn without separating dashes" do + assert IsbnVerifier.isbn?("3598215088") + end + + test "isbn without separating dashes and X as check digit" do + assert IsbnVerifier.isbn?("359821507X") + end + + test "isbn without check digit and dashes" do + refute IsbnVerifier.isbn?("359821507") + end + + test "too long isbn and no dashes" do + refute IsbnVerifier.isbn?("3598215078X") + end + + test "too short isbn" do + refute IsbnVerifier.isbn?("00") + end + + test "isbn without check digit" do + refute IsbnVerifier.isbn?("3-598-21507") + end + + test "too long isbn" do + refute IsbnVerifier.isbn?("3-598-21507-XA") + end + + test "check digit of X should not be used for 0" do + refute IsbnVerifier.isbn?("3-598-21515-X") + end + + test "input is 9 characters" do + refute IsbnVerifier.isbn?("134456729") + end + + test "invalid characters are not ignored" do + refute IsbnVerifier.isbn?("3132P34035") + end + + test "invalid characters are not ignored before checking length" do + refute IsbnVerifier.isbn?("3598P215088") + end + + test "input is too long but contains a valid isbn" do + refute IsbnVerifier.isbn?("98245726788") + end + + # Test cases from international ISBN to test variable dash placement + # Adapted from https://en.wikipedia.org/wiki/International_Standard_Book_Number#Registrant_element + + test "Qatar NCCAH, Doha" do + assert IsbnVerifier.isbn?("99921-58-10-7") + end + + test "Singapore World Scientific" do + assert IsbnVerifier.isbn?("9971-5-0210-0") + end + + test "Greece Sigma Publications" do + assert IsbnVerifier.isbn?("960-425-059-0") + end + + test "Czech Republic; Slovakia Taita Publishers" do + assert IsbnVerifier.isbn?("80-902734-1-6") + end + + test "Brazil Companhia das Letras" do + assert IsbnVerifier.isbn?("85-359-0277-5") + end + + test "English-speaking area Simon Wallenberg Press" do + assert IsbnVerifier.isbn?("1-84356-028-3") + end + + test "English-speaking area Scribner" do + assert IsbnVerifier.isbn?("0-684-84328-5") + end + + test "English-speaking area Frederick Ungar" do + assert IsbnVerifier.isbn?("0-8044-2957-X") + end + + test "English-speaking area J. A. Allen & Co." do + assert IsbnVerifier.isbn?("0-85131-041-9") + end + + test "English-speaking area Edupedia Publications Pvt Ltd." do + assert IsbnVerifier.isbn?("93-86954-21-4") + end + + test "English-speaking area Willmann-Bell" do + assert IsbnVerifier.isbn?("0-943396-04-2") + end + + test "English-speaking area KT Publishing" do + assert IsbnVerifier.isbn?("0-9752298-0-X") + end +end diff --git a/elixir/isbn-verifier/test/test_helper.exs b/elixir/isbn-verifier/test/test_helper.exs new file mode 100644 index 0000000..35fc5bf --- /dev/null +++ b/elixir/isbn-verifier/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true)