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
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,19 @@ This is the main idea behind the library: use the provided Value Objects where t

## Available Value Objects

| Category | Classes | What they give you |
| ----------- | ------------------------------------------------------------- | ----------------------------------------------------------------- |
| Base | `ValueObject`, `NullObject`, `Enum` | Immutability, equality by value, safe absence |
| Strings | `StringValueObject`, `Password`, `Email`, `Color` | Length, format and emptiness rules |
| Numbers | `NumberValueObject`, `Integer`, `PositiveNumber` | Range and integer guarantees |
| Money | `Money`, `Currency` | Exact arithmetic in minor units, no floating point errors |
| Identifiers | `ShortId`, `UUID` | Generated and validated identifiers |
| Date & time | `Timestamp`, `CalendarDay`, `Hour`, `Duration`, `MonthOfYear` | Arithmetic and comparison without hand-rolled `Date` code |
| Coordinates | `Latitude`, `Longitude`, `Coordinates` | Range-checked geographic points |
| Hashes | `MD5Hash`, `SHA256Hash`, `SHA512Hash` | Validated digests |
| Media | `Media` | Typed media references |
| Collections | `UniqueObjectArray` | Immutable collection without duplicates, by Value Object equality |
| Category | Classes | What they give you |
| ----------- | ------------------------------------------------------------------- | ----------------------------------------------------------------- |
| Base | `ValueObject`, `NullObject`, `Enum` | Immutability, equality by value, safe absence |
| Strings | `StringValueObject`, `Password`, `Email`, `Color` | Length, format and emptiness rules |
| Numbers | `NumberValueObject`, `Integer`, `PositiveInteger`, `PositiveNumber` | Range and integer guarantees |
| Money | `Money`, `Currency` | Exact arithmetic in minor units, no floating point errors |
| File size | `FileSize`, `FileSizeUnit` | Bytes with SI and IEC units, parsing and unit conversion |
| Identifiers | `ShortId`, `UUID` | Generated and validated identifiers |
| Date & time | `Timestamp`, `CalendarDay`, `Hour`, `Duration`, `MonthOfYear` | Arithmetic and comparison without hand-rolled `Date` code |
| Coordinates | `Latitude`, `Longitude`, `Coordinates` | Range-checked geographic points |
| Hashes | `MD5Hash`, `SHA256Hash`, `SHA512Hash` | Validated digests |
| Media | `Media` | Typed media references |
| Collections | `UniqueObjectArray` | Immutable collection without duplicates, by Value Object equality |

