Skip to content

Commit

Permalink
feat: add country currency code service
Browse files Browse the repository at this point in the history
  • Loading branch information
dev-ahmadbilal committed Sep 10, 2024
1 parent e567b6f commit 6ae3f8b
Show file tree
Hide file tree
Showing 10 changed files with 968 additions and 253 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Currency Master
[![npm package][npm-img]][npm-url]
[![Build Status][build-img]][build-url]
[![Downloads][download-img]][npm-url]
[![Issues][issues-img]][issues-url]
[![Code Coverage][codecov-img]][codecov-url]
[![Semantic Release][semantic-release-img]][semantic-release-url]
Expand Down Expand Up @@ -93,6 +94,7 @@ This project is licensed under the MIT License.
[build-url]:https://github.com/dev-ahmadbilal/currency-master/actions/workflows/release.yml
[npm-img]:https://img.shields.io/npm/v/currency-master
[npm-url]:https://www.npmjs.com/package/currency-master
[download-img]: https://badgen.net/npm/dt/currency-master
[issues-img]:https://img.shields.io/github/issues/dev-ahmadbilal/currency-master
[issues-url]:https://github.com/dev-ahmadbilal/currency-master/issues
[codecov-img]:https://codecov.io/gh/dev-ahmadbilal/currency-master/branch/main/graph/badge.svg
Expand Down
2 changes: 1 addition & 1 deletion src/conversion-master.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class ConversionMaster {
static async getCurrencyRates(currency: CurrencyEnum, date?: string): Promise<CurrencyRates> {
try {
const formattedDate = date || ConversionMaster.formatDate(new Date());
const url = `${API_BASE_URL}@${formattedDate}/v1/currencies/${currency}.json`;
const url = `${API_BASE_URL}@${formattedDate}/v1/currencies/${currency.toLowerCase()}.json`;
const response = await fetch(url);
const data: CurrencyApiResponse = await response.json();
return data[currency] as CurrencyRates;
Expand Down
80 changes: 80 additions & 0 deletions src/country-currency-code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { CountryCurrencyMap } from './types/country-currency.map';

// Define a type for valid country codes (keys of the object)
type CountryCode = keyof typeof CountryCurrencyMap;

export class CountryCurrencyCode {
// Object containing country codes as keys and currency codes as values
private static countryCurrencyMap = CountryCurrencyMap;

/**
* Returns the currency code for a given country code.
*
* @param {string} countryCode - The ISO 3166-1 alpha-2 country code.
* @returns {string | null} - The ISO 4217 currency code or null if the country code is invalid.
*
* @example
* ```ts
* const currency = CountryCurrencyCode.getCurrencyByCountryCode('US');
* console.log(currency); // Output: 'USD'
*
* const invalidCurrency = CountryCurrencyCode.getCurrencyByCountryCode('XYZ');
* console.log(invalidCurrency); // Output: null
* ```
*/
public static getCurrencyByCountryCode(countryCode: string): string | null {
const upperCaseCountryCode = countryCode.toUpperCase() as CountryCode;
return CountryCurrencyCode.countryCurrencyMap[upperCaseCountryCode] || null;
}

/**
* Returns the list of country codes associated with a given currency code.
*
* @param {string} currencyCode - The ISO 4217 currency code.
* @returns {string[] | null} - An array of country codes that use this currency or null if none are found.
*
* @example
* ```ts
* const countries = CountryCurrencyCode.getCountriesByCurrencyCode('USD');
* console.log(countries); // Output: ['US', 'UM']
* ```
*/
public static getCountriesByCurrencyCode(currencyCode: string): string[] | null {
const countries = Object.keys(CountryCurrencyCode.countryCurrencyMap).filter(
(country) => CountryCurrencyCode.countryCurrencyMap[country as CountryCode] === currencyCode.toUpperCase(),
);

return countries.length ? countries : null;
}

/**
* Returns a list of all available country codes.
*
* @returns {string[]} - An array of all available country codes.
*
* @example
* ```ts
* const countries = CountryCurrencyCode.getAllCountryCodes();
* console.log(countries); // Output: ['AF', 'AL', 'DZ', ...]
* ```
*/
public static getAllCountryCodes(): string[] {
return Object.keys(CountryCurrencyCode.countryCurrencyMap);
}

/**
* Returns a list of all unique currency codes.
*
* @returns {string[]} - An array of unique currency codes.
*
* @example
* ```ts
* const currencies = CountryCurrencyCode.getAllCurrencyCodes();
* console.log(currencies); // Output: ['AFN', 'ALL', 'USD', ...]
* ```
*/
public static getAllCurrencyCodes(): string[] {
const currencySet = new Set(Object.values(CountryCurrencyCode.countryCurrencyMap));
return Array.from(currencySet);
}
}
214 changes: 214 additions & 0 deletions src/currency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { CurrencySettings } from './types/currency-settings';

const defaultSettings: CurrencySettings = {
symbol: '$',
separator: ',',
decimal: '.',
errorOnInvalid: false,
precision: 2,
pattern: '!#',
negativePattern: '-!#',
fromCents: false,
groups: /(\d)(?=(\d{3})+\b)/g,
format: formatCurrency,
};

class Currency {
public intValue: number;
public value: number;
private settings: CurrencySettings;
private precisionFactor: number;

constructor(value: number | string | Currency, options?: Partial<CurrencySettings>) {
this.settings = { ...defaultSettings, ...options };
this.precisionFactor = Math.pow(10, this.settings.precision);
this.intValue = this.parseValue(value);
this.value = this.intValue / this.precisionFactor;
this.settings.increment ||= 1 / this.precisionFactor;

if (this.settings.useVedic) {
this.settings.groups = /(\d)(?=(\d\d)+\d\b)/g;
}
}

/**
* Parses the input value into an integer representation suitable for currency calculations.
* @param value - The value to parse (number, string, or Currency instance).
* @returns Parsed integer value.
*/
private parseValue(value: number | string | Currency): number {
if (value instanceof Currency) {
return value.intValue;
}

if (typeof value === 'number') {
return Math.round(value * (this.settings.fromCents ? 1 : this.precisionFactor));
}

const regex = new RegExp('[^-\\d' + this.settings.decimal + ']', 'g');
const cleanedValue = parseFloat(
value
.replace(/\((.*)\)/, '-$1')
.replace(regex, '')
.replace(this.settings.decimal, '.'),
);

if (isNaN(cleanedValue)) {
if (this.settings.errorOnInvalid) {
throw new Error('Invalid input');
}
return 0;
}

return Math.round(cleanedValue * (this.settings.fromCents ? 1 : this.precisionFactor));
}

/**
* Adds a specified amount to the currency value.
* @param amount - The amount to add (number, string, or Currency instance).
* @returns New Currency instance with the summed value.
* @example
* const currency = new Currency(10);
* const result = currency.add(5); // Result is a new Currency instance with value 15
*/
add(amount: number | string | Currency): Currency {
const result = this.intValue + this.parseValue(amount);
return new Currency(result / this.precisionFactor, this.settings);
}

/**
* Subtracts a specified amount from the currency value.
* @param amount - The amount to subtract (number, string, or Currency instance).
* @returns New Currency instance with the difference.
* @example
* const currency = new Currency(10);
* const result = currency.subtract(5); // Result is a new Currency instance with value 5
*/
subtract(amount: number | string | Currency): Currency {
const result = this.intValue - this.parseValue(amount);
return new Currency(result / this.precisionFactor, this.settings);
}

/**
* Multiplies the currency value by a specified factor.
* @param factor - The factor to multiply by.
* @returns New Currency instance with the multiplied value.
* @example
* const currency = new Currency(10);
* const result = currency.multiply(2); // Result is a new Currency instance with value 20
*/
multiply(factor: number): Currency {
const result = this.intValue * factor;
return new Currency(result / this.precisionFactor, this.settings);
}

/**
* Divides the currency value by a specified divisor.
* @param divisor - The divisor to divide by.
* @returns New Currency instance with the divided value.
* @example
* const currency = new Currency(10);
* const result = currency.divide(2); // Result is a new Currency instance with value 5
*/
divide(divisor: number): Currency {
const result = this.intValue / divisor;
return new Currency(result / this.precisionFactor, this.settings);
}

/**
* Distributes the currency value into an array of Currency instances evenly.
* @param parts - The number of parts to distribute into.
* @returns Array of Currency instances with the distributed values.
* @example
* const currency = new Currency(100);
* const parts = currency.distribute(3); // Returns an array of 3 Currency instances with values [33.34, 33.33, 33.33]
*/
distribute(parts: number): Currency[] {
const splitValue = Math.floor(this.intValue / parts);
const remainder = Math.abs(this.intValue - splitValue * parts);
const distributed = [];

for (let i = 0; i < parts; i++) {
const amount = splitValue + (i < remainder ? Math.sign(this.intValue) : 0);
distributed.push(new Currency(amount / this.precisionFactor, this.settings));
}

return distributed;
}

/**
* Returns the dollar portion of the currency value.
* @returns The dollar value as an integer.
* @example
* const currency = new Currency(123.45);
* const dollars = currency.dollars(); // Returns 123
*/
dollars(): number {
return Math.floor(this.value);
}

/**
* Returns the cent portion of the currency value.
* @returns The cent value as an integer.
* @example
* const currency = new Currency(123.45);
* const cents = currency.cents(); // Returns 45
*/
cents(): number {
return Math.floor(this.intValue % this.precisionFactor);
}

/**
* Formats the currency value as a string according to the formatting settings.
* @param options - Optional formatting options.
* @returns The formatted currency string.
* @example
* const currency = new Currency(1234.56);
* const formatted = currency.format(); // Returns '$1,234.56'
*/
format(options?: Partial<CurrencySettings>): string {
return this.settings.format(this, { ...this.settings, ...options });
}

/**
* Converts the currency value to a string.
* @returns String representation of the currency value.
* @example
* const currency = new Currency(1234.56);
* const value = currency.toString(); // Returns '1234.56'
*/
toString(): string {
return (this.intValue / this.precisionFactor).toFixed(this.settings.precision);
}

/**
* Returns the currency value for JSON serialization.
* @returns The numeric value for serialization.
* @example
* const currency = new Currency(1234.56);
* JSON.stringify(currency); // Returns '1234.56'
*/
toJSON(): number {
return this.value;
}
}

/**
* Formats a currency object.
* @param currency - The Currency object to format.
* @param settings - Settings for formatting.
* @returns The formatted currency string.
* @example
* const currency = new Currency(1234.56);
* const formatted = formatCurrency(currency, currency.settings); // Returns '$1,234.56'
*/
function formatCurrency(currency: Currency, settings: CurrencySettings): string {
const { pattern, negativePattern, symbol, separator, decimal, groups } = settings;
const [dollars, cents] = currency.value.toFixed(settings.precision).split('.');

return (currency.value >= 0 ? pattern : negativePattern)
.replace('!', symbol)
.replace('#', dollars.replace(groups, '$1' + separator) + (cents ? decimal + cents : ''));
}

export default Currency;
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { ConversionMaster } from './conversion-master';
export { CountryCurrencyCode } from './country-currency-code';
export * from './types';
Loading

0 comments on commit 6ae3f8b

Please sign in to comment.