# Formatting helpers

Turn raw ids, timestamps, durations, and big numbers into text a user can read with seedcord's formatting helpers.

Your handler works in ids, milliseconds, and raw numbers. The person reading the message works in dates, durations, and figures they can take in at a glance, so every value crossing between the two needs a conversion.

Those conversions are in `@seedcord/utils`, which your transport package re-exports. Everything here imports from `@seedcord/gateway` or `@seedcord/http` alongside the rest.

## A snowflake carries its own timestamp

Every Discord id encodes the moment it was created, in the top 42 bits. [`timestampFromSnowflake`](https://seedcord.org/docs/packages/utils/latest/functions/timestamp-from-snowflake) reads it back off any id, a user's, a guild's, a message's, or an interaction's.

```ts title="src/handlers/Ping.ts"
import {
    SlashHandler,
    SlashRoute,
    timestampFromSnowflake
} from '@seedcord/gateway';

@SlashRoute('ping')
export class Ping extends SlashHandler<'ping'> {
    public async execute(): Promise<void> {
        const sent = timestampFromSnowflake(this.event.id);

        await this.reply(`Roundtrip ${Date.now() - sent}ms.`);
    }
}
```

The scaffold's `/ping` measures a roundtrip this way, storing nothing. Discord stamped the id when it built the interaction, so the difference against `Date.now()` is the time in flight.

> **Warning**
>
> `timestampFromSnowflake` passes the id to `BigInt`, which throws a `SyntaxError` on a string holding anything other than digits. Check the string first when the id came from user input.

### Discord timestamps take seconds

`<t:1735689600:R>` renders as a live relative time in a message. Discord reads that number as **seconds**. JavaScript gives you milliseconds everywhere, so [`toEpochSeconds`](https://seedcord.org/docs/packages/utils/latest/functions/to-epoch-seconds) does the divide and the rounding.

```ts
const created = toEpochSeconds(timestampFromSnowflake(userId));
const line = `Account created <t:${created}:R>`;
```

[`currentTime`](https://seedcord.org/docs/packages/utils/latest/functions/current-time) returns now in the same unit, for a timestamp you build from scratch.

## Duration strings

[`parseDuration`](https://seedcord.org/docs/packages/utils/latest/functions/parse-duration) turns `'30m'` into milliseconds. It's the same parser [`Cooldown`](https://seedcord.org/docs/packages/core/latest/functions/cooldown) uses, exported so your own options can take the same shape.

```ts
parseDuration('24h'); // 86400000
parseDuration('500ms'); // 500
parseDuration('1.5h'); // null
parseDuration('30M'); // null
```

The grammar is one or more digits followed by one lowercase unit, `ms`, `s`, `m`, `h`, or `d`. Everything else returns null, including a fraction, an uppercase unit, a bare number, and a duration of zero.

[`ValidDuration`](https://seedcord.org/docs/packages/utils/latest/types/valid-duration) types a string in that shape. It accepts a fraction that the parser then rejects, so check the return.

## Shortening a big number

An embed showing `12345678` makes someone count digits. [`roundToDenomination`](https://seedcord.org/docs/packages/utils/latest/functions/round-to-denomination) shortens it to a suffixed form like `12.3M`.

```ts
roundToDenomination(999); // '999'
roundToDenomination(1234); // '1.2K'
roundToDenomination(999_999); // '1M'
roundToDenomination(12_345_678, { precision: 2 }); // '12.35M'
roundToDenomination(12_345_678, { suffixes: ['k'] }); // '12345.7k'
```

Anything below 1000 comes back unchanged. `precision` defaults to `1`. `suffixes` replaces the default `['K', 'M', 'B', 'T', 'Q']` with a list of any length. Shortening stops at the last suffix you give it, which leaves five digits in front of the `k` above.

999,999 rounds to 1000K. A label reading `1000K` looks wrong, so it carries to `1M`.

Shortening costs you precision, since 1234 and 1240 both read as `1.2K`. Keep the digits where the exact number is the point, as in a currency balance.

## Ranks

[`ordinal`](https://seedcord.org/docs/packages/utils/latest/functions/ordinal) adds the rank suffix a leaderboard prints. It covers 11th, 12th, and 13th, which a hand-rolled version usually gets wrong.

```ts
ordinal(1); // '1st'
ordinal(11); // '11th'
ordinal(21); // '21st'
ordinal(113); // '113th'
```

## Identifiers a reader can scan

[`prettify`](https://seedcord.org/docs/packages/utils/latest/functions/prettify) turns an identifier into words. It reads camelCase, PascalCase, snake_case, and kebab-case, which is how seedcord prints `ManageMessages` as `Manage Messages` in a permission refusal.

```ts
prettify('ManageMessages'); // 'Manage Messages'
prettify('snake_case_string'); // 'snake case string'
prettify('kebab-case-string'); // 'kebab case string'
prettify('someLongOption', { capitalize: true }); // 'Some long option'
```

`capitalize` raises the first letter of the whole phrase and lowers the rest, which gives you a sentence. Title case takes your own pass over the words.

## Picking at random

[`fyShuffle`](https://seedcord.org/docs/packages/utils/latest/functions/fy-shuffle) returns a shuffled copy and leaves the input alone. It runs Fisher-Yates, so every ordering is equally likely.

```ts
const [winner] = fyShuffle(entrants);
```

> **Tip**
>
> `items.sort(() => Math.random() - 0.5)` is the common way to write this by hand. It's measurably biased. A comparator has to give the same answer every time it sees a pair. That one answers at random on each call, which leaves some orderings far more likely than others.

[`generateCode`](https://seedcord.org/docs/packages/utils/latest/functions/generate-code) returns a random number with exactly that many digits, never starting with a zero. It draws from `Math.random()`, which suits a pairing code. Someone who sees enough of its earlier outputs can predict the next one, so use `node:crypto` for a code that guards something.

## Objects that won't stringify

discord.js structures point back at the client, which points back at them. `JSON.stringify` on one throws. [`filterCirculars`](https://seedcord.org/docs/packages/utils/latest/functions/filter-circulars) returns a copy with each cycle replaced by a marker.

```ts
const safe = filterCirculars(payload);
const withMarker = filterCirculars(payload, { marker: '[loop]' });
const viaJson = filterCirculars(payload, { mode: 'json', logger });
```

`mode` picks how the copy is built. The default `'decycle'` clones the value directly. `'json'` runs stringify and parse, which lets a `toJSON()` method rewrite its own object while that runs. Both read own enumerable properties, so a getter defined on a class prototype is absent from the result.

Pass a `logger`, as the `viaJson` line does, to see why a copy failed. Without one the failure returns quietly as `{ '[unserializable]': ... }`.

[`hasKeys`](https://seedcord.org/docs/packages/utils/latest/functions/has-keys) checks a nested path and narrows the type when it passes.

```ts
if (hasKeys(report, ['author.profile.handle', 'score'])) {
    report.author.profile.handle.toUpperCase();
    report.score.toFixed(2);
}
```

Each string is a dot path, checked against null and undefined at every step. The narrowing covers every path you passed.

## The rest

[The reference](https://seedcord.org/docs/packages/utils/latest) lists every function in the package with its signature and its documentation.
