# Formatting helpers

Helpers for the text your bot sends, so a raw id or a millisecond count reaches the user as something readable. Covers snowflake timestamps, durations, big numbers, ranks, names a reader scans, random picks, and objects that won't stringify.

Your transport package re-exports `@seedcord/utils`. 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://docs.seedcord.org/packages/utils/latest/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://docs.seedcord.org/packages/utils/latest/to-epoch-seconds) does the divide and the rounding.

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

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

## Duration strings

[`parseDuration`](https://docs.seedcord.org/packages/utils/latest/parse-duration) turns `'30m'` into milliseconds. It's the same parser [`Cooldown`](https://docs.seedcord.org/packages/core/latest/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://docs.seedcord.org/packages/utils/latest/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://docs.seedcord.org/packages/utils/latest/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://docs.seedcord.org/packages/utils/latest/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'
```

## Names a reader scans

[`prettify`](https://docs.seedcord.org/packages/utils/latest/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://docs.seedcord.org/packages/utils/latest/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://docs.seedcord.org/packages/utils/latest/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://docs.seedcord.org/packages/utils/latest/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://docs.seedcord.org/packages/utils/latest/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://docs.seedcord.org/packages/utils/latest) lists every function in the package with its signature and its documentation.
