Skip to content

Conversation

@u-ashikov
Copy link
Contributor

This pull request introduces support for multiple currencies (specifically BGN and EUR) and negative values in the number-to-words conversion library. It refactors the codebase to use currency descriptors and vocabularies, making the system extensible and easier to maintain. The documentation and tests are updated to reflect these new features.

Major new features:

  • Added support for multiple currencies via the new CurrencyDescriptor and NumberWordsVocabulary classes, with built-in descriptors for Bulgarian Lev (BGN) and Euro (EUR). (src/OneBitSoftware.Slovom/Currencies/CurrencyDescriptor.cs, src/OneBitSoftware.Slovom/Currencies/NumberWordsVocabulary.cs) [1] [2]
  • The NumbersToWords.Convert method now accepts a CurrencyDescriptor parameter, enabling conversion for different currencies and supporting negative values. (src/OneBitSoftware.Slovom/NumbersToWords.cs)

Documentation updates:

  • Expanded the README.md to document support for negative values, multiple currencies, and to provide example tables for both BGN and EUR conversions. (README.md) [1] [2] [3]

Code and test improvements:

  • Refactored internal logic to use vocabulary and currency descriptors, improving maintainability and extensibility. (src/OneBitSoftware.Slovom/NumbersToWords.cs) [1] [2]
  • Added a new test case for the value "100 лева" to ensure coverage. (src/Tests/ConvertTests.cs)
  • Bumped the package version to 1.2.0 to reflect the new features. (src/OneBitSoftware.Slovom/OneBitSoftware.Slovom.csproj)

Copilot AI review requested due to automatic review settings January 19, 2026 10:10
Copy link
Member

@TonyTroeff TonyTroeff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that the documentation and tests are thorough!

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds support for multiple currencies (BGN and EUR) and negative values to the number-to-words conversion library. The implementation introduces a currency descriptor pattern with vocabulary abstraction, replacing the previous hardcoded BGN-only approach.

