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#

  1. Create format utilities: Add functions to lib/format/ directory
  2. Use consistently: Import formatters across your codebase
  3. Consider locale: Use Intl APIs for locale-aware formatting
  4. Create React components: Wrap formatters in reusable components
  5. Handle edge cases: Check for null, undefined, and invalid values

Best Practices#

  1. Use Intl APIs - Native browser support for i18n formatting
  2. Be consistent - Use the same format everywhere
  3. Handle zeros - Special case for zero values
  4. Consider locale - Some formats vary by region
  5. Test edge cases - Large numbers, negatives, decimals
  6. Memoize expensive operations - Cache formatted values when possible
  7. Type your options - Use TypeScript for format options
  8. Document patterns - Comment non-obvious format strings