diff --git a/config.json b/config.json index 2d0bd0f..352a0be 100644 --- a/config.json +++ b/config.json @@ -287,6 +287,14 @@ "prerequisites": [], "difficulty": 5 }, + { + "slug": "roman-numerals", + "name": "Roman Numerals", + "uuid": "0a966ccb-5161-41e5-a5ce-f3ce84b4f1b0", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "sieve", "name": "Sieve of Eratosthenes", diff --git a/exercises/practice/roman-numerals/.docs/instructions.md b/exercises/practice/roman-numerals/.docs/instructions.md new file mode 100644 index 0000000..50e2f5b --- /dev/null +++ b/exercises/practice/roman-numerals/.docs/instructions.md @@ -0,0 +1,12 @@ +# Introduction + +Your task is to convert a number from Arabic numerals to Roman numerals. + +For this exercise, we are only concerned about traditional Roman numerals, in which the largest number is MMMCMXCIX (or 3,999). + +~~~~exercism/note +There are lots of different ways to convert between Arabic and Roman numerals. +We recommend taking a naive approach first to familiarise yourself with the concept of Roman numerals and then search for more efficient methods. + +Make sure to check out our Deep Dive video at the end to explore the different approaches you can take! +~~~~ diff --git a/exercises/practice/roman-numerals/.docs/introduction.md b/exercises/practice/roman-numerals/.docs/introduction.md new file mode 100644 index 0000000..6fd942f --- /dev/null +++ b/exercises/practice/roman-numerals/.docs/introduction.md @@ -0,0 +1,59 @@ +# Description + +Today, most people in the world use Arabic numerals (0–9). +But if you travelled back two thousand years, you'd find that most Europeans were using Roman numerals instead. + +To write a Roman numeral we use the following Latin letters, each of which has a value: + +| M | D | C | L | X | V | I | +| ---- | --- | --- | --- | --- | --- | --- | +| 1000 | 500 | 100 | 50 | 10 | 5 | 1 | + +A Roman numeral is a sequence of these letters, and its value is the sum of the letters' values. +For example, `XVIII` has the value 18 (`10 + 5 + 1 + 1 + 1 = 18`). + +There's one rule that makes things trickier though, and that's that **the same letter cannot be used more than three times in succession**. +That means that we can't express numbers such as 4 with the seemingly natural `IIII`. +Instead, for those numbers, we use a subtraction method between two letters. +So we think of `4` not as `1 + 1 + 1 + 1` but instead as `5 - 1`. +And slightly confusingly to our modern thinking, we write the smaller number first. +This applies only in the following cases: 4 (`IV`), 9 (`IX`), 40 (`XL`), 90 (`XC`), 400 (`CD`) and 900 (`CM`). + +Order matters in Roman numerals! +Letters (and the special compounds above) must be ordered by decreasing value from left to right. + +Here are some examples: + +```text + 105 => CV +---- => -- + 100 => C ++ 5 => V +``` + +```text + 106 => CVI +---- => -- + 100 => C ++ 5 => V ++ 1 => I +``` + +```text + 104 => CIV +---- => --- + 100 => C ++ 4 => IV +``` + +And a final more complex example: + +```text + 1996 => MCMXCVI +----- => ------- + 1000 => M ++ 900 => CM ++ 90 => XC ++ 5 => V ++ 1 => I +``` diff --git a/exercises/practice/roman-numerals/.eslintrc b/exercises/practice/roman-numerals/.eslintrc new file mode 100644 index 0000000..1dbeac2 --- /dev/null +++ b/exercises/practice/roman-numerals/.eslintrc @@ -0,0 +1,18 @@ +{ + "root": true, + "extends": "@exercism/eslint-config-javascript", + "env": { + "jest": true + }, + "overrides": [ + { + "files": [ + "*.spec.js" + ], + "excludedFiles": [ + "custom.spec.js" + ], + "extends": "@exercism/eslint-config-javascript/maintainers" + } + ] +} diff --git a/exercises/practice/roman-numerals/.meta/config.json b/exercises/practice/roman-numerals/.meta/config.json new file mode 100644 index 0000000..017c6bb --- /dev/null +++ b/exercises/practice/roman-numerals/.meta/config.json @@ -0,0 +1,28 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "roman-numerals.wat" + ], + "test": [ + "roman-numerals.spec.js" + ], + "example": [ + ".meta/proof.ci.wat" + ], + "invalidator": [ + "package.json" + ] + }, + "blurb": "Convert modern Arabic numbers into Roman numerals.", + "source": "The Roman Numeral Kata", + "source_url": "https://codingdojo.org/kata/RomanNumerals/", + "custom": { + "version.tests.compatibility": "jest-27", + "flag.tests.task-per-describe": false, + "flag.tests.may-run-long": false, + "flag.tests.includes-optional": false + } +} diff --git a/exercises/practice/roman-numerals/.meta/proof.ci.wat b/exercises/practice/roman-numerals/.meta/proof.ci.wat new file mode 100644 index 0000000..aafa04b --- /dev/null +++ b/exercises/practice/roman-numerals/.meta/proof.ci.wat @@ -0,0 +1,101 @@ +(module + (memory (export "mem") 1) + + (global $C i32 (i32.const 67)) + (global $D i32 (i32.const 68)) + (global $I i32 (i32.const 73)) + (global $L i32 (i32.const 76)) + (global $M i32 (i32.const 77)) + (global $V i32 (i32.const 86)) + (global $X i32 (i32.const 88)) + + ;; returns the end offset + (func $print (param $digit i32) (param $ten i32) (param $five i32) (param $one i32) (param $beginOffset i32) (result i32) + (local $offset i32) + (local $remainder i32) + + (local.set $offset (local.get $beginOffset)) + (local.set $remainder (i32.rem_u + (local.get $digit) + (i32.const 5) + )) + (if (i32.eq (local.get $remainder) (i32.const 4)) (then + (i32.store8 (local.get $offset) (local.get $one)) + (local.set $offset (i32.add (local.get $offset) (i32.const 1))) + (if (i32.eq (local.get $digit) (i32.const 9)) (then + (i32.store8 (local.get $offset) (local.get $ten)) + ) (else + (i32.store8 (local.get $offset) (local.get $five)) + )) + (local.set $offset (i32.add (local.get $offset) (i32.const 1))) + ) (else + (if (i32.ge_u (local.get $digit) (i32.const 5)) (then + (i32.store8 (local.get $offset) (local.get $five)) + (local.set $offset (i32.add (local.get $offset) (i32.const 1))) + )) + (if (local.get $remainder) (then + (loop $ones + (i32.store8 (local.get $offset) (local.get $one)) + (local.set $offset (i32.add (local.get $offset) (i32.const 1))) + (local.set $remainder (i32.sub (local.get $remainder) (i32.const 1))) + (br_if $ones (local.get $remainder)) + ) + )) + )) + (return (local.get $offset)) + ) + + ;; + ;; Convert a number into a Roman numeral + ;; + ;; @param {i32} number - The number to convert + ;; + ;; @returns {(i32,i32)} - Offset and length of result string + ;; in linear memory. + ;; + (func (export "roman") (param $number i32) (result i32 i32) + (local $returnOffset i32) + (local $digit i32) + (local $offset i32) + + (local.set $returnOffset (i32.const 100)) + (local.set $offset (local.get $returnOffset)) + + (local.set $digit (i32.div_u + (local.get $number) + (i32.const 1000) + )) + (local.set $offset + (call $print (local.get $digit) (global.get $X) (global.get $V) (global.get $M) (local.get $offset)) + ) + + (local.set $digit (i32.rem_u + (i32.div_u (local.get $number) (i32.const 100)) + (i32.const 10) + )) + (local.set $offset + (call $print (local.get $digit) (global.get $M) (global.get $D) (global.get $C) (local.get $offset)) + ) + + (local.set $digit (i32.rem_u + (i32.div_u (local.get $number) (i32.const 10)) + (i32.const 10) + )) + (local.set $offset + (call $print (local.get $digit) (global.get $C) (global.get $L) (global.get $X) (local.get $offset)) + ) + + (local.set $digit (i32.rem_u + (local.get $number) + (i32.const 10) + )) + (local.set $offset + (call $print (local.get $digit) (global.get $X) (global.get $V) (global.get $I) (local.get $offset)) + ) + + (return + (local.get $returnOffset) + (i32.sub (local.get $offset) (local.get $returnOffset)) + ) + ) +) diff --git a/exercises/practice/roman-numerals/.meta/tests.toml b/exercises/practice/roman-numerals/.meta/tests.toml new file mode 100644 index 0000000..709011b --- /dev/null +++ b/exercises/practice/roman-numerals/.meta/tests.toml @@ -0,0 +1,91 @@ +# 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. + +[19828a3a-fbf7-4661-8ddd-cbaeee0e2178] +description = "1 is I" + +[f088f064-2d35-4476-9a41-f576da3f7b03] +description = "2 is II" + +[b374a79c-3bea-43e6-8db8-1286f79c7106] +description = "3 is III" + +[05a0a1d4-a140-4db1-82e8-fcc21fdb49bb] +description = "4 is IV" + +[57c0f9ad-5024-46ab-975d-de18c430b290] +description = "5 is V" + +[20a2b47f-e57f-4797-a541-0b3825d7f249] +description = "6 is VI" + +[ff3fb08c-4917-4aab-9f4e-d663491d083d] +description = "9 is IX" + +[6d1d82d5-bf3e-48af-9139-87d7165ed509] +description = "16 is XVI" + +[2bda64ca-7d28-4c56-b08d-16ce65716cf6] +description = "27 is XXVII" + +[a1f812ef-84da-4e02-b4f0-89c907d0962c] +description = "48 is XLVIII" + +[607ead62-23d6-4c11-a396-ef821e2e5f75] +description = "49 is XLIX" + +[d5b283d4-455d-4e68-aacf-add6c4b51915] +description = "59 is LIX" + +[4465ffd5-34dc-44f3-ada5-56f5007b6dad] +description = "66 is LXVI" + +[46b46e5b-24da-4180-bfe2-2ef30b39d0d0] +description = "93 is XCIII" + +[30494be1-9afb-4f84-9d71-db9df18b55e3] +description = "141 is CXLI" + +[267f0207-3c55-459a-b81d-67cec7a46ed9] +description = "163 is CLXIII" + +[902ad132-0b4d-40e3-8597-ba5ed611dd8d] +description = "166 is CLXVI" + +[cdb06885-4485-4d71-8bfb-c9d0f496b404] +description = "402 is CDII" + +[6b71841d-13b2-46b4-ba97-dec28133ea80] +description = "575 is DLXXV" + +[dacb84b9-ea1c-4a61-acbb-ce6b36674906] +description = "666 is DCLXVI" + +[432de891-7fd6-4748-a7f6-156082eeca2f] +description = "911 is CMXI" + +[e6de6d24-f668-41c0-88d7-889c0254d173] +description = "1024 is MXXIV" + +[efbe1d6a-9f98-4eb5-82bc-72753e3ac328] +description = "1666 is MDCLXVI" + +[bb550038-d4eb-4be2-a9ce-f21961ac3bc6] +description = "3000 is MMM" + +[3bc4b41c-c2e6-49d9-9142-420691504336] +description = "3001 is MMMI" + +[2f89cad7-73f6-4d1b-857b-0ef531f68b7e] +description = "3888 is MMMDCCCLXXXVIII" + +[4e18e96b-5fbb-43df-a91b-9cb511fe0856] +description = "3999 is MMMCMXCIX" diff --git a/exercises/practice/roman-numerals/.npmrc b/exercises/practice/roman-numerals/.npmrc new file mode 100644 index 0000000..d26df80 --- /dev/null +++ b/exercises/practice/roman-numerals/.npmrc @@ -0,0 +1 @@ +audit=false diff --git a/exercises/practice/roman-numerals/LICENSE b/exercises/practice/roman-numerals/LICENSE new file mode 100644 index 0000000..90e73be --- /dev/null +++ b/exercises/practice/roman-numerals/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Exercism + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/exercises/practice/roman-numerals/babel.config.js b/exercises/practice/roman-numerals/babel.config.js new file mode 100644 index 0000000..9c17ba5 --- /dev/null +++ b/exercises/practice/roman-numerals/babel.config.js @@ -0,0 +1,4 @@ +export default { + presets: ["@exercism/babel-preset-javascript"], + plugins: [], +}; diff --git a/exercises/practice/roman-numerals/package.json b/exercises/practice/roman-numerals/package.json new file mode 100644 index 0000000..587b052 --- /dev/null +++ b/exercises/practice/roman-numerals/package.json @@ -0,0 +1,34 @@ +{ + "name": "@exercism/wasm-roman-numerals", + "description": "Exercism exercises in WebAssembly.", + "type": "module", + "private": true, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/exercism/wasm", + "directory": "exercises/practice/roman-numerals" + }, + "jest": { + "maxWorkers": 1 + }, + "devDependencies": { + "@babel/core": "^7.23.3", + "@exercism/babel-preset-javascript": "^0.4.0", + "@exercism/eslint-config-javascript": "^0.6.0", + "@types/jest": "^29.5.8", + "@types/node": "^20.9.1", + "babel-jest": "^29.7.0", + "core-js": "^3.33.2", + "eslint": "^8.54.0", + "jest": "^29.7.0" + }, + "dependencies": { + "@exercism/wasm-lib": "^0.2.0" + }, + "scripts": { + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js ./*", + "watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch ./*", + "lint": "eslint ." + } +} diff --git a/exercises/practice/roman-numerals/roman-numerals.spec.js b/exercises/practice/roman-numerals/roman-numerals.spec.js new file mode 100644 index 0000000..2254520 --- /dev/null +++ b/exercises/practice/roman-numerals/roman-numerals.spec.js @@ -0,0 +1,200 @@ +import { compileWat, WasmRunner } from "@exercism/wasm-lib"; + +let wasmModule; +let currentInstance; + +beforeAll(async () => { + try { + const watPath = new URL("./roman-numerals.wat", import.meta.url); + const { buffer } = await compileWat(watPath); + wasmModule = await WebAssembly.compile(buffer); + } catch (err) { + console.log(`Error compiling *.wat: \n${err}`); + process.exit(1); + } +}); + +function roman(input) { + const [outputOffset, outputLength] = currentInstance.exports.roman(input); + + // Decode JS string from returned offset and length + return currentInstance.get_mem_as_utf8(outputOffset, outputLength); +} + +describe("Roman Numerals", () => { + beforeEach(async () => { + currentInstance = null; + if (!wasmModule) { + return Promise.reject(); + } + try { + currentInstance = await new WasmRunner(wasmModule); + return Promise.resolve(); + } catch (err) { + console.log(`Error instantiating WebAssembly module: ${err}`); + return Promise.reject(); + } + }); + + test("1 is I", () => { + const expected = "I"; + const actual = roman(1); + expect(actual).toEqual(expected); + }); + + xtest("2 is II", () => { + const expected = "II"; + const actual = roman(2); + expect(actual).toEqual(expected); + }); + + xtest("3 is III", () => { + const expected = "III"; + const actual = roman(3); + expect(actual).toEqual(expected); + }); + + xtest("4 is IV", () => { + const expected = "IV"; + const actual = roman(4); + expect(actual).toEqual(expected); + }); + + xtest("5 is V", () => { + const expected = "V"; + const actual = roman(5); + expect(actual).toEqual(expected); + }); + + xtest("6 is VI", () => { + const expected = "VI"; + const actual = roman(6); + expect(actual).toEqual(expected); + }); + + xtest("9 is IX", () => { + const expected = "IX"; + const actual = roman(9); + expect(actual).toEqual(expected); + }); + + xtest("16 is XVI", () => { + const expected = "XVI"; + const actual = roman(16); + expect(actual).toEqual(expected); + }); + + xtest("27 is XXVII", () => { + const expected = "XXVII"; + const actual = roman(27); + expect(actual).toEqual(expected); + }); + + xtest("48 is XLVIII", () => { + const expected = "XLVIII"; + const actual = roman(48); + expect(actual).toEqual(expected); + }); + + xtest("49 is XLIX", () => { + const expected = "XLIX"; + const actual = roman(49); + expect(actual).toEqual(expected); + }); + + xtest("59 is LIX", () => { + const expected = "LIX"; + const actual = roman(59); + expect(actual).toEqual(expected); + }); + + xtest("66 is LXVI", () => { + const expected = "LXVI"; + const actual = roman(66); + expect(actual).toEqual(expected); + }); + + xtest("93 is XCIII", () => { + const expected = "XCIII"; + const actual = roman(93); + expect(actual).toEqual(expected); + }); + + xtest("141 is CXLI", () => { + const expected = "CXLI"; + const actual = roman(141); + expect(actual).toEqual(expected); + }); + + xtest("163 is CLXIII", () => { + const expected = "CLXIII"; + const actual = roman(163); + expect(actual).toEqual(expected); + }); + + xtest("166 is CLXVI", () => { + const expected = "CLXVI"; + const actual = roman(166); + expect(actual).toEqual(expected); + }); + + xtest("402 is CDII", () => { + const expected = "CDII"; + const actual = roman(402); + expect(actual).toEqual(expected); + }); + + xtest("575 is DLXXV", () => { + const expected = "DLXXV"; + const actual = roman(575); + expect(actual).toEqual(expected); + }); + + xtest("666 is DCLXVI", () => { + const expected = "DCLXVI"; + const actual = roman(666); + expect(actual).toEqual(expected); + }); + + xtest("911 is CMXI", () => { + const expected = "CMXI"; + const actual = roman(911); + expect(actual).toEqual(expected); + }); + + xtest("1024 is MXXIV", () => { + const expected = "MXXIV"; + const actual = roman(1024); + expect(actual).toEqual(expected); + }); + + xtest("1666 is MDCLXVI", () => { + const expected = "MDCLXVI"; + const actual = roman(1666); + expect(actual).toEqual(expected); + }); + + xtest("3000 is MMM", () => { + const expected = "MMM"; + const actual = roman(3000); + expect(actual).toEqual(expected); + }); + + xtest("3001 is MMMI", () => { + const expected = "MMMI"; + const actual = roman(3001); + expect(actual).toEqual(expected); + }); + + xtest("3888 is MMMDCCCLXXXVIII", () => { + const expected = "MMMDCCCLXXXVIII"; + const actual = roman(3888); + expect(actual).toEqual(expected); + }); + + xtest("3999 is MMMCMXCIX", () => { + const expected = "MMMCMXCIX"; + const actual = roman(3999); + expect(actual).toEqual(expected); + }); +}); diff --git a/exercises/practice/roman-numerals/roman-numerals.wat b/exercises/practice/roman-numerals/roman-numerals.wat new file mode 100644 index 0000000..57a672b --- /dev/null +++ b/exercises/practice/roman-numerals/roman-numerals.wat @@ -0,0 +1,15 @@ +(module + (memory (export "mem") 1) + + ;; + ;; Convert a number into a Roman numeral + ;; + ;; @param {i32} number - The number to convert + ;; + ;; @returns {(i32,i32)} - Offset and length of result string + ;; in linear memory. + ;; + (func (export "roman") (param $number i32) (result i32 i32) + (return (i32.const 0) (i32.const 0)) + ) +)