Formatting Pattern
Format numbers, currency, text, and file sizes consistently across your application.
Overview#
Consistent formatting improves user experience and reduces code duplication. This pattern provides utilities for numbers, currency, text manipulation, and duration formatting using native JavaScript APIs and custom helpers.
When to use:
- Displaying prices and currency
- Formatting large numbers (1K, 1M)
- Text truncation and highlighting
- File size display
- Duration and time formatting
Key features:
- Number formatting with locales
- Currency handling
- Text manipulation
- File size formatting
- Duration display
- React components
Code Example#
Number Formatting#
1// lib/format/numbers.ts
2
3// Format number with locale
4export function formatNumber(
5 value: number,
6 options?: Intl.NumberFormatOptions
7): string {
8 return new Intl.NumberFormat('en-US', options).format(value)
9}
10
11// Compact number (1K, 1M, etc.)
12export function formatCompact(value: number): string {
13 return new Intl.NumberFormat('en-US', {
14 notation: 'compact',
15 maximumFractionDigits: 1
16 }).format(value)
17}
18
19// Percentage
20export function formatPercent(value: number, decimals = 1): string {
21 return new Intl.NumberFormat('en-US', {
22 style: 'percent',
23 minimumFractionDigits: decimals,
24 maximumFractionDigits: decimals
25 }).format(value)
26}
27
28// Ordinal (1st, 2nd, 3rd, etc.)
29export function formatOrdinal(value: number): string {
30 const pr = new Intl.PluralRules('en-US', { type: 'ordinal' })
31 const suffixes: Record<string, string> = {
32 one: 'st',
33 two: 'nd',
34 few: 'rd',
35 other: 'th'
36 }
37 return `${value}${suffixes[pr.select(value)]}`
38}
39
40// Decimal places
41export function formatDecimal(value: number, places = 2): string {
42 return value.toFixed(places)
43}
44
45// With units
46export function formatWithUnit(
47 value: number,
48 unit: string,
49 options?: { compact?: boolean }
50): string {
51 const formatted = options?.compact ? formatCompact(value) : formatNumber(value)
52 return `${formatted} ${unit}${value !== 1 ? 's' : ''}`
53}Currency Formatting#
1// lib/format/currency.ts
2
3interface CurrencyOptions {
4 currency?: string
5 locale?: string
6 compact?: boolean
7}
8
9export function formatCurrency(
10 amount: number,
11 options: CurrencyOptions = {}
12): string {
13 const { currency = 'USD', locale = 'en-US', compact = false } = options
14
15 return new Intl.NumberFormat(locale, {
16 style: 'currency',
17 currency,
18 notation: compact ? 'compact' : 'standard',
19 maximumFractionDigits: compact ? 1 : 2
20 }).format(amount)
21}
22
23// Format cents to dollars
24export function formatCents(cents: number, options?: CurrencyOptions): string {
25 return formatCurrency(cents / 100, options)
26}
27
28// Parse currency string to number
29export function parseCurrency(value: string): number {
30 const cleaned = value.replace(/[^0-9.-]+/g, '')
31 return parseFloat(cleaned) || 0
32}
33
34// Currency input formatter (for controlled inputs)
35export function formatCurrencyInput(value: string): string {
36 const num = value.replace(/[^0-9]/g, '')
37 if (!num) return ''
38 const cents = parseInt(num, 10)
39 return formatCents(cents)
40}Text Formatting#
1// lib/format/text.ts
2
3// Truncate with ellipsis
4export function truncate(text: string, maxLength: number): string {
5 if (text.length <= maxLength) return text
6 return text.slice(0, maxLength - 3) + '...'
7}
8
9// Truncate by words
10export function truncateWords(text: string, maxWords: number): string {
11 const words = text.split(/\s+/)
12 if (words.length <= maxWords) return text
13 return words.slice(0, maxWords).join(' ') + '...'
14}
15
16// Capitalize first letter
17export function capitalize(text: string): string {
18 return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()
19}
20
21// Title case
22export function titleCase(text: string): string {
23 return text
24 .toLowerCase()
25 .split(' ')
26 .map(word => word.charAt(0).toUpperCase() + word.slice(1))
27 .join(' ')
28}
29
30// Sentence case
31export function sentenceCase(text: string): string {
32 return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()
33}
34
35// Slugify
36export function slugify(text: string): string {
37 return text
38 .toLowerCase()
39 .trim()
40 .replace(/[^\w\s-]/g, '')
41 .replace(/[\s_-]+/g, '-')
42 .replace(/^-+|-+$/g, '')
43}
44
45// Pluralize
46export function pluralize(
47 count: number,
48 singular: string,
49 plural?: string
50): string {
51 if (count === 1) return singular
52 return plural ?? `${singular}s`
53}
54
55// Add "a" or "an"
56export function addArticle(word: string): string {
57 const vowels = ['a', 'e', 'i', 'o', 'u']
58 const article = vowels.includes(word[0].toLowerCase()) ? 'an' : 'a'
59 return `${article} ${word}`
60}Text Highlighting#
1// lib/format/text.ts
2
3interface HighlightPart {
4 text: string
5 highlighted: boolean
6}
7
8export function highlightText(
9 text: string,
10 query: string
11): HighlightPart[] {
12 if (!query) return [{ text, highlighted: false }]
13
14 const regex = new RegExp(`(${escapeRegex(query)})`, 'gi')
15 const parts = text.split(regex)
16
17 return parts.map(part => ({
18 text: part,
19 highlighted: part.toLowerCase() === query.toLowerCase()
20 }))
21}
22
23function escapeRegex(str: string): string {
24 return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
25}
26
27// React component
28export function Highlight({ text, query }: { text: string; query: string }) {
29 const parts = highlightText(text, query)
30
31 return (
32 <>
33 {parts.map((part, i) => (
34 <span
35 key={i}
36 className={part.highlighted ? 'bg-yellow-200' : undefined}
37 >
38 {part.text}
39 </span>
40 ))}
41 </>
42 )
43}File Size Formatting#
1// lib/format/filesize.ts
2
3const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
4
5export function formatFileSize(bytes: number, decimals = 2): string {
6 if (bytes === 0) return '0 B'
7
8 const k = 1024
9 const i = Math.floor(Math.log(bytes) / Math.log(k))
10 const size = bytes / Math.pow(k, i)
11
12 return `${size.toFixed(decimals)} ${units[i]}`
13}
14
15export function parseFileSize(sizeString: string): number {
16 const match = sizeString.match(/^([\d.]+)\s*([A-Z]+)$/i)
17 if (!match) return 0
18
19 const [, value, unit] = match
20 const unitIndex = units.findIndex(u => u.toLowerCase() === unit.toLowerCase())
21 if (unitIndex === -1) return 0
22
23 return parseFloat(value) * Math.pow(1024, unitIndex)
24}Duration Formatting#
1// lib/format/duration.ts
2
3interface DurationParts {
4 days: number
5 hours: number
6 minutes: number
7 seconds: number
8}
9
10export function parseDuration(ms: number): DurationParts {
11 const seconds = Math.floor(ms / 1000) % 60
12 const minutes = Math.floor(ms / (1000 * 60)) % 60
13 const hours = Math.floor(ms / (1000 * 60 * 60)) % 24
14 const days = Math.floor(ms / (1000 * 60 * 60 * 24))
15
16 return { days, hours, minutes, seconds }
17}
18
19export function formatDuration(
20 ms: number,
21 options?: { short?: boolean }
22): string {
23 const { days, hours, minutes, seconds } = parseDuration(ms)
24 const parts: string[] = []
25
26 if (options?.short) {
27 if (days > 0) parts.push(`${days}d`)
28 if (hours > 0) parts.push(`${hours}h`)
29 if (minutes > 0) parts.push(`${minutes}m`)
30 if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`)
31 return parts.join(' ')
32 }
33
34 if (days > 0) parts.push(`${days} day${days !== 1 ? 's' : ''}`)
35 if (hours > 0) parts.push(`${hours} hour${hours !== 1 ? 's' : ''}`)
36 if (minutes > 0) parts.push(`${minutes} minute${minutes !== 1 ? 's' : ''}`)
37 if (seconds > 0 || parts.length === 0) {
38 parts.push(`${seconds} second${seconds !== 1 ? 's' : ''}`)
39 }
40
41 return parts.join(', ')
42}
43
44// Timer format (HH:MM:SS)
45export function formatTimer(ms: number): string {
46 const { hours, minutes, seconds } = parseDuration(ms)
47 const pad = (n: number) => n.toString().padStart(2, '0')
48
49 if (hours > 0) {
50 return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
51 }
52 return `${pad(minutes)}:${pad(seconds)}`
53}
54
55// Video duration format
56export function formatVideoDuration(seconds: number): string {
57 const mins = Math.floor(seconds / 60)
58 const secs = Math.floor(seconds % 60)
59 return `${mins}:${secs.toString().padStart(2, '0')}`
60}React Components#
1// components/format/Currency.tsx
2import { formatCurrency } from '@/lib/format/currency'
3
4interface CurrencyProps {
5 value: number
6 currency?: string
7 compact?: boolean
8}
9
10export function Currency({
11 value,
12 currency = 'USD',
13 compact = false
14}: CurrencyProps) {
15 return <span>{formatCurrency(value, { currency, compact })}</span>
16}
17
18// components/format/Number.tsx
19import { formatNumber, formatCompact, formatPercent } from '@/lib/format/numbers'
20
21interface NumberProps {
22 value: number
23 compact?: boolean
24 percent?: boolean
25}
26
27export function Number({ value, compact, percent }: NumberProps) {
28 if (percent) return <span>{formatPercent(value)}</span>
29 if (compact) return <span>{formatCompact(value)}</span>
30 return <span>{formatNumber(value)}</span>
31}
32
33// components/format/FileSize.tsx
34import { formatFileSize } from '@/lib/format/filesize'
35
36interface FileSizeProps {
37 bytes: number
38 decimals?: number
39}
40
41export function FileSize({ bytes, decimals = 2 }: FileSizeProps) {
42 return <span>{formatFileSize(bytes, decimals)}</span>
43}Usage Instructions#
- Create format utilities: Add functions to
lib/format/directory - Use consistently: Import formatters across your codebase
- Consider locale: Use Intl APIs for locale-aware formatting
- Create React components: Wrap formatters in reusable components
- Handle edge cases: Check for null, undefined, and invalid values
Best Practices#
- Use Intl APIs - Native browser support for i18n formatting
- Be consistent - Use the same format everywhere
- Handle zeros - Special case for zero values
- Consider locale - Some formats vary by region
- Test edge cases - Large numbers, negatives, decimals
- Memoize expensive operations - Cache formatted values when possible
- Type your options - Use TypeScript for format options
- Document patterns - Comment non-obvious format strings
Related Patterns#
- Date Handling - Date and time formatting
- Validation - Input validation
- Tables - Displaying formatted data