Every class has its own page in the [API reference](https://haskou.github.io/value-objects/reference/).

Expand Down
9 changes: 9 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const referenceSidebar = [
items: [
{ text: 'NumberValueObject', link: '/reference/number-value-object' },
{ text: 'Integer', link: '/reference/integer' },
{ text: 'PositiveInteger', link: '/reference/positive-integer' },
{ text: 'PositiveNumber', link: '/reference/positive-number' },
],
},
Expand All @@ -76,6 +77,14 @@ const referenceSidebar = [
{ text: 'Currency', link: '/reference/currency' },
],
},
{
text: 'File size',
collapsed: true,
items: [
{ text: 'FileSize', link: '/reference/file-size' },
{ text: 'FileSizeUnit', link: '/reference/file-size-unit' },
],
},
{
text: 'Time',
collapsed: true,
Expand Down
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ yarn add @haskou/value-objects
| Section | Contents |
| --- | --- |
| [Base objects](/reference/value-object) | `ValueObject`, `NullObject`, `Enum` |
| [Strings and numbers](/reference/string-value-object) | `StringValueObject`, `Email`, `Color`, `Password`, `NumberValueObject`, `Integer`, `PositiveNumber` |
| [Strings and numbers](/reference/string-value-object) | `StringValueObject`, `Email`, `Color`, `Password`, `NumberValueObject`, `Integer`, `PositiveInteger`, `PositiveNumber` |
| [Identifiers](/reference/uuid) | `UUID`, `ShortId` |
| [Money](/reference/money) | `Money`, `Currency` |
| [File size](/reference/file-size) | `FileSize`, `FileSizeUnit` |
| [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
63 changes: 63 additions & 0 deletions docs/reference/file-size-unit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: FileSizeUnit
description: Enum value object for file size units, SI and IEC.
---

# `FileSizeUnit`

Enum value object for file size units, SI and IEC.

## Import

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

## Signature

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

## Constructor

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

## Values

| Standard | Base | Units |
| --- | --- | --- |
| Bytes | 1 | `BYTE` (`B`) |
| SI | 1000 | `KILOBYTE` (`kB`), `MEGABYTE` (`MB`), `GIGABYTE` (`GB`), `TERABYTE` (`TB`), `PETABYTE` (`PB`) |
| IEC | 1024 | `KIBIBYTE` (`KiB`), `MEBIBYTE` (`MiB`), `GIBIBYTE` (`GiB`), `TEBIBYTE` (`TiB`), `PEBIBYTE` (`PiB`) |

## Members

| Member | Description |
| --- | --- |
| `static SI` | Bytes plus the SI units, from smallest to largest. |
| `static IEC` | Bytes plus the IEC units, from smallest to largest. |
| `getBytes()` | Returns the bytes in one unit as `PositiveInteger`. |
| `getValues()` | Returns the accepted symbols. |

## Throws

- `ValueNotInEnumError` when the symbol is not one of the values above. Symbols are case-sensitive.

## Example

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

FileSizeUnit.MEBIBYTE.getBytes().valueOf(); // 1048576
new FileSizeUnit('MB').isEqual(FileSizeUnit.MEGABYTE); // true
new FileSizeUnit('KB'); // throws ValueNotInEnumError
```

## Related

- [`FileSize`](/reference/file-size)
- [`Enum`](/reference/enum)
- [Reference overview](/reference/)
84 changes: 84 additions & 0 deletions docs/reference/file-size.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: FileSize
description: File size value object stored in bytes.
---

# `FileSize`

File size value object stored in bytes.

## Import

```typescript
import { FileSize, FileSizeUnit } from '@haskou/value-objects';
```

## Signature

```typescript
class FileSize extends PositiveInteger
```

## Constructor

```typescript
constructor(bytes: number | NumberValueObject)
```

## Validation

Accepts integers greater than or equal to `0`.

## Methods

| Method | Description |
| --- | --- |
| `static fromBytes(bytes)` | Creates a size from bytes. |
| `static from(value, unit)` | Creates a size from a value in a `FileSizeUnit`, rounded to the nearest byte. |
| `static parse(text)` | Parses text such as `1.5 MiB` or `2MB`. |
| `toBytes()` | Returns the bytes as `PositiveInteger`. |
| `to(unit)` | Returns the size in `unit` as `NumberValueObject`, without rounding. |
| `toHumanReadable(units)` | Returns `{ value, unit }` using the largest unit of `units` that fits. |
| `add(other)` | Returns the sum as `FileSize`. |
| `subtract(other)` | Returns the difference as `FileSize`. |

Comparison and the rest of the arithmetic are inherited from `NumberValueObject`.

## Throws

This class can throw:

- `InvalidPositiveNumberError`
- `InvalidIntegerError`
- `InvalidNumberError`
- `InvalidFormatError` from `parse` when the text is not `<number> <unit>`
- `ValueNotInEnumError` from `parse` when the unit is unknown

## Example

```typescript
import { FileSize, FileSizeUnit } from '@haskou/value-objects';

const size = FileSize.from(1.5, FileSizeUnit.MEBIBYTE);

size.valueOf(); // 1572864
size.to(FileSizeUnit.KIBIBYTE).valueOf(); // 1536

const { value, unit } = size.toHumanReadable(FileSizeUnit.IEC);
`${value} ${unit}`; // '1.5 MiB'

FileSize.parse('2 MB').valueOf(); // 2000000
```

## Notes

- `toHumanReadable` does not format or round. Format `value` where you present it, for example with `Intl.NumberFormat`.
- SI units use base 1000 (`kB`, `MB`) and IEC units use base 1024 (`KiB`, `MiB`). `KB` is not a valid unit.
- `multiply` and `divide` do not round: a non-integer result throws `InvalidIntegerError`.
- Factories use the calling class, so subclasses get their own type back.

## Related

- [`FileSizeUnit`](/reference/file-size-unit)
- [`PositiveInteger`](/reference/positive-integer)
- [Reference overview](/reference/)
6 changes: 6 additions & 0 deletions docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Each exported class has its own page.
- [`Password`](/reference/password)
- [`NumberValueObject`](/reference/number-value-object)
- [`Integer`](/reference/integer)
- [`PositiveInteger`](/reference/positive-integer)
- [`PositiveNumber`](/reference/positive-number)
- [`Email`](/reference/email)
- [`Color`](/reference/color)
Expand All @@ -42,6 +43,11 @@ Each exported class has its own page.
- [`Money`](/reference/money)
- [`Currency`](/reference/currency)

## File size

- [`FileSize`](/reference/file-size)
- [`FileSizeUnit`](/reference/file-size-unit)

## Time

- [`CalendarDay`](/reference/calendar-day)
Expand Down
62 changes: 62 additions & 0 deletions docs/reference/positive-integer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: PositiveInteger
description: Integer value object constrained to non-negative values.
---

# `PositiveInteger`

Integer value object constrained to non-negative values.

## Import

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

## Signature

```typescript
class PositiveInteger extends Integer
```

## Constructor

```typescript
constructor(value: number | NumberValueObject)
```

## Validation

Accepts integers greater than or equal to `0`.

## Throws

This class can throw:

- `InvalidPositiveNumberError`
- `InvalidIntegerError`
- `InvalidNumberError`

## Example

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

const zero = new PositiveInteger(0);
const items = new PositiveInteger(3);

items.subtract(1).valueOf(); // 2
items.subtract(4); // throws InvalidPositiveNumberError
items.divide(2); // throws InvalidIntegerError
```

## Notes

- Like `PositiveNumber`, it accepts `0`.
- Arithmetic is inherited from `NumberValueObject` and goes through the constructor, so a result that is negative or not an integer throws.

## Related

- [`Integer`](/reference/integer)
- [`PositiveNumber`](/reference/positive-number)
- [Reference overview](/reference/)
16 changes: 16 additions & 0 deletions src/value-objects/PositiveInteger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { InvalidPositiveNumberError } from '../errors/InvalidPositiveNumberError';
import { assert } from '../patterns/Assert';
import { Integer } from './Integer';
import { NullObject } from './NullObject';
import { NumberValueObject } from './NumberValueObject';

export class PositiveInteger extends Integer {
constructor(value: number | NumberValueObject) {
super(value?.valueOf());

if (NullObject.isNullObject(this)) {
return this;
}
assert(this.value >= 0, new InvalidPositiveNumberError());
}
}
64 changes: 64 additions & 0 deletions src/value-objects/file-size/FileSize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { InvalidFormatError } from '../../errors/InvalidFormatError';
import { assert } from '../../patterns/Assert';
import { NumberValueObject } from '../NumberValueObject';
import { PositiveInteger } from '../PositiveInteger';
import { FileSizeUnit, FileSizeUnits } from './FileSizeUnit';

export interface HumanReadableFileSize {
value: NumberValueObject;
unit: FileSizeUnit;
}

export class FileSize extends PositiveInteger {
private static readonly FORMAT = /^(\d+(?:\.\d+)?)\s*(\S+)$/;

public static fromBytes<T extends FileSize = FileSize>(
this: new (bytes: number | NumberValueObject) => T,
bytes: number | NumberValueObject,
): T {
return new this(bytes);
}

public static from<T extends FileSize = FileSize>(
this: new (bytes: number | NumberValueObject) => T,
value: number | NumberValueObject,
unit: FileSizeUnit,
): T {
return new this(Math.round(value.valueOf() * unit.getBytes().valueOf()));
}

public static parse<T extends FileSize = FileSize>(
this: new (bytes: number | NumberValueObject) => T,
text: string,
): T {
const match = FileSize.FORMAT.exec(text.trim());

assert(match, new InvalidFormatError(text));

const [, value, symbol] = match;
const unit = new FileSizeUnit(symbol);

return new this(Math.round(Number(value) * unit.getBytes().valueOf()));
}

public toBytes(): PositiveInteger {
return new PositiveInteger(this.value);
}

public to(unit: FileSizeUnit): NumberValueObject {
return new NumberValueObject(this.value / unit.getBytes().valueOf());
}

public toHumanReadable(units: FileSizeUnits): HumanReadableFileSize {
const [smallest, ...rest] = [...units].sort((a, b) =>
a.getBytes().isLessThan(b.getBytes()) ? -1 : 1,
);
const unit = rest.reduce(
(fitting, candidate) =>
this.isGreaterOrEqualThan(candidate.getBytes()) ? candidate : fitting,
smallest,
);

return { unit, value: this.to(unit) };
}
}
Loading
Loading