From 0c91c84d6f9eee38a662e5cd958fb13d004fe282 Mon Sep 17 00:00:00 2001 From: Cool-Katt Date: Sat, 23 Mar 2024 12:19:27 +0200 Subject: [PATCH] [New Exercise] Ledger (#2398) * Adding autogenerated files * Adding scaffolding, tests and solution * Accidentally deleted a key in `config.json` * Update proof.ci.js --- config.json | 10 ++ .../practice/ledger/.docs/instructions.md | 14 ++ exercises/practice/ledger/.eslintrc | 14 ++ exercises/practice/ledger/.gitignore | 5 + exercises/practice/ledger/.meta/config.json | 17 ++ exercises/practice/ledger/.meta/proof.ci.js | 99 +++++++++++ exercises/practice/ledger/.meta/tests.toml | 48 ++++++ exercises/practice/ledger/.npmrc | 1 + exercises/practice/ledger/LICENSE | 21 +++ exercises/practice/ledger/babel.config.js | 4 + exercises/practice/ledger/ledger.js | 163 ++++++++++++++++++ exercises/practice/ledger/ledger.spec.js | 139 +++++++++++++++ exercises/practice/ledger/package.json | 34 ++++ 13 files changed, 569 insertions(+) create mode 100644 exercises/practice/ledger/.docs/instructions.md create mode 100644 exercises/practice/ledger/.eslintrc create mode 100644 exercises/practice/ledger/.gitignore create mode 100644 exercises/practice/ledger/.meta/config.json create mode 100644 exercises/practice/ledger/.meta/proof.ci.js create mode 100644 exercises/practice/ledger/.meta/tests.toml create mode 100644 exercises/practice/ledger/.npmrc create mode 100644 exercises/practice/ledger/LICENSE create mode 100644 exercises/practice/ledger/babel.config.js create mode 100644 exercises/practice/ledger/ledger.js create mode 100644 exercises/practice/ledger/ledger.spec.js create mode 100644 exercises/practice/ledger/package.json diff --git a/config.json b/config.json index d4b1d7a260..20350270e7 100644 --- a/config.json +++ b/config.json @@ -2598,6 +2598,16 @@ "strings" ], "difficulty": 2 + }, + { + "slug": "ledger", + "name": "Ledger", + "uuid": "8716b347-e18f-48a6-b373-426cc4ca98cb", + "practices": [], + "prerequisites": [ + "string-formatting" + ], + "difficulty": 5 } ] }, diff --git a/exercises/practice/ledger/.docs/instructions.md b/exercises/practice/ledger/.docs/instructions.md new file mode 100644 index 0000000000..a53e5c15e3 --- /dev/null +++ b/exercises/practice/ledger/.docs/instructions.md @@ -0,0 +1,14 @@ +# Instructions + +Refactor a ledger printer. + +The ledger exercise is a refactoring exercise. +There is code that prints a nicely formatted ledger, given a locale (American or Dutch) and a currency (US dollar or euro). +The code however is rather badly written, though (somewhat surprisingly) it consistently passes the test suite. + +Rewrite this code. +Remember that in refactoring the trick is to make small steps that keep the tests passing. +That way you can always quickly go back to a working version. +Version control tools like git can help here as well. + +Please keep a log of what changes you've made and make a comment on the exercise containing that log, this will help reviewers. diff --git a/exercises/practice/ledger/.eslintrc b/exercises/practice/ledger/.eslintrc new file mode 100644 index 0000000000..1d4446029c --- /dev/null +++ b/exercises/practice/ledger/.eslintrc @@ -0,0 +1,14 @@ +{ + "root": true, + "extends": "@exercism/eslint-config-javascript", + "env": { + "jest": true + }, + "overrides": [ + { + "files": [".meta/proof.ci.js", ".meta/exemplar.js", "*.spec.js"], + "excludedFiles": ["custom.spec.js"], + "extends": "@exercism/eslint-config-javascript/maintainers" + } + ] +} diff --git a/exercises/practice/ledger/.gitignore b/exercises/practice/ledger/.gitignore new file mode 100644 index 0000000000..31c57dd53a --- /dev/null +++ b/exercises/practice/ledger/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/bin/configlet +/bin/configlet.exe +/pnpm-lock.yaml +/yarn.lock diff --git a/exercises/practice/ledger/.meta/config.json b/exercises/practice/ledger/.meta/config.json new file mode 100644 index 0000000000..4e0ae63eb6 --- /dev/null +++ b/exercises/practice/ledger/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "Cool-Katt" + ], + "files": { + "solution": [ + "ledger.js" + ], + "test": [ + "ledger.spec.js" + ], + "example": [ + ".meta/proof.ci.js" + ] + }, + "blurb": "Refactor a ledger printer." +} diff --git a/exercises/practice/ledger/.meta/proof.ci.js b/exercises/practice/ledger/.meta/proof.ci.js new file mode 100644 index 0000000000..bfb4709ed3 --- /dev/null +++ b/exercises/practice/ledger/.meta/proof.ci.js @@ -0,0 +1,99 @@ +class LedgerEntry { + constructor(date, description, change) { + this.date = new Date(date); + this.description = description; + this.change = change; + } +} + +class FormattedLedgerEntry { + constructor(entry, locale, dateFormat, currencyFormat) { + this.entry = entry; + this.locale = locale; + this.dateFormat = dateFormat; + this.currencyFormat = currencyFormat; + } + + date() { + return this.entry.date.toLocaleDateString(this.locale, this.dateFormat); + } + + description(length = 25) { + if (this.entry.description.length > length) { + return `${this.entry.description.substring(0, length - 3)}...`; + } + + return this.entry.description.padEnd(length, ' '); + } + + change(offset = 13) { + const formatted = (this.entry.change / 100).toLocaleString( + this.locale, + this.currencyFormat, + ); + + const trailingSpace = formatted.includes(')') ? '' : ' '; + return `${formatted}${trailingSpace}`.padStart(offset, ' '); + } + + toTableRow() { + return [this.date(), this.description(), this.change()].join(' | '); + } +} + +const OPTIONS = { + HEADERS: { + 'en-US': ['Date', 'Description', 'Change'], + 'nl-NL': ['Datum', 'Omschrijving', 'Verandering'], + }, + headerRow: function (locale) { + const [date, description, change] = this.HEADERS[locale]; + return [ + date.padEnd(10, ' '), + description.padEnd(25, ' '), + change.padEnd(13, ' '), + ].join(' | '); + }, + dateFormatOptions: function () { + return { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }; + }, + currencyFormatOptions: function (currency, locale) { + return { + style: 'currency', + currency: currency, + currencySign: locale === 'en-US' ? 'accounting' : 'standard', + currencyDisplay: locale === 'en-US' ? 'symbol' : 'narrowSymbol', + }; + }, +}; + +export const createEntry = (date, description, change) => + new LedgerEntry(date, description, change); + +export function formatEntries(currency, locale, entries) { + let dateFormat = OPTIONS.dateFormatOptions(); + let currencyFormat = OPTIONS.currencyFormatOptions(currency, locale); + + let rows = entries + .sort( + (a, b) => + a.date - b.date || + a.change - b.change || + a.description.localeCompare(b.description), + ) + .map((entry) => { + let formattedEntry = new FormattedLedgerEntry( + entry, + locale, + dateFormat, + currencyFormat, + ); + return formattedEntry.toTableRow(); + }); + + return [OPTIONS.headerRow(locale), ...rows].join('\n'); +} diff --git a/exercises/practice/ledger/.meta/tests.toml b/exercises/practice/ledger/.meta/tests.toml new file mode 100644 index 0000000000..4ea45ceb12 --- /dev/null +++ b/exercises/practice/ledger/.meta/tests.toml @@ -0,0 +1,48 @@ +# 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. + +[d131ecae-a30e-436c-b8f3-858039a27234] +description = "empty ledger" + +[ce4618d2-9379-4eca-b207-9df1c4ec8aaa] +description = "one entry" + +[8d02e9cb-e6ee-4b77-9ce4-e5aec8eb5ccb] +description = "credit and debit" + +[502c4106-0371-4e7c-a7d8-9ce33f16ccb1] +description = "multiple entries on same date ordered by description" +include = false + +[29dd3659-6c2d-4380-94a8-6d96086e28e1] +description = "final order tie breaker is change" + +[9b9712a6-f779-4f5c-a759-af65615fcbb9] +description = "overlong description is truncated" + +[67318aad-af53-4f3d-aa19-1293b4d4c924] +description = "euros" + +[bdc499b6-51f5-4117-95f2-43cb6737208e] +description = "Dutch locale" + +[86591cd4-1379-4208-ae54-0ee2652b4670] +description = "Dutch locale and euros" + +[876bcec8-d7d7-4ba4-82bd-b836ac87c5d2] +description = "Dutch negative number with 3 digits before decimal point" + +[29670d1c-56be-492a-9c5e-427e4b766309] +description = "American negative number with 3 digits before decimal point" + +[9c70709f-cbbd-4b3b-b367-81d7c6101de4] +description = "multiple entries on same date ordered by description" +reimplements = "502c4106-0371-4e7c-a7d8-9ce33f16ccb1" diff --git a/exercises/practice/ledger/.npmrc b/exercises/practice/ledger/.npmrc new file mode 100644 index 0000000000..d26df800bb --- /dev/null +++ b/exercises/practice/ledger/.npmrc @@ -0,0 +1 @@ +audit=false diff --git a/exercises/practice/ledger/LICENSE b/exercises/practice/ledger/LICENSE new file mode 100644 index 0000000000..90e73be03b --- /dev/null +++ b/exercises/practice/ledger/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/ledger/babel.config.js b/exercises/practice/ledger/babel.config.js new file mode 100644 index 0000000000..b781d5a667 --- /dev/null +++ b/exercises/practice/ledger/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ['@exercism/babel-preset-javascript'], + plugins: [], +}; diff --git a/exercises/practice/ledger/ledger.js b/exercises/practice/ledger/ledger.js new file mode 100644 index 0000000000..97bf4e0175 --- /dev/null +++ b/exercises/practice/ledger/ledger.js @@ -0,0 +1,163 @@ +class LedgerEntry { + constructor() { + this.date = undefined; + this.description = undefined; + this.change = undefined; + } +} + +export function createEntry(date, description, change) { + let entry = new LedgerEntry(); + entry.date = new Date(date); + entry.description = description; + entry.change = change; + return entry; +} + +export function formatEntries(currency, locale, entries) { + let table = ''; + if (locale === 'en-US') { + // Generate Header Row + table += + 'Date'.padEnd(10, ' ') + + ' | ' + + 'Description'.padEnd(25, ' ') + + ' | ' + + 'Change'.padEnd(13, ' ') + + '\n'; + + // Sort entries + entries.sort( + (a, b) => + a.date - b.date || + a.change - b.change || + a.description.localeCompare(b.description), + ); + + entries.forEach((entry) => { + // Write entry date to table + const dateStr = `${(entry.date.getMonth() + 1) + .toString() + .padStart(2, '0')}/${entry.date + .getDate() + .toString() + .padStart(2, '0')}/${entry.date.getFullYear()}`; + table += `${dateStr} | `; + + // Write entry description to table + const truncatedDescription = + entry.description.length > 25 + ? `${entry.description.substring(0, 22)}...` + : entry.description.padEnd(25, ' '); + table += `${truncatedDescription} | `; + + // Write entry change to table + let changeStr = ''; + if (currency === 'USD') { + let formatingOptions = { + style: 'currency', + currency: 'USD', + //currencySign: 'accounting', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }; + if (entry.change < 0) { + changeStr = `(${Math.abs(entry.change / 100).toLocaleString( + 'en-US', + formatingOptions, + )})`; + } else { + changeStr = `${(entry.change / 100).toLocaleString( + 'en-US', + formatingOptions, + )} `; + } + } else if (currency === 'EUR') { + let formatingOptions = { + style: 'currency', + currency: 'EUR', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }; + if (entry.change < 0) { + changeStr = `(${Math.abs(entry.change / 100).toLocaleString( + 'en-US', + formatingOptions, + )})`; + } else { + changeStr = `${(entry.change / 100).toLocaleString( + 'en-US', + formatingOptions, + )} `; + } + } + table += changeStr.padStart(13, ' '); + table += '\n'; + }); + } else if (locale === 'nl-NL') { + // Generate Header Row + table += + 'Datum'.padEnd(10, ' ') + + ' | ' + + 'Omschrijving'.padEnd(25, ' ') + + ' | ' + + 'Verandering'.padEnd(13, ' ') + + '\n'; + + // Sort entries + entries.sort( + (a, b) => + a.date - b.date || + a.change - b.change || + a.description.localeCompare(b.description), + ); + + entries.forEach((entry) => { + // Write entry date to table + const dateStr = `${entry.date.getDate().toString().padStart(2, '0')}-${( + entry.date.getMonth() + 1 + ) + .toString() + .padStart(2, '0')}-${entry.date.getFullYear()}`; + table += `${dateStr} | `; + + // Write entry description to table + const truncatedDescription = + entry.description.length > 25 + ? `${entry.description.substring(0, 22)}...` + : entry.description.padEnd(25, ' '); + table += `${truncatedDescription} | `; + + // Write entry change to table + let changeStr = ''; + if (currency === 'USD') { + let formatingOptions = { + style: 'currency', + currency: 'USD', + currencyDisplay: 'narrowSymbol', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }; + changeStr = `${(entry.change / 100).toLocaleString( + 'nl-NL', + formatingOptions, + )} `; + } else if (currency === 'EUR') { + let formatingOptions = { + style: 'currency', + currency: 'EUR', + currencyDisplay: 'narrowSymbol', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }; + changeStr = `${(entry.change / 100).toLocaleString( + 'nl-NL', + formatingOptions, + )} `; + } + table += changeStr.padStart(13, ' '); + table += '\n'; + }); + } + return table.replace(/\n$/, ''); +} diff --git a/exercises/practice/ledger/ledger.spec.js b/exercises/practice/ledger/ledger.spec.js new file mode 100644 index 0000000000..95cfe852fd --- /dev/null +++ b/exercises/practice/ledger/ledger.spec.js @@ -0,0 +1,139 @@ +import { createEntry, formatEntries } from './ledger'; + +describe('Ledger', () => { + test('empty ledger', () => { + let currency = 'USD'; + let locale = 'en-US'; + let entries = []; + let expected = [ + 'Date | Description | Change ', + ].join('\n'); + expect(formatEntries(currency, locale, entries)).toEqual(expected); + }); + + xtest('one entry', () => { + let currency = 'USD'; + let locale = 'en-US'; + let entries = [createEntry('2015-01-01', 'Buy present', -1000)]; + let expected = [ + 'Date | Description | Change ', + '01/01/2015 | Buy present | ($10.00)', + ].join('\n'); + expect(formatEntries(currency, locale, entries)).toEqual(expected); + }); + + xtest('credit and debit', () => { + let currency = 'USD'; + let locale = 'en-US'; + let entries = [ + createEntry('2015-01-02', 'Get present', 1000), + createEntry('2015-01-01', 'Buy present', -1000), + ]; + let expected = [ + 'Date | Description | Change ', + '01/01/2015 | Buy present | ($10.00)', + '01/02/2015 | Get present | $10.00 ', + ].join('\n'); + expect(formatEntries(currency, locale, entries)).toEqual(expected); + }); + + xtest('final order tie breaker is change', () => { + let currency = 'USD'; + let locale = 'en-US'; + let entries = [ + createEntry('2015-01-01', 'Something', 0), + createEntry('2015-01-01', 'Something', -1), + createEntry('2015-01-01', 'Something', 1), + ]; + let expected = [ + 'Date | Description | Change ', + '01/01/2015 | Something | ($0.01)', + '01/01/2015 | Something | $0.00 ', + '01/01/2015 | Something | $0.01 ', + ].join('\n'); + expect(formatEntries(currency, locale, entries)).toEqual(expected); + }); + + xtest('overlong description is truncated', () => { + let currency = 'USD'; + let locale = 'en-US'; + let entries = [ + createEntry('2015-01-01', 'Freude schoner Gotterfunken', -123456), + ]; + let expected = [ + 'Date | Description | Change ', + '01/01/2015 | Freude schoner Gotterf... | ($1,234.56)', + ].join('\n'); + expect(formatEntries(currency, locale, entries)).toEqual(expected); + }); + + xtest('euros', () => { + let currency = 'EUR'; + let locale = 'en-US'; + let entries = [createEntry('2015-01-01', 'Buy present', -1000)]; + let expected = [ + 'Date | Description | Change ', + '01/01/2015 | Buy present | (€10.00)', + ].join('\n'); + expect(formatEntries(currency, locale, entries)).toEqual(expected); + }); + + xtest('Dutch locale', () => { + let currency = 'USD'; + let locale = 'nl-NL'; + let entries = [createEntry('2015-03-12', 'Buy present', 123456)]; + let expected = [ + 'Datum | Omschrijving | Verandering ', + '12-03-2015 | Buy present | $ 1.234,56 ', + ].join('\n'); + expect(formatEntries(currency, locale, entries)).toEqual(expected); + }); + + xtest('Dutch locale and euros', () => { + let currency = 'EUR'; + let locale = 'nl-NL'; + let entries = [createEntry('2015-03-12', 'Buy present', 123456)]; + let expected = [ + 'Datum | Omschrijving | Verandering ', + '12-03-2015 | Buy present | € 1.234,56 ', + ].join('\n'); + expect(formatEntries(currency, locale, entries)).toEqual(expected); + }); + + xtest('Dutch negative number with 3 digits before decimal point', () => { + let currency = 'USD'; + let locale = 'nl-NL'; + let entries = [createEntry('2015-03-12', 'Buy present', -12345)]; + let expected = [ + 'Datum | Omschrijving | Verandering ', + '12-03-2015 | Buy present | $ -123,45 ', + ].join('\n'); + expect(formatEntries(currency, locale, entries)).toEqual(expected); + }); + + xtest('American negative number with 3 digits before decimal point', () => { + let currency = 'USD'; + let locale = 'en-US'; + let entries = [createEntry('2015-03-12', 'Buy present', -12345)]; + let expected = [ + 'Date | Description | Change ', + '03/12/2015 | Buy present | ($123.45)', + ].join('\n'); + expect(formatEntries(currency, locale, entries)).toEqual(expected); + }); + + xtest('multiple entries on same date ordered by description', () => { + let currency = 'USD'; + let locale = 'en-US'; + let entries = [ + createEntry('2015-01-01', 'Get present', 1000), + createEntry('2015-01-01', 'Buy present', -1000), + ]; + let expected = [ + 'Date | Description | Change ', + '01/01/2015 | Buy present | ($10.00)', + '01/01/2015 | Get present | $10.00 ', + ].join('\n'); + expect(formatEntries(currency, locale, entries)).toEqual(expected); + }); +}); diff --git a/exercises/practice/ledger/package.json b/exercises/practice/ledger/package.json new file mode 100644 index 0000000000..8ef6d4f376 --- /dev/null +++ b/exercises/practice/ledger/package.json @@ -0,0 +1,34 @@ +{ + "name": "@exercism/javascript-ledger", + "description": "Exercism practice exercise on ledger", + "author": "Katrina Owen", + "contributors": [ + "Cool-Katt (https://github.com/Cool-Katt)", + "Derk-Jan Karrenbeld (https://derk-jan.com)", + "Tejas Bubane (https://tejasbubane.github.io/)" + ], + "private": true, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/exercism/javascript", + "directory": "exercises/practice/ledger" + }, + "devDependencies": { + "@babel/core": "^7.23.0", + "@exercism/babel-preset-javascript": "^0.2.1", + "@exercism/eslint-config-javascript": "^0.6.0", + "@types/jest": "^29.5.4", + "@types/node": "^20.5.6", + "babel-jest": "^29.6.4", + "core-js": "~3.32.2", + "eslint": "^8.49.0", + "jest": "^29.7.0" + }, + "dependencies": {}, + "scripts": { + "test": "jest ./*", + "watch": "jest --watch ./*", + "lint": "eslint ." + } +}