npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@proedis/formatters

v2.0.1

Published

Set of smart formatters to display data as string on user interface

Readme

@proedis/formatters

Numbers and durations turned into the strings a person actually reads — in Italian or in English. 🔢

npm license


✨ What's in the box

Three formatters, zero dependencies. 🪶

| Formatter | Turns | Into | | --- | --- | --- | | formatNumber | 1234.5678 | '1,234.57' | | formatDuration | 90061000 | '1 day, 1 hour, 1 minute and 1 second' | | pluralize | 3 | '3 files' |

Each one is callable straight away, and each one carries a create method that hands back a preconfigured copy — so an application declares its currency style or its duration style once, and passes only the value from then on.

📦 Installation

yarn add @proedis/formatters

🚀 Quick start

import { formatDuration, formatNumber, pluralize } from '@proedis/formatters';

formatNumber(1234.5678, { precision: 2 });               // '1,234.57'
formatDuration(90061000, { locale: 'it' });              // '1 giorno, 1 ora, 1 minuto e 1 secondo'
pluralize(3, 'file', 'files');                           // '3 files'

/** …or declare the style once */
const currency = formatNumber.create({ precision: 2, prefix: '€', pattern: '%p%m%n' });

currency(1234.5);                                        // '€1,234.50'
currency(1234.5, { prefix: '$' });                       // '$1,234.50' — a per call override

📖 API

🔢 formatNumber(value, config?)

| Option | Default | What it does | | --- | --- | --- | | precision | 0 | how many decimals to render | | flexibleDecimals | false | drop trailing zeros, down to minPrecision | | minPrecision | 0 | the floor for flexible decimals | | decimalSeparator | '.' | | | thousandSeparator | ',' | | | prefix / suffix | '' | | | pattern | '%p %m %n %s' | how the pieces are arranged |

The pattern is what makes the arrangement yours rather than the formatter's. It holds four placeholders — %p prefix, %m minus sign, %n number, %s suffix. Runs of whitespace collapse to one space and the result is trimmed, so an unused placeholder leaves no gap behind.

⚠️ The default pattern puts a space on both sides of the minus sign, which is rarely what a currency wants:

formatNumber(-1234.5, { precision: 2, prefix: '€' });                     // '€ - 1,234.50'
formatNumber(-1234.5, { precision: 2, prefix: '€', pattern: '%p%m%n' });  // '€-1,234.50' ✅

Flexible decimals are for prices and quantities that should not shout .00 at you, while still lining up:

formatNumber(1.5, { precision: 4, flexibleDecimals: true });                   // '1.5'
formatNumber(1.5, { precision: 4, flexibleDecimals: true, minPrecision: 2 });  // '1.50'
formatNumber(1,   { precision: 4, flexibleDecimals: true, minPrecision: 2 });  // '1.00'

⏱️ formatDuration(value, config?)

The value is split across the requested units from the largest down, each unit taking what is left after the ones before it, and the smallest unit keeps the remainder as decimals. Only non zero parts are rendered, so nothing pads the output with zeros.

| Option | Default | What it does | | --- | --- | --- | | units | [ 'y', 'mo', 'w', 'd', 'h', 'm', 's' ] | which units may appear | | sourceUnit | 'ms' | the unit the input is expressed in | | locale | 'en' | 'en' or 'it' | | largest | null | render at most this many parts | | maxDecimals | 2 | precision of the smallest unit | | round | false | round every part, carrying a full unit up into the next larger one | | delimiter | ', ' | between all parts but the last two | | conjunction | per locale | between the last two parts | | decimals | per locale | the decimal separator of a count |

const units: DurationUnit[] = [ 'd', 'h', 'm' ];

formatDuration(90061000, { units });                  // '1 day, 1 hour and 1.02 minutes'
formatDuration(90061000, { units, round: true });     // '1 day, 1 hour and 1 minute'
formatDuration(90061000, { largest: 2 });             // '1 day and 1 hour'
formatDuration(3.5, { sourceUnit: 'h' });             // '3 hours and 30 minutes'
formatDuration(0);                                    // '0 seconds'

💡 round is the option to reach for when a UI must never show 59.6 minutes: it rounds each part and, when a rounded part adds up to a whole larger unit, moves it up instead of printing 60 minutes.

Locales bring their own separator and conjunction, so 'it' is not just a word swap:

formatDuration(5400000, { units: [ 'h' ] });                  // '1.5 hours'
formatDuration(5400000, { units: [ 'h' ], locale: 'it' });    // '1,5 ore'

An unrecognised locale silently falls back to English. Locale is a literal union, so that can only happen from untyped JavaScript — and a formatter in a render path is the wrong place to throw.

🔤 pluralize(value, ifSingular, ifPlural)

Only an exact 1 is singular; 0 and any decimal take the plural. Each form is either a suffix appended after the count, or a function taking full control:

pluralize(1, 'file', 'files');    // '1 file'
pluralize(0, 'file', 'files');    // '0 files'
pluralize(null, 'file', 'files'); // '0 files'

pluralize(3, (n) => `only ${n} left`, (n) => `${n} in stock`);   // '3 in stock'

const files = pluralize.create('file', 'files');
files(12);                        // '12 files'

🏭 create, and how configuration layers

formatNumber.create and formatDuration.create return a formatter that carries defaults and can itself be narrowed further:

const currency = formatNumber.create({ precision: 2, prefix: '€', pattern: '%p%m%n' });
const dollars  = currency.create({ prefix: '$' });

dollars(9.5);   // '$9.50'

Configuration layers at three points — the parent's defaults, the ones given to create, the ones passed to a single call — with the later always winning. ⚠️ The layering is a shallow spread, one level deep: a nested object in a configuration replaces its counterpart rather than merging with it. Every configuration in this package is flat, so this only matters if you build your own formatter on instantiateFormatter.

🧮 Number coercion, everywhere

Every formatter runs its input through the same coercion: null, undefined, a string that does not parse, NaN and both infinities all become the fallback, normally 0. A formatter sits in a render path, and returning '0' always beats propagating NaN into the interface.

🔀 Migrating to 2.x

This release is mostly bug fixes, and three of them changed output you may have worked around:

| Fixed | Before | Now | | --- | --- | --- | | Single part durations | the conjunction was glued to the front of the result: formatDuration(1000) returned ' and 1 second' | '1 second' | | round: true | threw TypeError on every call — the loop read one index past the end of the parts array | rounds, and carries a full unit up | | conjunction / decimals | written straight into the shared locale dictionary, so one call leaked its override into every later call for the lifetime of the process | resolved per call, nothing is mutated | | The decimals option | declared in the config interface, never read. The per locale values were also swapped — en carried ',' and it carried '.' | wired up, and each locale carries its own correct separator | | Unrecognised locale | reached for global.console, which does not exist in a browser: a ReferenceError instead of a warning | a silent fallback to English |

⚠️ Because decimals now works and the locale separators were corrected, Italian durations render 1,5 ore where they used to render 1.5 ore. That is the fix, but it is a visible change.

The internal locale layer was reshaped along the way (LocaleTransformer, LocaleDictionary and extractLocaleDictionary are gone, replaced by DurationLocaleDictionary and DurationUnitLabel). None of it was ever exported from the package root, so a normal import is unaffected.

🤝 Compatibility

| Requirement | Range | | --- | --- | | Dependencies | none | | typescript | >=5.2.0 | | Runtime | ES2022 — roughly Safari 16.4 / Chrome 94 / Node 16.11 |

📄 License

MIT © Proedis S.r.l.