Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ try {
| Identifiers | `ShortId`, `UUID` |
| Date & time | `Timestamp`, `CalendarDay`, `Hour`, `Duration`, `MonthOfYear` |
| Coordinates | `Latitude`, `Longitude`, `Coordinates` |
| Money | `Money`, `Currency` |
| Hashes | `MD5Hash`, `SHA256Hash`, `SHA512Hash` |
| Media | `Media` |
| Collections | `UniqueObjectArray` |
Expand Down
8 changes: 8 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ const referenceSidebar = [
{ text: 'ShortId', link: '/reference/short-id' },
],
},
{
text: 'Money',
collapsed: true,
items: [
{ text: 'Money', link: '/reference/money' },
{ text: 'Currency', link: '/reference/currency' },
],
},
{
text: 'Time',
collapsed: true,
Expand Down
1 change: 1 addition & 0 deletions docs/getting-started/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ amount.isGreaterThan(5); // true
| IDs | `ShortId`, `UUID` |
| Time | `Timestamp`, `CalendarDay`, `Day`, `DayOfWeek`, `Duration`, `Hour`, `Month`, `MonthOfYear`, `TimestampInterval`, `Year` |
| Location | `Latitude`, `Longitude`, `Coordinates` |
| Money | `Money`, `Currency` |
| Hashes | `Hash`, `MD5Hash`, `SHA256Hash`, `SHA512Hash` |
| Media | `Media` |
| Collections | `UniqueObjectArray` |
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ yarn add @haskou/value-objects
| [Base objects](/reference/value-object) | `ValueObject`, `NullObject`, `Enum` |
| [Strings and numbers](/reference/string-value-object) | `StringValueObject`, `Email`, `Color`, `Password`, `NumberValueObject`, `Integer`, `PositiveNumber` |
| [Identifiers](/reference/uuid) | `UUID`, `ShortId` |
| [Money](/reference/money) | `Money`, `Currency` |
| [Time](/reference/timestamp) | `Timestamp`, `TimestampInterval`, `CalendarDay`, `Duration`, `Hour`, `Day`, `Year`, `Month`, `MonthOfYear`, `DayOfWeek` |
| [Coordinates](/reference/coordinates) | `Latitude`, `Longitude`, `Coordinates` |
| [Hashes](/reference/hash) | `Hash`, `MD5Hash`, `SHA256Hash`, `SHA512Hash` |
Expand Down
53 changes: 53 additions & 0 deletions docs/reference/currency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: Currency
description: ISO 4217 currency code with its minor unit digits.
---

# `Currency`

ISO 4217 currency code with its minor unit digits.

## Import

```typescript
import { Currency } from '@haskou/value-objects';
```

## Signature

```typescript
class Currency extends Enum<string>
```

## Constructor

```typescript
constructor(value: string)
```

## Methods

| Method | Description |
| --------------------------- | ---------------------------------------------------------------------- |
| `static fromSymbol(symbol)` | Resolves `$` (USD), `£`, `¥` (JPY), `€`, `₡`, `₩`, `₪`, `₹` and `₺`. |
| `getValues()` | Returns the supported ISO codes. |
| `getMinorUnitDigits()` | Returns the decimals of the minor unit (`2` for `EUR`, `0` for `JPY`). |

## Throws

- `ValueNotInEnumError` for unsupported codes.
- `InvalidFormatError` for unknown symbols.

## Example

```typescript
import { Currency } from '@haskou/value-objects';

new Currency('JPY').getMinorUnitDigits(); // 0
Currency.fromSymbol('€').valueOf(); // 'EUR'
```

## Related

- [Money](/reference/money)
- [Reference overview](/reference/)
5 changes: 5 additions & 0 deletions docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ Each exported class has its own page.
- [`Longitude`](/reference/longitude)
- [`Coordinates`](/reference/coordinates)

## Money

- [`Money`](/reference/money)
- [`Currency`](/reference/currency)

## Time

- [`CalendarDay`](/reference/calendar-day)
Expand Down
77 changes: 77 additions & 0 deletions docs/reference/money.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
title: Money
description: Exact monetary amount with currency, stored as integer minor units.
---

# `Money`

Exact monetary amount with currency. The amount is held as an integer number of minor units (cents), so arithmetic never accumulates floating point errors.

## Import

```typescript
import { Money } from '@haskou/value-objects';
```

## Signature

```typescript
class Money extends ValueObject<string>
```

The canonical value is `"<amount> <ISO code>"`, for example `"19.99 EUR"` or `"1500 JPY"`.

## Constructor

```typescript
constructor(value: string | StringValueObject)
```

The constructor is strict: the number of decimals must equal the currency's minor unit digits (`19.99 EUR`, `1500 JPY`), and `-0.00` is rejected. Use `fromString()` for lenient input.

## Methods

| Method | Description |
| ---------------------------------------- | ---------------------------------------------------------------------------------- |
| `static fromMinorUnits(units, currency)` | Builds from an integer amount of minor units and a currency code or `Currency`. |
| `static fromString(text)` | Parses `20€`, `€20`, `20 €`, `$19.5`, `-€3.07`, `20.50 EUR`, `EUR 20` or `20 eur`. |
| `getCurrency()` | Returns the `Currency`. |
| `getMinorUnits()` | Returns the amount in minor units as an `Integer`. |
| `isZero()` | True when the amount is zero. |
| `isNegative()` | True when the amount is below zero. |
| `isGreaterThan(other)` | Compares amounts of the same currency. |
| `isLessThan(other)` | Compares amounts of the same currency. |
| `add(other)` | Returns the sum as a new `Money`. |
| `subtract(other)` | Returns the difference as a new `Money`. |
| `multiply(factor)` | Returns the product rounded to the nearest minor unit. |

## Throws

- `InvalidMoneyError` for malformed text, too many decimals, missing or duplicated currency, or amounts beyond ±2^53 minor units.
- `CurrencyMismatchError` when operating on two different currencies.
- `ValueNotInEnumError` for unsupported ISO codes.
- `InvalidFormatError` for unknown currency symbols.

## Example

```typescript
import { Money } from '@haskou/value-objects';

const price = Money.fromString('19.99 EUR');

price.multiply(3).valueOf(); // '59.97 EUR'
Money.fromString('0.10 EUR').add(Money.fromString('0.20 EUR')).valueOf(); // '0.30 EUR'
Money.fromMinorUnits(1500, 'JPY').valueOf(); // '1500 JPY'
```

## Notes

- Symbols `$` and `¥` resolve to `USD` and `JPY`; use ISO codes (`20 CAD`) for any other dollar or yen currency.
- Locale separators (`1,50`) are rejected.
- Allocation, `>=`/`<=`, `negate`, `abs` and locale formatting are not provided.

## Related

- [Currency](/reference/currency)
- [Error handling](/guides/error-handling)
- [Reference overview](/reference/)
7 changes: 7 additions & 0 deletions src/errors/CurrencyMismatchError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { DomainError } from './DomainError';

export class CurrencyMismatchError extends DomainError {
constructor(expected: string, received: string) {
super(`Currency mismatch: expected ${expected}, received ${received}`);
}
}
7 changes: 7 additions & 0 deletions src/errors/InvalidMoneyError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { DomainError } from './DomainError';

export class InvalidMoneyError extends DomainError {
constructor(value: unknown) {
super(`Invalid money ${String(value)}`);
}
}
2 changes: 2 additions & 0 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './BaseError';
export * from './CurrencyMismatchError';
export * from './DomainError';
export * from './InvalidColorError';
export * from './InvalidDayError';
Expand All @@ -12,6 +13,7 @@ export * from './InvalidLatitudeError';
export * from './InvalidLongitudeError';
export * from './InvalidEmailError';
export * from './InvalidMinutesError';
export * from './InvalidMoneyError';
export * from './InvalidNumberError';
export * from './InvalidPasswordError';
export * from './InvalidPositiveNumberError';
Expand Down
1 change: 1 addition & 0 deletions src/value-objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './coordinates';
export * from './hashes';
export * from './ids';
export * from './media';
export * from './money';
export * from './time';
export * from './Color';
export * from './Email';
Expand Down
70 changes: 70 additions & 0 deletions src/value-objects/money/Currency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { InvalidFormatError } from '../../errors/InvalidFormatError';
import { assert } from '../../patterns/Assert';
import { Enum } from '../Enum';

export class Currency extends Enum<string> {
private static readonly MINOR_UNIT_DIGITS: Record<string, number> = {
AED: 2,
ARS: 2,
AUD: 2,
BRL: 2,
CAD: 2,
CHF: 2,
CLP: 0,
CNY: 2,
COP: 2,
CRC: 2,
CZK: 2,
DKK: 2,
EUR: 2,
GBP: 2,
HKD: 2,
HUF: 2,
IDR: 2,
ILS: 2,
INR: 2,
ISK: 0,
JPY: 0,
KRW: 0,
KWD: 3,
MXN: 2,
NOK: 2,
NZD: 2,
PEN: 2,
PLN: 2,
RON: 2,
SEK: 2,
SGD: 2,
THB: 2,
TRY: 2,
USD: 2,
ZAR: 2,
};

private static readonly SYMBOLS: Record<string, string> = {
$: 'USD',
'£': 'GBP',
'¥': 'JPY',
'₡': 'CRC',
'₩': 'KRW',
'₪': 'ILS',
'€': 'EUR',
'₹': 'INR',
'₺': 'TRY',
};

public static fromSymbol(symbol: string): Currency {
const code = Currency.SYMBOLS[symbol];
assert(code, new InvalidFormatError(symbol));

return new Currency(code);
}

public getValues(): string[] {
return Object.keys(Currency.MINOR_UNIT_DIGITS);
}

public getMinorUnitDigits(): number {
return Currency.MINOR_UNIT_DIGITS[this.valueOf()];
}
}
Loading
Loading