Changes:

  • Added multi-currency support through CurrencyDescriptor and NumberWordsVocabulary classes with built-in support for BGN and EUR
  • Implemented negative number handling with "Минус" prefix
  • Refactored the Convert method to accept a CurrencyDescriptor parameter (breaking API change)

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/OneBitSoftware.Slovom/Currencies/CurrencyDescriptor.cs New file defining currency descriptors for BGN and EUR with gender-specific vocabularies
src/OneBitSoftware.Slovom/Currencies/NumberWordsVocabulary.cs New file defining the vocabulary structure for number-to-word conversions
src/OneBitSoftware.Slovom/NumbersToWords.cs Refactored to use currency descriptors and vocabularies; added negative number support
src/Tests/ConvertTests.cs Added comprehensive test cases for BGN, EUR, and negative values
src/OneBitSoftware.Slovom/OneBitSoftware.Slovom.csproj Version bumped from 1.1.0 to 1.2.0
README.md Updated with EUR examples, negative value support documentation, and corrected maximum value

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 189 to 195
[Theory]
[InlineData(-100000)]
public void Convert_WithNumberLessThanNegativeLimit_ShouldThrowArgumentOutOfRangeException(decimal number)
{
Assert.Throws<ArgumentOutOfRangeException>(() => NumbersToWords.Convert(number, CurrencyDescriptor.Bgn));
Assert.Throws<ArgumentOutOfRangeException>(() => NumbersToWords.Convert(number, CurrencyDescriptor.Euro));
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite only validates the lower bound exception (-100000) but does not test for the upper bound. Add a test case to verify that values above the maximum supported value (99999.99 or 999999.99) throw an ArgumentOutOfRangeException.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

dotnet add package OneBitSoftware.Slovom
```

## Examples
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README does not include a code usage example showing how to call the Convert method with the required CurrencyDescriptor parameter. Add a usage section with code examples like: NumbersToWords.Convert(32048.27m, CurrencyDescriptor.Bgn) or NumbersToWords.Convert(32048.27m, CurrencyDescriptor.Euro).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


if (number == 1 && stotinki == 0) return under20[leva] + AppendLvMale; // един лев
var leva = (int)number;
var stotinki = (int)((number % 1.0m) * 100);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name 'stotinki' is specific to Bulgarian Lev currency (стотинки = cents in BGN), but this method now handles multiple currencies including Euro which uses евроценти. Consider renaming to a more generic name like 'minorUnit', 'fractionalPart', or 'cents' to better reflect its purpose across different currencies.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (number == 1 && stotinki == 0) return AppendNegativePrefix(currencyDescriptor.Vocabulary.NumbersZeroToNineteen[leva] + currencyDescriptor.MajorCurrencyUnitSingular, isNegativeNumber); // един лев, едно евро

string stotinkiWords;
var levaWords = leva != 1 ? ConvertWholeNumber(leva, currencyDescriptor.Vocabulary) + currencyDescriptor.MajorCurrencyUnitPlural : currencyDescriptor.Vocabulary.NumbersZeroToNineteen[leva] + currencyDescriptor.MajorCurrencyUnitSingular;
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name 'levaWords' is specific to Bulgarian Lev currency but this method now handles multiple currencies. Consider renaming to 'majorUnitWords' or 'wholePartWords' to better reflect its purpose across different currencies.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

number = Math.Abs(number); // Convert negative number to positive
public static string Convert(decimal number, CurrencyDescriptor currencyDescriptor)
{
if (number < -99999.99m) throw new ArgumentOutOfRangeException(nameof(number), "Входното число не може да бъде по-малко от -99999.99");
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation only checks for numbers less than -99999.99 but does not validate the upper bound. According to the README, the maximum supported value is 999999.99. Add validation to ensure the number is not greater than 99999.99 (or 999999.99 if that's the intended limit). Without this, values above the supported range will either produce incorrect results or potentially cause runtime errors.

Suggested change
if (number < -99999.99m) throw new ArgumentOutOfRangeException(nameof(number), "Входното число не може да бъде по-малко от -99999.99");
if (number < -99999.99m || number > 99999.99m)
throw new ArgumentOutOfRangeException(nameof(number), "Входното число трябва да бъде в интервала [-99999.99; 99999.99]");

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

- It supports negative values.
- It writes decimal fractions in the short form: `X лева и ст.` when the value is above zero, and the full word when it is under the value of `1`: `девет стотинки`.
- The current maximum value is `999999.99` and the minimum is `0.`.
- The current maximum value is `999999.99` and the minimum is `-999999.99`.
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README states that the maximum supported value is 999999.99, but the code only handles numbers less than 100000 (line 14). Numbers between 100000 and 999999 will return "Числото е твърде голямо" (the number is too large). Either update the README to reflect the actual maximum of 99999.99, or implement support for numbers up to 999999.99.

Suggested change
- The current maximum value is `999999.99` and the minimum is `-999999.99`.
- The current maximum value is `99999.99` and the minimum is `-99999.99`.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

[InlineData(-1001, "Минус хиляда и един лева")]
[InlineData(-2000, "Минус две хиляди лева")]
[InlineData(-10000, "Минус десет хиляди лева")]
[InlineData(-21000, "Минус двадесет и една хиляди и нула лева")]
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expected output "Минус двадесет и една хиляди и нула лева" includes "и нула лева" (and zero leva), which is grammatically awkward in Bulgarian. When the remainder after thousands is zero, the output should just be "Минус двадесет и една хиляди лева" without the "и нула" part. This issue occurs in the TensOfThousands method when b == 1 and e == 0 (line 100 in NumbersToWords.cs).

Suggested change
[InlineData(-21000, "Минус двадесет и една хиляди и нула лева")]
[InlineData(-21000, "Минус двадесет и една хиляди лева")]

Copilot uses AI. Check for mistakes.
number = Math.Abs(number); // Convert negative number to positive

if (number == 1 && stotinki == 0) return under20[leva] + AppendLvMale; // един лев
var leva = (int)number;
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name 'leva' is specific to Bulgarian Lev currency, but this method now handles multiple currencies including Euro. Consider renaming to a more generic name like 'majorUnit', 'wholePart', or 'integerPart' to better reflect its purpose across different currencies.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@u-ashikov u-ashikov merged commit 9af4e5c into main Jan 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants