diff --git a/config.json b/config.json index bea37047..9e064b94 100644 --- a/config.json +++ b/config.json @@ -876,6 +876,14 @@ "practices": [], "prerequisites": [], "difficulty": 5 + }, + { + "slug": "ledger", + "name": "Ledger", + "uuid": "cccabb0a-b5b5-4f18-aead-18f62af4a89f", + "practices": [], + "prerequisites": [], + "difficulty": 4 } ], "foregone": [ diff --git a/exercises/practice/ledger/.docs/instructions.append.md b/exercises/practice/ledger/.docs/instructions.append.md new file mode 100644 index 00000000..97c9e71f --- /dev/null +++ b/exercises/practice/ledger/.docs/instructions.append.md @@ -0,0 +1,4 @@ +# Instructions append + +When working with the ledger, treat the `e` symbol as a stand-in for the Euro currency symbol (€). +This substitution ensures that the program remains functional and adheres to the ASCII-only constraint, without sacrificing usability. diff --git a/exercises/practice/ledger/.docs/instructions.md b/exercises/practice/ledger/.docs/instructions.md new file mode 100644 index 00000000..a53e5c15 --- /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/.meta/config.json b/exercises/practice/ledger/.meta/config.json new file mode 100644 index 00000000..fa548014 --- /dev/null +++ b/exercises/practice/ledger/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "Falilah" + ], + "files": { + "solution": [ + "src/lib.cairo" + ], + "test": [ + "tests/ledger.cairo" + ], + "example": [ + ".meta/example.cairo" + ], + "invalidator": [ + "Scarb.toml" + ] + }, + "blurb": "Refactor a ledger printer." +} diff --git a/exercises/practice/ledger/.meta/example.cairo b/exercises/practice/ledger/.meta/example.cairo new file mode 100644 index 00000000..4562a260 --- /dev/null +++ b/exercises/practice/ledger/.meta/example.cairo @@ -0,0 +1,224 @@ +// Refactored code +use core::to_byte_array::AppendFormattedToByteArray; + +#[derive(Debug, PartialEq, Drop)] +pub enum Currency { + USD, + EUR, +} + +#[derive(Debug, PartialEq, Drop)] +pub enum Locale { + en_US, + nl_NL, +} + +pub fn format_entries( + currency: Currency, locale: Locale, transactions: Array<(ByteArray, ByteArray, ByteArray)> +) -> Array { + let mut ledger: Array = array![]; + + // Step 1: Define the header based on the locale + let header = match @locale { + Locale::en_US => "Date | Description | Change ", + Locale::nl_NL => "Datum | Omschrijving | Verandering ", + }; + ledger.append(header); + + // Step 2: Process transactions + for ( + date, transaction, change + ) in transactions { + let formatted_date = format_date(@date, @locale); + + // Format the change based on the currency and locale + let formatted_change = format_change(@change, @currency, @locale); + + // Format the transaction row + let mut row = formatted_date; + row += " | "; + row += format_transaction(transaction); + row += " | "; + row += formatted_change; + + // Append the row to the ledger + ledger.append(row); + }; + + ledger +} + +// format date based on the locale handled in 1 function +fn format_date(date: @ByteArray, locale: @Locale) -> ByteArray { + let (mut year, mut month, mut day) = split_date(date); + match locale { + Locale::en_US => { + day += "/"; + month += "/"; + ByteArrayTrait::concat(@month, @ByteArrayTrait::concat(@day, @year)) + }, + Locale::nl_NL => { + day += "-"; + month += "-"; + ByteArrayTrait::concat(@day, @ByteArrayTrait::concat(@month, @year)) + }, + } +} + +// split date into year, month and day +fn split_date(date: @ByteArray) -> (ByteArray, ByteArray, ByteArray) { + let mut year = ""; + let mut month = ""; + let mut day = ""; + let mut sep = 0; + let mut i = 0; + + while i < date.len() { + if sep == 0 && i < 4 && date[i] != '-' { + year.append_byte(date[i]); + } else if date[i] == '-' { + sep += 1; + } else if sep == 1 && i < 7 && date[i] != '-' { + month.append_byte(date[i]); + } else { + day.append_byte(date[i]); + } + i += 1; + }; + + (year, month, day) +} + +fn format_change(change: @ByteArray, currency: @Currency, locale: @Locale) -> ByteArray { + let mut int_value: u32 = 0; + let mut negative = false; + let mut i = 0; + + if change[i] == '-' { + negative = true; + i += 1; + } + + while i < change.len() { + if let Option::Some(digit) = char_to_digit(change[i]) { + int_value = int_value * 10 + digit.into(); + } + i += 1; + }; + + let formatted_value = format_number(int_value, negative, currency, locale); + let mut extra = ""; + + if formatted_value.len() < 13 { + let diff = 13 - formatted_value.len(); + let mut i = 0; + while i < diff { + extra += " "; + i += 1; + } + } + ByteArrayTrait::concat(@extra, @formatted_value) +} + +fn format_number(value: u32, negative: bool, currency: @Currency, locale: @Locale) -> ByteArray { + let mut result = ""; + + if negative && locale == @Locale::en_US { + result.append_byte('('); + } + + match currency { + Currency::USD => result.append_byte('$'), + Currency::EUR => result.append_byte('e'), + }; + + if locale == @Locale::nl_NL { + result.append_byte(' '); + } + + if negative && locale != @Locale::en_US { + result.append_byte('-'); + } + + let whole = value / 100; + + result += add_sep(whole, locale); + let fraction = value % 100; + if locale == @Locale::en_US { + result.append_byte('.'); + } else { + result.append_byte(','); + } + + if fraction < 10 { + result.append_byte('0'); + @fraction.append_formatted_to_byte_array(ref result, 10); + } else { + @fraction.append_formatted_to_byte_array(ref result, 10); + } + + if negative && locale == @Locale::en_US { + result.append_byte(')'); + } else { + result.append_byte(' '); + } + + result +} + + +fn add_sep(whole: u32, locale: @Locale) -> ByteArray { + let mut result = ""; + let mut temp = ""; + @whole.append_formatted_to_byte_array(ref temp, 10); + if temp.len() > 3 { + result.append_byte(temp[0]); + let mut i = 1; + let mut sep = 0; + while i < temp.len() { + if sep == 0 { + if locale == @Locale::nl_NL { + result.append_byte('.'); + } else { + result.append_byte(','); + } + sep = 3; + } + result.append_byte(temp[i]); + i += 1; + sep -= 1; + } + } else { + result += temp; + } + result +} + +fn format_transaction(transaction: ByteArray) -> ByteArray { + let mut formatted = ""; + + if transaction.len() > 22 { + let mut i = 0; + while i < 22 { + formatted.append_byte(transaction[i]); + i += 1; + }; + formatted += "..."; + } else { + formatted += transaction; + while formatted.len() < 25 { + formatted.append_byte(' '); + }; + } + + formatted +} + + +fn char_to_digit(c: u8) -> Option { + if c >= '0' && c <= '9' { + Option::Some(c - '0') + } else { + Option::None + } +} diff --git a/exercises/practice/ledger/.meta/tests.toml b/exercises/practice/ledger/.meta/tests.toml new file mode 100644 index 00000000..4ea45ceb --- /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/Scarb.toml b/exercises/practice/ledger/Scarb.toml new file mode 100644 index 00000000..78db418a --- /dev/null +++ b/exercises/practice/ledger/Scarb.toml @@ -0,0 +1,7 @@ +[package] +name = "ledger" +version = "0.1.0" +edition = "2024_07" + +[dev-dependencies] +cairo_test = "2.9.2" diff --git a/exercises/practice/ledger/src/lib.cairo b/exercises/practice/ledger/src/lib.cairo new file mode 100644 index 00000000..4cc24c8d --- /dev/null +++ b/exercises/practice/ledger/src/lib.cairo @@ -0,0 +1,378 @@ +use core::to_byte_array::{AppendFormattedToByteArray}; + +#[derive(Debug, PartialEq, Drop)] +pub enum Currency { + USD, + EUR, + JPY, +} + +#[derive(Debug, PartialEq, Drop)] +pub enum Locale { + en_US, + nl_NL, +} + +pub fn format_entries( + currency: Currency, locale: Locale, transactions: Array<(ByteArray, ByteArray, ByteArray)> +) -> Array { + let mut ledger: Array = ArrayTrait::new(); + let mut header = ""; + if locale == Locale::en_US { + header = "Date | Description | Change "; + } else if locale == Locale::nl_NL { + header = "Datum | Omschrijving | Verandering "; + } + + ledger.append(header); + for ( + date, transaction, change + ) in transactions { + let mut formatted_date = ""; + if locale == Locale::en_US { + let mut year = "/"; + let mut month = ""; + let mut day = "/"; + + let mut i = 0; + let mut sep = 0; + + while i < date.len() { + if sep == 0 && i < 4 && date[i] != '-' { + year.append_byte(date[i]); + } else if date[i] == '-' { + sep += 1; + } else if sep == 1 && i < 7 && date[i] != '-' { + month.append_byte(date[i]); + } else { + day.append_byte(date[i]); + } + i += 1; + }; + + formatted_date = ByteArrayTrait::concat(@month, @ByteArrayTrait::concat(@day, @year)) + } else { + let mut year = "-"; + let mut month = "-"; + let mut day = ""; + + let mut i = 0; + let mut sep = 0; + + while i < date.len() { + if sep == 0 && i < 4 && date[i] != '-' { + year.append_byte(date[i]); + } else if date[i] == '-' { + sep += 1; + } else if sep == 1 && i < 7 && date[i] != '-' { + month.append_byte(date[i]); + } else { + day.append_byte(date[i]); + } + i += 1; + }; + + formatted_date = ByteArrayTrait::concat(@day, @ByteArrayTrait::concat(@month, @year)); + } + + let mut formatted_change = ""; + if currency == Currency::USD { + if locale == Locale::en_US { + let mut result = "$"; + let mut i = 0; + let mut op = ""; + let mut cl = ""; + if change[i] == '-' { + op += "("; + cl += ")"; + i += 1; + } else { + op += " "; + cl += " "; + } + let mut int: u32 = 0; + while i < change.len() { + let c = char_to_digit(change[i]); + + match c { + Option::Some(v) => { int = int * 10 + v.into(); }, + Option::None => { break; } + } + i += 1; + }; + + let val = int / 100; + let mut temp = ""; + @val.append_formatted_to_byte_array(ref temp, 10); + if temp.len() > 3 { + result.append_byte(temp[0]); + let mut i = 1; + let mut sep = 0; + while i < temp.len() { + if sep == 0 { + result.append_byte(','); + sep = 3; + } + result.append_byte(temp[i]); + i += 1; + sep -= 1; + } + } else { + result += temp; + } + + result += "."; + let mut rem = int % 100; + if int < 10 { + result += "0"; + } + + @rem.append_formatted_to_byte_array(ref result, 10); + if result[result.len() - 2] == '.' { + result += "0"; + } + + result = ByteArrayTrait::concat(@ByteArrayTrait::concat(@op, @result), @cl); + + let mut extra = ""; + if result.len() < 13 { + let diff = 13 - result.len(); + let mut i = 0; + while i < diff { + extra += " "; + i += 1; + } + } + + formatted_change = ByteArrayTrait::concat(@extra, @result); + } else { + let mut result = " $ "; + let mut i = 0; + let mut op = ""; + let mut cl = ""; + if change[i] == '-' { + result = " $ -"; + i += 1; + } + op += " "; + cl += " "; + + let mut int: u32 = 0; + while i < change.len() { + let c = char_to_digit(change[i]); + + match c { + Option::Some(v) => { int = int * 10 + v.into(); }, + Option::None => { break; } + } + i += 1; + }; + + let val = int / 100; + let mut temp = ""; + @val.append_formatted_to_byte_array(ref temp, 10); + if temp.len() > 3 { + result.append_byte(temp[0]); + let mut i = 1; + let mut sep = 0; + while i < temp.len() { + if sep == 0 { + result.append_byte('.'); + sep = 3; + } + result.append_byte(temp[i]); + i += 1; + sep -= 1; + } + } else { + result += temp; + } + + result += ","; + let mut rem = int % 100; + if int < 10 { + result += "0"; + } + + @rem.append_formatted_to_byte_array(ref result, 10); + if result[result.len() - 2] == ',' { + result += "0"; + } + + result = ByteArrayTrait::concat(@ByteArrayTrait::concat(@op, @result), @cl); + + let mut extra = ""; + if result.len() < 13 { + let diff = 13 - result.len(); + let mut i = 0; + while i < diff { + extra += " "; + i += 1; + } + } + + formatted_change = ByteArrayTrait::concat(@extra, @result); + } + } else { + if locale == Locale::en_US { + // formatted_change = format_eur(change); + + let mut result = "e"; + let mut i = 0; + let mut op = ""; + let mut cl = ""; + if change[i] == '-' { + op += "("; + cl += ")"; + i += 1; + } else { + op += " "; + cl += " "; + } + let mut int: u32 = 0; + while i < change.len() { + let c = char_to_digit(change[i]); + + match c { + Option::Some(v) => { int = int * 10 + v.into(); }, + Option::None => { break; } + } + i += 1; + }; + + let val = int / 100; + @val.append_formatted_to_byte_array(ref result, 10); + result += "."; + let rem = int % 100; + if int < 10 { + result += "0"; + } + @rem.append_formatted_to_byte_array(ref result, 10); + + if result[result.len() - 2] == '.' { + result += "0"; + } + + result = ByteArrayTrait::concat(@ByteArrayTrait::concat(@op, @result), @cl); + + let mut extra = ""; + if result.len() < 13 { + let diff = 13 - result.len(); + let mut i = 0; + while i < diff { + extra += " "; + i += 1; + } + } + + formatted_change = ByteArrayTrait::concat(@extra, @result); + } else { + // formatted_change = format_eur_nil(change); + + let mut result = "e "; + let mut i = 0; + let mut op = ""; + let mut cl = ""; + if change[i] == '-' { + op += "("; + cl += ")"; + i += 1; + } else { + op += " "; + cl += " "; + } + let mut int: u32 = 0; + while i < change.len() { + let c = char_to_digit(change[i]); + + match c { + Option::Some(v) => { int = int * 10 + v.into(); }, + Option::None => { break; } + } + i += 1; + }; + let val = int / 100; + let mut temp = ""; + @val.append_formatted_to_byte_array(ref temp, 10); + if temp.len() > 3 { + result.append_byte(temp[0]); + let mut i = 1; + let mut sep = 0; + while i < temp.len() { + if sep == 0 { + result.append_byte('.'); + sep = 3; + } + result.append_byte(temp[i]); + i += 1; + sep -= 1; + } + } else { + result += temp; + } + result += ","; + let rem = int % 100; + if int < 10 { + result += "0"; + } + @rem.append_formatted_to_byte_array(ref result, 10); + + if result[result.len() - 2] == ',' { + result += "0"; + } + + result = ByteArrayTrait::concat(@ByteArrayTrait::concat(@op, @result), @cl); + let mut extra = ""; + if result.len() < 13 { + let diff = 13 - result.len(); + let mut i = 0; + while i < diff { + extra += " "; + i += 1; + } + } + + formatted_change = ByteArrayTrait::concat(@extra, @result); + } + } + + let mut row = formatted_date; + row += " | "; + if transaction.len() > 22 { + let mut i = 0; + while i < 22 { + row.append_byte(transaction[i]); + i += 1; + }; + row += "..."; + } else { + row += transaction; + } + + if row.len() < 38 { + let dif = 38 - row.len(); + let mut i = 0; + while i < dif { + row += " "; + i += 1; + } + } + row += " | "; + row += formatted_change; + + ledger.append(row); + }; + + ledger +} + +fn char_to_digit(c: u8) -> Option { + let zero_ascii = '0'; + let nine_ascii = '9'; + + if c >= zero_ascii && c <= nine_ascii { + Option::Some(c - zero_ascii) + } else { + Option::None // Return None for invalid characters + } +} diff --git a/exercises/practice/ledger/tests/ledger.cairo b/exercises/practice/ledger/tests/ledger.cairo new file mode 100644 index 00000000..d7b5e0c8 --- /dev/null +++ b/exercises/practice/ledger/tests/ledger.cairo @@ -0,0 +1,188 @@ +use ledger::{format_entries, Currency, Locale}; + +#[test] +fn empty_ledger() { + let currency = Currency::USD; + let locale = Locale::en_US; + let entries = array![]; + + let expect: Array = array!["Date | Description | Change "]; + let result = format_entries(currency, locale, entries); + + assert_eq!(result, expect); +} + +#[test] +#[ignore] +fn one_entry() { + let currency = Currency::USD; + let locale = Locale::en_US; + let entries = array![("2015-01-01", "Buy present", "-1000")]; + + let expected: Array = array![ + "Date | Description | Change ", + "01/01/2015 | Buy present | ($10.00)" + ]; + let result = format_entries(currency, locale, entries); + + assert_eq!(result, expected); +} + +#[test] +#[ignore] +fn credit_and_debit() { + let currency = Currency::USD; + let locale = Locale::en_US; + let entries = array![ + ("2015-01-01", "Buy present", "-1000"), ("2015-01-02", "Get present", "1000") + ]; + + let expected: Array = array![ + "Date | Description | Change ", + "01/01/2015 | Buy present | ($10.00)", + "01/02/2015 | Get present | $10.00 " + ]; + let result = format_entries(currency, locale, entries); + + assert_eq!(result, expected); +} + +#[test] +#[ignore] +fn final_order_tie_breaker_is_change() { + let currency = Currency::USD; + let locale = Locale::en_US; + let entries = array![ + ("2015-01-01", "Something", "-1"), + ("2015-01-01", "Something", "0"), + ("2015-01-01", "Something", "1") + ]; + + let expected: Array = array![ + "Date | Description | Change ", + "01/01/2015 | Something | ($0.01)", + "01/01/2015 | Something | $0.00 ", + "01/01/2015 | Something | $0.01 " + ]; + let result = format_entries(currency, locale, entries); + + assert_eq!(result, expected); +} + + +#[test] +#[ignore] +fn overlong_description_is_truncated() { + let currency = Currency::USD; + let locale = Locale::en_US; + let entries = array![("2015-01-01", "Freude schoner Gotterfunken", "-123456")]; + + let expected: Array = array![ + "Date | Description | Change ", + "01/01/2015 | Freude schoner Gotterf... | ($1,234.56)" + ]; + let result = format_entries(currency, locale, entries); + + assert_eq!(result, expected); +} + +#[test] +#[ignore] +fn euros() { + let currency = Currency::EUR; + let locale = Locale::en_US; + let entries = array![("2015-01-01", "Buy present", "-1000")]; + + let expected: Array = array![ + "Date | Description | Change ", + "01/01/2015 | Buy present | (e10.00)" + ]; + let result = format_entries(currency, locale, entries); + + assert_eq!(result, expected); +} + +#[test] +#[ignore] +fn dutch_locale() { + let currency = Currency::USD; + let locale = Locale::nl_NL; + let entries = array![("2015-03-12", "Buy present", "123456")]; + + let expected: Array = array![ + "Datum | Omschrijving | Verandering ", + "12-03-2015 | Buy present | $ 1.234,56 " + ]; + let result = format_entries(currency, locale, entries); + + assert_eq!(result, expected); +} + +#[test] +#[ignore] +fn dutch_locale_and_euros() { + let currency = Currency::EUR; + let locale = Locale::nl_NL; + let entries = array![("2015-03-12", "Buy present", "123456")]; + + let expected: Array = array![ + "Datum | Omschrijving | Verandering ", + "12-03-2015 | Buy present | e 1.234,56 " + ]; + let result = format_entries(currency, locale, entries); + + assert_eq!(result, expected); +} + + +#[test] +#[ignore] +fn dutch_negative_number_with_3_digits_before_decimal_point() { + let currency = Currency::USD; + let locale = Locale::nl_NL; + let entries = array![("2015-03-12", "Buy present", "-12345")]; + + let expected: Array = array![ + "Datum | Omschrijving | Verandering ", + "12-03-2015 | Buy present | $ -123,45 " + ]; + let result = format_entries(currency, locale, entries); + + assert_eq!(result, expected); +} + +#[test] +#[ignore] +fn american_negative_number_with_3_digits_before_decimal_point() { + let currency = Currency::USD; + let locale = Locale::en_US; + let entries = array![("2015-03-12", "Buy present", "-12345")]; + + let expected: Array = array![ + "Date | Description | Change ", + "03/12/2015 | Buy present | ($123.45)" + ]; + let result = format_entries(currency, locale, entries); + + assert_eq!(result, expected); +} + + +#[test] +#[ignore] +fn multiple_entries_on_same_date_ordered_by_description() { + let currency = Currency::USD; + let locale = Locale::en_US; + let entries = array![ + ("2015-01-01", "Buy present", "-1000"), ("2015-01-01", "Get present", "1000") + ]; + + let expect: Array = array![ + "Date | Description | Change ", + "01/01/2015 | Buy present | ($10.00)", + "01/01/2015 | Get present | $10.00 " + ]; + let result = format_entries(currency, locale, entries); + + assert_eq!(result, expect); +}