Skip to content

Commit f3e3dd7

Browse files
committed
feat: add isbn-verifier exercise
1 parent 9c6eb1d commit f3e3dd7

9 files changed

Lines changed: 493 additions & 0 deletions

File tree

config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@
6565
"prerequisites": [],
6666
"difficulty": 1
6767
},
68+
{
69+
"slug": "isbn-verifier",
70+
"name": "ISBN Verifier",
71+
"uuid": "c4e3ea2f-8648-49ba-a9a5-54ba11b5c27e",
72+
"practices": [],
73+
"prerequisites": [],
74+
"difficulty": 3
75+
},
6876
{
6977
"slug": "leap",
7078
"name": "Leap",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Instructions
2+
3+
The [ISBN-10 verification process][isbn-verification] is used to validate book identification numbers.
4+
These normally contain dashes and look like: `3-598-21508-8`
5+
6+
## ISBN
7+
8+
The ISBN-10 format is 9 digits (0 to 9) plus one check character (either a digit or an X only).
9+
In the case the check character is an X, this represents the value '10'.
10+
These may be communicated with or without hyphens, and can be checked for their validity by the following formula:
11+
12+
```text
13+
(d₁ * 10 + d₂ * 9 + d₃ * 8 + d₄ * 7 + d₅ * 6 + d₆ * 5 + d₇ * 4 + d₈ * 3 + d₉ * 2 + d₁₀ * 1) mod 11 == 0
14+
```
15+
16+
If the result is 0, then it is a valid ISBN-10, otherwise it is invalid.
17+
18+
## Example
19+
20+
Let's take the ISBN-10 `3-598-21508-8`.
21+
We plug it in to the formula, and get:
22+
23+
```text
24+
(3 * 10 + 5 * 9 + 9 * 8 + 8 * 7 + 2 * 6 + 1 * 5 + 5 * 4 + 0 * 3 + 8 * 2 + 8 * 1) mod 11 == 0
25+
```
26+
27+
Since the result is 0, this proves that our ISBN is valid.
28+
29+
## Task
30+
31+
Given a string the program should check if the provided string is a valid ISBN-10.
32+
Putting this into place requires some thinking about preprocessing/parsing of the string prior to calculating the check digit for the ISBN.
33+
34+
The program should be able to verify ISBN-10 both with and without separating dashes.
35+
36+
## Caveats
37+
38+
Converting from strings to numbers can be tricky in certain languages.
39+
Now, it's even trickier since the check digit of an ISBN-10 may be 'X' (representing '10').
40+
For instance `3-598-21507-X` is a valid ISBN-10.
41+
42+
[isbn-verification]: https://en.wikipedia.org/wiki/International_Standard_Book_Number
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"authors": [
3+
"vaeng"
4+
],
5+
"files": {
6+
"solution": [
7+
"solution.jai"
8+
],
9+
"test": [
10+
"tests.jai"
11+
],
12+
"example": [
13+
".meta/example.jai"
14+
]
15+
},
16+
"blurb": "Check if a given string is a valid ISBN-10 number.",
17+
"source": "Converting a string into a number and some basic processing utilizing a relatable real world example.",
18+
"source_url": "https://en.wikipedia.org/wiki/International_Standard_Book_Number#ISBN-10_check_digit_calculation"
19+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
is_valid :: (isbn: string) -> bool {
2+
sum: int;
3+
counter: int;
4+
for isbn {
5+
if it == #char "-" {
6+
continue;
7+
}
8+
counter += 1;
9+
if it == #char "X" && counter == 10 {
10+
sum += 10;
11+
} else if it >= #char "0" && it <= #char "9" {
12+
sum += (it - #char "0") * (11 - counter);
13+
} else {
14+
return false;
15+
}
16+
}
17+
return counter == 10 && sum % 11 == 0;
18+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
to_test_case :: (canonical_case: CanonicalCase) -> Test {
2+
_, isbn := get_property(canonical_case.input, "isbn");
3+
4+
return Test.{
5+
name = to_test_name(canonical_case.description_path),
6+
body = tprint("assert_equal(%, is_valid(%));", to_code(canonical_case.expected), to_code(isbn)),
7+
skip = canonical_case.index > 0
8+
};
9+
}
10+
11+
#run run_generator("isbn-verifier", to_test_case);
12+
13+
#import "Basic";
14+
#import,dir "../../../shared/generator";
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
12+
[0caa3eac-d2e3-4c29-8df8-b188bc8c9292]
13+
description = "valid isbn"
14+
15+
[19f76b53-7c24-45f8-87b8-4604d0ccd248]
16+
description = "invalid isbn check digit"
17+
18+
[4164bfee-fb0a-4a1c-9f70-64c6a1903dcd]
19+
description = "valid isbn with a check digit of 10"
20+
21+
[3ed50db1-8982-4423-a993-93174a20825c]
22+
description = "check digit is a character other than X"
23+
24+
[9416f4a5-fe01-4b61-a07b-eb75892ef562]
25+
description = "invalid check digit in isbn is not treated as zero"
26+
27+
[c19ba0c4-014f-4dc3-a63f-ff9aefc9b5ec]
28+
description = "invalid character in isbn is not treated as zero"
29+
30+
[28025280-2c39-4092-9719-f3234b89c627]
31+
description = "X is only valid as a check digit"
32+
33+
[8005b57f-f194-44ee-88d2-a77ac4142591]
34+
description = "only one check digit is allowed"
35+
36+
[fdb14c99-4cf8-43c5-b06d-eb1638eff343]
37+
description = "X is not substituted by the value 10"
38+
39+
[f6294e61-7e79-46b3-977b-f48789a4945b]
40+
description = "valid isbn without separating dashes"
41+
42+
[185ab99b-3a1b-45f3-aeec-b80d80b07f0b]
43+
description = "isbn without separating dashes and X as check digit"
44+
45+
[7725a837-ec8e-4528-a92a-d981dd8cf3e2]
46+
description = "isbn without check digit and dashes"
47+
48+
[47e4dfba-9c20-46ed-9958-4d3190630bdf]
49+
description = "too long isbn and no dashes"
50+
51+
[737f4e91-cbba-4175-95bf-ae630b41fb60]
52+
description = "too short isbn"
53+
54+
[5458a128-a9b6-4ff8-8afb-674e74567cef]
55+
description = "isbn without check digit"
56+
57+
[70b6ad83-d0a2-4ca7-a4d5-a9ab731800f7]
58+
description = "check digit of X should not be used for 0"
59+
60+
[94610459-55ab-4c35-9b93-ff6ea1a8e562]
61+
description = "empty isbn"
62+
63+
[7bff28d4-d770-48cc-80d6-b20b3a0fb46c]
64+
description = "input is 9 characters"
65+
66+
[ed6e8d1b-382c-4081-8326-8b772c581fec]
67+
description = "invalid characters are not ignored after checking length"
68+
69+
[daad3e58-ce00-4395-8a8e-e3eded1cdc86]
70+
description = "invalid characters are not ignored before checking length"
71+
72+
[fb5e48d8-7c03-4bfb-a088-b101df16fdc3]
73+
description = "input is too long but contains a valid isbn"
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Jai, being a very young language, doesn't have a built-in testing framework.
2+
// This file provides basic testing functionality that can be used in exercises
3+
// to run the tests and print a report of which tests passed and which failed.
4+
5+
// This macro is used to compare the expected and actual values in a test.
6+
// It works on Code values for the expected and actual values, which allows it to capture
7+
// the code of the test assertion for reporting purposes.
8+
assert_equal :: (expected: Code, actual: Code, call := #caller_code, loc := #caller_location) #expand {
9+
// The test name is derived from the name of the test procedure, which is the caller of this macro.
10+
test_name := #procedure_name();
11+
12+
// Convert the call to this macro into its original source code form for reporting purposes.
13+
test_code_builder: String_Builder;
14+
print_expression(*test_code_builder, compiler_get_nodes(call));
15+
test_code := builder_to_string(*test_code_builder);
16+
17+
expected_value := #insert expected;
18+
actual_value := #insert actual;
19+
20+
if expected_value == actual_value {
21+
set_test_result(test_name, .PASSED, "", test_code);
22+
} else {
23+
message := sprint("Expected: %, actual: %", expected_value, actual_value);
24+
set_test_result(test_name, .FAILED, message, test_code);
25+
26+
// Short-circuit test execution to avoid cascading failures
27+
`return;
28+
}
29+
}
30+
31+
// This macro allows skipping a test.
32+
// A skipped test's code will _not_ be executed.
33+
skip :: (code: Code) #expand {
34+
// Do not skip the test if the '-run_skipped' build option was provided.
35+
// This is used by the test runner to forcibly run _all_ tests, including skipped tests.
36+
skip_test := !has_build_option("-run_skipped");
37+
38+
if skip_test {
39+
// Get the name of the test procedure being skipped for reporting purposes
40+
test_procedure_call := cast (*Code_Procedure_Call)compiler_get_nodes(code);
41+
test_procedure_identifier := cast (*Code_Ident)test_procedure_call.procedure_expression;
42+
test_name := test_procedure_identifier.name;
43+
set_test_result(test_name, .SKIPPED, "", "");
44+
45+
// As we're skipping the test, we don't insert the original test procedure call, which prevents
46+
// the test from being executed
47+
} else {
48+
// Don't skip the test, so insert the original test procedure call to execute the test as normal.
49+
#insert code;
50+
}
51+
}
52+
53+
set_test_result :: inline (test_name: string, status: TestStatus, message: string, test_code: string) {
54+
test: TestResult = .{test_name, status, message, test_code};
55+
56+
// If there already is a test result for this test name, it means the test has multiple assertions.
57+
// As we short-circuit test execution on failure, the existing test result _must_ be a passing test,
58+
// so we can safely overwrite the existing test result
59+
if context.test_run.tests.count > 0 && context.test_run.tests[context.test_run.tests.count - 1].name == test_name {
60+
context.test_run.tests[context.test_run.tests.count - 1] = test;
61+
} else {
62+
array_add(*context.test_run.tests, test);
63+
}
64+
65+
// If we have one failing test, the whole test run is considered failed
66+
if status == .FAILED context.test_run.status = .FAILED;
67+
}
68+
69+
before_running_tests :: () {
70+
set_build_options_dc(.{do_output=false});
71+
context.started_at = current_time_monotonic();
72+
}
73+
74+
after_running_tests :: () {
75+
if has_build_option("-json") {
76+
write_json_test_report();
77+
} else {
78+
print_test_report();
79+
}
80+
81+
if context.test_run.status == .FAILED {
82+
exit(1);
83+
} else {
84+
exit(0);
85+
}
86+
}
87+
88+
// Print the test results in the Exercism Test Runner output format:
89+
// https://exercism.org/docs/building/tooling/test-runners/interface#h-output-format
90+
write_json_test_report :: () {
91+
// As there is no built-in JSON support in Jai, we manually build a JSON string.
92+
json_builder: String_Builder;
93+
init_string_builder(*json_builder);
94+
95+
append(*json_builder, "{\n");
96+
append(*json_builder, " \"version\": 2,\n");
97+
append(*json_builder, tprint(" \"status\": \"%\",\n", test_status_to_string(context.test_run.status)));
98+
append(*json_builder, " \"tests\": [\n");
99+
100+
for context.test_run.tests {
101+
append(*json_builder, " {\n");
102+
append(*json_builder, tprint(" \"name\": \"%\",\n", json_escape_string(it.name)));
103+
append(*json_builder, tprint(" \"status\": \"%\",\n", test_status_to_string(it.status)));
104+
if it.message.count > 0 {
105+
append(*json_builder, tprint(" \"message\": \"%\",\n", json_escape_string(it.message)));
106+
}
107+
append(*json_builder, tprint(" \"test_code\": \"%\"\n", json_escape_string(it.test_code)));
108+
append(*json_builder, " }");
109+
if it_index < context.test_run.tests.count - 1 {
110+
append(*json_builder, ",");
111+
}
112+
append(*json_builder, "\n");
113+
}
114+
115+
append(*json_builder, " ]\n");
116+
append(*json_builder, "}\n");
117+
118+
json := builder_to_string(*json_builder);
119+
write_entire_file("results.json", json);
120+
}
121+
122+
json_escape_string :: (str: string) -> string {
123+
escaped_builder: String_Builder;
124+
init_string_builder(*escaped_builder);
125+
126+
for str {
127+
if it == {
128+
case #char "\""; append(*escaped_builder, "\\\"");
129+
case #char "\\"; append(*escaped_builder, "\\\\");
130+
case #char "\n"; append(*escaped_builder, "\\n");
131+
case #char "\r"; append(*escaped_builder, "\\r");
132+
case #char "\t"; append(*escaped_builder, "\\t");
133+
case; append(*escaped_builder, it);
134+
}
135+
}
136+
return builder_to_string(*escaped_builder);
137+
}
138+
139+
test_status_to_string :: inline (status: TestStatus) -> string {
140+
if status == {
141+
case .PASSED;
142+
return "pass";
143+
case .FAILED;
144+
return "fail";
145+
case .SKIPPED;
146+
return "skip";
147+
case;
148+
return "error";
149+
};
150+
}
151+
152+
print_test_report :: () {
153+
failed_tests, passed_tests, skipped_tests := 0;
154+
duration := to_float64_seconds(current_time_monotonic() - context.started_at);
155+
156+
for test: context.test_run.tests {
157+
if test.status == {
158+
case .PASSED;
159+
passed_tests += 1;
160+
case .FAILED;
161+
failed_tests += 1;
162+
print("Test '%' failed.\n %\n", test.name, test.message);
163+
case .SKIPPED;
164+
skipped_tests += 1;
165+
}
166+
}
167+
print("Test summary: total: %, failed: %, succeeded: %, skipped: %, duration: % seconds\n", failed_tests + passed_tests + skipped_tests, failed_tests, passed_tests, skipped_tests, duration);
168+
}
169+
170+
has_build_option :: inline (arg: string) -> bool {
171+
for get_build_options().compile_time_command_line if it == arg return true;
172+
return false;
173+
}
174+
175+
TestStatus :: enum u8 {
176+
PASSED;
177+
FAILED;
178+
SKIPPED;
179+
}
180+
181+
TestResult :: struct {
182+
name: string;
183+
status: TestStatus;
184+
message: string;
185+
test_code: string;
186+
};
187+
188+
TestRun :: struct {
189+
version := 2;
190+
status := TestStatus.PASSED;
191+
tests: [..] TestResult;
192+
};
193+
194+
#add_context test_run: TestRun;
195+
#add_context started_at: Apollo_Time;
196+
197+
#import "Basic";
198+
#import "Compiler";
199+
#import "File";
200+
#import "Program_Print";
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
is_valid :: (isbn: string) -> bool {
2+
// TODO: implement this procedure
3+
}

0 commit comments

Comments
 (0)