Spaces:
Sleeping
Sleeping
File size: 5,927 Bytes
cd99321 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | import type { AssignmentsMap } from '../types'
// Collapses verbose Nominatim display_name strings (e.g. "Place, 1, Road, Neighbourhood,
// City, County, State, Country, Postcode, Country") into "Place, Postcode, Country".
// Clean short names (≤3 parts) pass through untouched.
export function formatLocationName(raw: string | null | undefined): string {
if (!raw) return ''
const parts = raw.split(',').map(p => p.trim()).filter(Boolean)
if (parts.length <= 3) return raw.trim()
// Dedup preserving insertion order
const seen = new Set<string>()
const unique: string[] = []
for (const p of parts) {
if (!seen.has(p.toLowerCase())) { seen.add(p.toLowerCase()); unique.push(p) }
}
if (unique.length <= 3) return unique.join(', ')
const name = unique[0]
const last = unique[unique.length - 1]
const secondLast = unique.length >= 2 ? unique[unique.length - 2] : null
// Detect postcode at tail: short alphanumeric with at least one digit, ≤10 chars
const postalRe = /^[A-Z0-9][A-Z0-9\s\-]{1,8}$/i
const isLastPostal = postalRe.test(last) && /\d/.test(last) && last.length <= 10
const postcode = isLastPostal ? last : null
const country = isLastPostal ? secondLast : last
const result: string[] = [name]
if (postcode && postcode !== name) result.push(postcode)
if (country && country !== name && country !== postcode) result.push(country)
return result.join(', ')
}
const ZERO_DECIMAL_CURRENCIES = new Set(['JPY', 'KRW', 'VND', 'CLP', 'ISK', 'HUF'])
export function currencyDecimals(currency: string): number {
return ZERO_DECIMAL_CURRENCIES.has(currency.toUpperCase()) ? 0 : 2
}
// Each currency formats in its own home convention (symbol position, grouping and
// decimal separators) regardless of the app language — so EUR is always "1.234,56 €"
// and USD always "$1,234.56". Intl derives all of that from the locale, so we map
// each supported currency to a representative locale (Latin-digit variants for the
// Arabic/Bengali ones to avoid non-Latin numerals).
const CURRENCY_LOCALE: Record<string, string> = {
EUR: 'de-DE', USD: 'en-US', GBP: 'en-GB', JPY: 'ja-JP', CHF: 'de-CH',
CZK: 'cs-CZ', PLN: 'pl-PL', SEK: 'sv-SE', NOK: 'nb-NO', DKK: 'da-DK',
TRY: 'tr-TR', THB: 'th-TH', AUD: 'en-AU', CAD: 'en-CA', NZD: 'en-NZ',
BRL: 'pt-BR', MXN: 'es-MX', INR: 'en-IN', IDR: 'id-ID', MYR: 'ms-MY',
PHP: 'en-PH', SGD: 'en-SG', KRW: 'ko-KR', CNY: 'zh-CN', HKD: 'en-HK',
TWD: 'zh-TW', ZAR: 'en-ZA', AED: 'en-AE', SAR: 'en-SA', ILS: 'he-IL',
EGP: 'en-EG', MAD: 'fr-MA', HUF: 'hu-HU', RON: 'ro-RO', BGN: 'bg-BG',
HRK: 'hr-HR', ISK: 'is-IS', RUB: 'ru-RU', UAH: 'uk-UA', BDT: 'en-BD',
LKR: 'en-LK', VND: 'vi-VN', CLP: 'es-CL', COP: 'es-CO', PEN: 'es-PE',
ARS: 'es-AR',
}
export function currencyLocale(currency: string): string {
return CURRENCY_LOCALE[(currency || '').toUpperCase()] || 'en-US'
}
/**
* Locale- and currency-correct money formatting via Intl: the symbol position,
* thousands/decimal separators and decimal count all follow the user's locale
* and the currency itself (e.g. de-DE EUR → "1.234,56 €", en-US USD → "$1,234.56",
* ja-JP JPY → "¥1,235"). Falls back to a "<number> CODE" suffix for unknown codes.
*/
export function formatMoney(
value: number,
currency: string,
locale: string,
opts?: { decimals?: number },
): string {
const cur = (currency || 'EUR').toUpperCase()
const decimals = opts?.decimals ?? currencyDecimals(cur)
// Format in the currency's home convention, not the app language, so the symbol
// position and separators are always correct for that currency. `locale` stays
// as a last-resort fallback for the error path.
const fmtLocale = currencyLocale(cur)
try {
return new Intl.NumberFormat(fmtLocale, {
style: 'currency',
currency: cur,
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(value || 0)
} catch {
return `${(value || 0).toLocaleString(locale || fmtLocale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals })} ${cur}`
}
}
export function formatDate(dateStr: string | null | undefined, locale: string, timeZone?: string): string | null {
if (!dateStr) return null
const opts: Intl.DateTimeFormatOptions = {
weekday: 'short', day: 'numeric', month: 'short',
timeZone: timeZone || 'UTC',
}
return new Date(dateStr + 'T00:00:00Z').toLocaleDateString(locale, opts)
}
export function formatTime(timeStr: string | null | undefined, locale: string, timeFormat: string): string {
if (!timeStr) return ''
try {
const parts = timeStr.split(':')
const h = Number(parts[0]) || 0
const m = Number(parts[1]) || 0
if (isNaN(h)) return timeStr
if (timeFormat === '12h') {
const period = h >= 12 ? 'PM' : 'AM'
const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h
return `${h12}:${String(m).padStart(2, '0')} ${period}`
}
const str = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`
return locale?.startsWith('de') ? `${str} Uhr` : str
} catch { return timeStr }
}
export function splitReservationDateTime(value?: string | null): { date: string | null; time: string | null } {
if (!value) return { date: null, time: null }
const isoDate = /^\d{4}-\d{2}-\d{2}$/
if (value.includes('T')) {
const [d, t] = value.split('T')
return { date: isoDate.test(d) ? d : null, time: t ? t.slice(0, 5) : null }
}
if (isoDate.test(value)) return { date: value, time: null }
if (/^\d{1,2}:\d{2}/.test(value)) return { date: null, time: value.slice(0, 5) }
return { date: null, time: null }
}
export function dayTotalCost(dayId: number, assignments: AssignmentsMap, currency: string): string | null {
const da = assignments[String(dayId)] || []
const total = da.reduce((s, a) => s + (parseFloat(String(a.place?.price ?? '')) || 0), 0)
return total > 0 ? `${total.toFixed(0)} ${currency}` : null
}
|