commit ddd1c2df9e9848b59faf92e9e707e9be4b8fd26f Author: upstream Date: Fri Sep 4 09:04:26 2026 +0200 Upstream EvanHahn/HumanizeDuration.js state of the touched files before f21a996c26 diff --git a/humanize-duration.js b/humanize-duration.js new file mode 100644 index 0000000..0df4917 --- /dev/null +++ b/humanize-duration.js @@ -0,0 +1,1665 @@ +// HumanizeDuration.js - https://git.io/j0HgmQ + +/** + * @typedef {string | ((unitCount: number) => string)} Unit + */ + +/** + * @typedef {"y" | "mo" | "w" | "d" | "h" | "m" | "s" | "ms"} UnitName + */ + +/** + * @typedef {Object} UnitMeasures + * @prop {number} y + * @prop {number} mo + * @prop {number} w + * @prop {number} d + * @prop {number} h + * @prop {number} m + * @prop {number} s + * @prop {number} ms + */ + +/** + * @internal + * @typedef {[string, string, string, string, string, string, string, string, string, string]} DigitReplacements + */ + +/** + * @typedef {Object} Language + * @prop {Unit} y + * @prop {Unit} mo + * @prop {Unit} w + * @prop {Unit} d + * @prop {Unit} h + * @prop {Unit} m + * @prop {Unit} s + * @prop {Unit} ms + * @prop {string} [decimal] + * @prop {string} [delimiter] + * @prop {DigitReplacements} [_digitReplacements] + * @prop {boolean} [_numberFirst] + * @prop {boolean} [_hideCountIf2] + */ + +/** + * @typedef {Object} Options + * @prop {string} [language] + * @prop {Record} [languages] + * @prop {string[]} [fallbacks] + * @prop {string} [delimiter] + * @prop {string} [spacer] + * @prop {boolean} [round] + * @prop {number} [largest] + * @prop {UnitName[]} [units] + * @prop {string} [decimal] + * @prop {string} [conjunction] + * @prop {number} [maxDecimalPoints] + * @prop {UnitMeasures} [unitMeasures] + * @prop {boolean} [serialComma] + * @prop {DigitReplacements} [digitReplacements] + */ + +/** + * @internal + * @typedef {Options & Required>} NormalizedOptions + */ + +/** + * @typedef {((ms: number, humanizerOptions?: Options) => string) & NormalizedOptions} Humanizer + */ + +( + /** + * @param {Record} global + */ + function (global) { + // Fallback for `Object.assign` if relevant. + var assign = + Object.assign || + /** @param {...any} destination */ + function (destination) { + var source; + for (var i = 1; i < arguments.length; i++) { + source = arguments[i]; + for (var prop in source) { + if (has(source, prop)) { + /** @type {any} */ (destination)[prop] = source[prop]; + } + } + } + return destination; + }; + + // Fallback for `Array.isArray` if relevant. + var isArray = + Array.isArray || + function (arg) { + return Object.prototype.toString.call(arg) === "[object Array]"; + }; + + // This has to be defined separately because of a bug: we want to alias + // `gr` and `el` for backwards-compatiblity. In a breaking change, we can + // remove `gr` entirely. + // See https://github.com/EvanHahn/HumanizeDuration.js/issues/143 for more. + var GREEK = onesLanguage( + ["χρόνος", "χρόνια"], + ["μήνας", "μήνες"], + ["εβδομάδα", "εβδομάδες"], + ["μέρα", "μέρες"], + ["ώρα", "ώρες"], + ["λεπτό", "λεπτά"], + ["δευτερόλεπτο", "δευτερόλεπτα"], + ["χιλιοστό του δευτερολέπτου", "χιλιοστά του δευτερολέπτου"], + "," + ); + + /** + * @internal + * @type {Record} + */ + var LANGUAGES = { + af: onesLanguage( + ["jaar", "jaar"], + ["maand", "maande"], + ["week", "weke"], + ["dag", "dae"], + ["uur", "ure"], + ["minuut", "minute"], + ["sekonde", "sekondes"], + ["millisekonde", "millisekondes"], + "," + ), + am: language("ዓመት", "ወር", "ሳምንት", "ቀን", "ሰዓት", "ደቂቃ", "ሰከንድ", "ሚሊሰከንድ"), + ar: assign( + language( + function (c) { + return ["سنة", "سنتان", "سنوات"][getArabicForm(c)]; + }, + function (c) { + return ["شهر", "شهران", "أشهر"][getArabicForm(c)]; + }, + function (c) { + return ["أسبوع", "أسبوعين", "أسابيع"][getArabicForm(c)]; + }, + function (c) { + return ["يوم", "يومين", "أيام"][getArabicForm(c)]; + }, + function (c) { + return ["ساعة", "ساعتين", "ساعات"][getArabicForm(c)]; + }, + function (c) { + return ["دقيقة", "دقيقتان", "دقائق"][getArabicForm(c)]; + }, + function (c) { + return ["ثانية", "ثانيتان", "ثواني"][getArabicForm(c)]; + }, + function (c) { + return ["جزء من الثانية", "جزآن من الثانية", "أجزاء من الثانية"][ + getArabicForm(c) + ]; + }, + "," + ), + { + delimiter: " ﻭ ", + _hideCountIf2: true, + _digitReplacements: ["۰", "١", "٢", "٣", "٤", "٥", "٦", "٧", "٨", "٩"] + } + ), + bg: slavicLanguage( + ["години", "година", "години"], + ["месеца", "месец", "месеца"], + ["седмици", "седмица", "седмици"], + ["дни", "ден", "дни"], + ["часа", "час", "часа"], + ["минути", "минута", "минути"], + ["секунди", "секунда", "секунди"], + ["милисекунди", "милисекунда", "милисекунди"] + ), + bn: language( + "বছর", + "মাস", + "সপ্তাহ", + "দিন", + "ঘন্টা", + "মিনিট", + "সেকেন্ড", + "মিলিসেকেন্ড" + ), + ca: onesLanguage( + ["any", "anys"], + ["mes", "mesos"], + ["setmana", "setmanes"], + ["dia", "dies"], + ["hora", "hores"], + ["minut", "minuts"], + ["segon", "segons"], + ["milisegon", "milisegons"], + "," + ), + ckb: language( + "ساڵ", + "مانگ", + "هەفتە", + "ڕۆژ", + "کاژێر", + "خولەک", + "چرکە", + "میلی چرکە", + "." + ), + cs: language( + function (c) { + return ["rok", "roku", "roky", "let"][getCzechOrSlovakForm(c)]; + }, + function (c) { + return ["měsíc", "měsíce", "měsíce", "měsíců"][ + getCzechOrSlovakForm(c) + ]; + }, + function (c) { + return ["týden", "týdne", "týdny", "týdnů"][getCzechOrSlovakForm(c)]; + }, + function (c) { + return ["den", "dne", "dny", "dní"][getCzechOrSlovakForm(c)]; + }, + function (c) { + return ["hodina", "hodiny", "hodiny", "hodin"][ + getCzechOrSlovakForm(c) + ]; + }, + function (c) { + return ["minuta", "minuty", "minuty", "minut"][ + getCzechOrSlovakForm(c) + ]; + }, + function (c) { + return ["sekunda", "sekundy", "sekundy", "sekund"][ + getCzechOrSlovakForm(c) + ]; + }, + function (c) { + return ["milisekunda", "milisekundy", "milisekundy", "milisekund"][ + getCzechOrSlovakForm(c) + ]; + }, + "," + ), + cy: language( + "flwyddyn", + "mis", + "wythnos", + "diwrnod", + "awr", + "munud", + "eiliad", + "milieiliad" + ), + da: onesLanguage( + ["år", "år"], + ["måned", "måneder"], + ["uge", "uger"], + ["dag", "dage"], + ["time", "timer"], + ["minut", "minutter"], + ["sekund", "sekunder"], + ["millisekund", "millisekunder"], + "," + ), + de: onesLanguage( + ["Jahr", "Jahre"], + ["Monat", "Monate"], + ["Woche", "Wochen"], + ["Tag", "Tage"], + ["Stunde", "Stunden"], + ["Minute", "Minuten"], + ["Sekunde", "Sekunden"], + ["Millisekunde", "Millisekunden"], + "," + ), + el: GREEK, + en: onesLanguage( + ["year", "years"], + ["month", "months"], + ["week", "weeks"], + ["day", "days"], + ["hour", "hours"], + ["minute", "minutes"], + ["second", "seconds"], + ["millisecond", "milliseconds"] + ), + eo: onesLanguage( + ["jaro", "jaroj"], + ["monato", "monatoj"], + ["semajno", "semajnoj"], + ["tago", "tagoj"], + ["horo", "horoj"], + ["minuto", "minutoj"], + ["sekundo", "sekundoj"], + ["milisekundo", "milisekundoj"], + "," + ), + es: onesLanguage( + ["año", "años"], + ["mes", "meses"], + ["semana", "semanas"], + ["día", "días"], + ["hora", "horas"], + ["minuto", "minutos"], + ["segundo", "segundos"], + ["milisegundo", "milisegundos"], + "," + ), + et: onesLanguage( + ["aasta", "aastat"], + ["kuu", "kuud"], + ["nädal", "nädalat"], + ["päev", "päeva"], + ["tund", "tundi"], + ["minut", "minutit"], + ["sekund", "sekundit"], + ["millisekund", "millisekundit"], + "," + ), + eu: language( + "urte", + "hilabete", + "aste", + "egun", + "ordu", + "minutu", + "segundo", + "milisegundo", + "," + ), + fa: language( + "سال", + "ماه", + "هفته", + "روز", + "ساعت", + "دقیقه", + "ثانیه", + "میلی ثانیه" + ), + fi: onesLanguage( + ["vuosi", "vuotta"], + ["kuukausi", "kuukautta"], + ["viikko", "viikkoa"], + ["päivä", "päivää"], + ["tunti", "tuntia"], + ["minuutti", "minuuttia"], + ["sekunti", "sekuntia"], + ["millisekunti", "millisekuntia"], + "," + ), + fo: onesLanguage( + ["ár", "ár"], + ["mánaður", "mánaðir"], + ["vika", "vikur"], + ["dagur", "dagar"], + ["tími", "tímar"], + ["minuttur", "minuttir"], + ["sekund", "sekund"], + ["millisekund", "millisekund"], + "," + ), + fr: language( + function (c) { + return "an" + (c >= 2 ? "s" : ""); + }, + "mois", + function (c) { + return "semaine" + (c >= 2 ? "s" : ""); + }, + function (c) { + return "jour" + (c >= 2 ? "s" : ""); + }, + function (c) { + return "heure" + (c >= 2 ? "s" : ""); + }, + function (c) { + return "minute" + (c >= 2 ? "s" : ""); + }, + function (c) { + return "seconde" + (c >= 2 ? "s" : ""); + }, + function (c) { + return "milliseconde" + (c >= 2 ? "s" : ""); + }, + "," + ), + gr: GREEK, + he: onesLanguage( + ["שנה", "שנים"], + ["חודש", "חודשים"], + ["שבוע", "שבועות"], + ["יום", "ימים"], + ["שעה", "שעות"], + ["דקה", "דקות"], + ["שניה", "שניות"], + ["מילישנייה", "מילישניות"] + ), + hr: language( + function (c) { + if (c % 10 === 2 || c % 10 === 3 || c % 10 === 4) { + return "godine"; + } + return "godina"; + }, + function (c) { + if (c === 1) { + return "mjesec"; + } else if (c === 2 || c === 3 || c === 4) { + return "mjeseca"; + } + return "mjeseci"; + }, + function (c) { + if (c % 10 === 1 && c !== 11) { + return "tjedan"; + } + return "tjedna"; + }, + onesUnit(["dan", "dana"]), + function (c) { + if (c === 1) { + return "sat"; + } else if (c === 2 || c === 3 || c === 4) { + return "sata"; + } + return "sati"; + }, + function (c) { + var mod10 = c % 10; + if ( + (mod10 === 2 || mod10 === 3 || mod10 === 4) && + (c < 10 || c > 14) + ) { + return "minute"; + } + return "minuta"; + }, + function (c) { + var mod10 = c % 10; + if (mod10 === 5 || (Math.floor(c) === c && c >= 10 && c <= 19)) { + return "sekundi"; + } else if (mod10 === 1) { + return "sekunda"; + } else if (mod10 === 2 || mod10 === 3 || mod10 === 4) { + return "sekunde"; + } + return "sekundi"; + }, + function (c) { + if (c === 1) { + return "milisekunda"; + } else if (c % 10 === 2 || c % 10 === 3 || c % 10 === 4) { + return "milisekunde"; + } + return "milisekundi"; + }, + "," + ), + hi: language( + "साल", + onesUnit(["महीना", "महीने"]), + onesUnit(["हफ़्ता", "हफ्ते"]), + "दिन", + onesUnit(["घंटा", "घंटे"]), + "मिनट", + "सेकंड", + "मिलीसेकंड" + ), + hu: language( + "év", + "hónap", + "hét", + "nap", + "óra", + "perc", + "másodperc", + "ezredmásodperc", + "," + ), + id: language( + "tahun", + "bulan", + "minggu", + "hari", + "jam", + "menit", + "detik", + "milidetik" + ), + is: onesLanguage( + ["ár", "ár"], + ["mánuður", "mánuðir"], + ["vika", "vikur"], + ["dagur", "dagar"], + ["klukkutími", "klukkutímar"], + ["mínúta", "mínútur"], + ["sekúnda", "sekúndur"], + ["millisekúnda", "millisekúndur"] + ), + it: onesLanguage( + ["anno", "anni"], + ["mese", "mesi"], + ["settimana", "settimane"], + ["giorno", "giorni"], + ["ora", "ore"], + ["minuto", "minuti"], + ["secondo", "secondi"], + ["millisecondo", "millisecondi"], + "," + ), + ja: language("年", "ヶ月", "週間", "日", "時間", "分", "秒", "ミリ秒"), + km: language( + "ឆ្នាំ", + "ខែ", + "សប្តាហ៍", + "ថ្ងៃ", + "ម៉ោង", + "នាទី", + "វិនាទី", + "មិល្លីវិនាទី" + ), + kn: onesLanguage( + ["ವರ್ಷ", "ವರ್ಷಗಳು"], + ["ತಿಂಗಳು", "ತಿಂಗಳುಗಳು"], + ["ವಾರ", "ವಾರಗಳು"], + ["ದಿನ", "ದಿನಗಳು"], + ["ಗಂಟೆ", "ಗಂಟೆಗಳು"], + ["ನಿಮಿಷ", "ನಿಮಿಷಗಳು"], + ["ಸೆಕೆಂಡ್", "ಸೆಕೆಂಡುಗಳು"], + ["ಮಿಲಿಸೆಕೆಂಡ್", "ಮಿಲಿಸೆಕೆಂಡುಗಳು"] + ), + ko: language("년", "개월", "주일", "일", "시간", "분", "초", "밀리 초"), + ku: language( + "sal", + "meh", + "hefte", + "roj", + "seet", + "deqe", + "saniye", + "mîlîçirk", + "," + ), + lo: language( + "ປີ", + "ເດືອນ", + "ອາທິດ", + "ມື້", + "ຊົ່ວໂມງ", + "ນາທີ", + "ວິນາທີ", + "ມິນລິວິນາທີ", + "," + ), + lt: language( + function (c) { + return c % 10 === 0 || (c % 100 >= 10 && c % 100 <= 20) + ? "metų" + : "metai"; + }, + function (c) { + return ["mėnuo", "mėnesiai", "mėnesių"][getLithuanianForm(c)]; + }, + function (c) { + return ["savaitė", "savaitės", "savaičių"][getLithuanianForm(c)]; + }, + function (c) { + return ["diena", "dienos", "dienų"][getLithuanianForm(c)]; + }, + function (c) { + return ["valanda", "valandos", "valandų"][getLithuanianForm(c)]; + }, + function (c) { + return ["minutė", "minutės", "minučių"][getLithuanianForm(c)]; + }, + function (c) { + return ["sekundė", "sekundės", "sekundžių"][getLithuanianForm(c)]; + }, + function (c) { + return ["milisekundė", "milisekundės", "milisekundžių"][ + getLithuanianForm(c) + ]; + }, + "," + ), + lv: language( + function (c) { + return getLatvianForm(c) ? "gads" : "gadi"; + }, + function (c) { + return getLatvianForm(c) ? "mēnesis" : "mēneši"; + }, + function (c) { + return getLatvianForm(c) ? "nedēļa" : "nedēļas"; + }, + function (c) { + return getLatvianForm(c) ? "diena" : "dienas"; + }, + function (c) { + return getLatvianForm(c) ? "stunda" : "stundas"; + }, + function (c) { + return getLatvianForm(c) ? "minūte" : "minūtes"; + }, + function (c) { + return getLatvianForm(c) ? "sekunde" : "sekundes"; + }, + function (c) { + return getLatvianForm(c) ? "milisekunde" : "milisekundes"; + }, + "," + ), + mk: onesLanguage( + ["година", "години"], + ["месец", "месеци"], + ["недела", "недели"], + ["ден", "дена"], + ["час", "часа"], + ["минута", "минути"], + ["секунда", "секунди"], + ["милисекунда", "милисекунди"], + "," + ), + mn: language( + "жил", + "сар", + "долоо хоног", + "өдөр", + "цаг", + "минут", + "секунд", + "миллисекунд" + ), + mr: language( + onesUnit(["वर्ष", "वर्षे"]), + onesUnit(["महिना", "महिने"]), + onesUnit(["आठवडा", "आठवडे"]), + "दिवस", + "तास", + onesUnit(["मिनिट", "मिनिटे"]), + "सेकंद", + "मिलिसेकंद" + ), + ms: language( + "tahun", + "bulan", + "minggu", + "hari", + "jam", + "minit", + "saat", + "milisaat" + ), + nl: onesLanguage( + ["jaar", "jaar"], + ["maand", "maanden"], + ["week", "weken"], + ["dag", "dagen"], + ["uur", "uur"], + ["minuut", "minuten"], + ["seconde", "seconden"], + ["milliseconde", "milliseconden"], + "," + ), + nn: onesLanguage( + ["år", "år"], + ["månad", "månader"], + ["veke", "veker"], + ["dag", "dagar"], + ["time", "timar"], + ["minutt", "minutt"], + ["sekund", "sekund"], + ["millisekund", "millisekund"], + "," + ), + no: onesLanguage( + ["år", "år"], + ["måned", "måneder"], + ["uke", "uker"], + ["dag", "dager"], + ["time", "timer"], + ["minutt", "minutter"], + ["sekund", "sekunder"], + ["millisekund", "millisekunder"], + "," + ), + pl: language( + function (c) { + return ["rok", "roku", "lata", "lat"][getPolishForm(c)]; + }, + function (c) { + return ["miesiąc", "miesiąca", "miesiące", "miesięcy"][ + getPolishForm(c) + ]; + }, + function (c) { + return ["tydzień", "tygodnia", "tygodnie", "tygodni"][ + getPolishForm(c) + ]; + }, + function (c) { + return ["dzień", "dnia", "dni", "dni"][getPolishForm(c)]; + }, + function (c) { + return ["godzina", "godziny", "godziny", "godzin"][getPolishForm(c)]; + }, + function (c) { + return ["minuta", "minuty", "minuty", "minut"][getPolishForm(c)]; + }, + function (c) { + return ["sekunda", "sekundy", "sekundy", "sekund"][getPolishForm(c)]; + }, + function (c) { + return ["milisekunda", "milisekundy", "milisekundy", "milisekund"][ + getPolishForm(c) + ]; + }, + "," + ), + pt: onesLanguage( + ["ano", "anos"], + ["mês", "meses"], + ["semana", "semanas"], + ["dia", "dias"], + ["hora", "horas"], + ["minuto", "minutos"], + ["segundo", "segundos"], + ["milissegundo", "milissegundos"], + "," + ), + ro: language( + romanianUnit("an", "ani", "de ani"), + romanianUnit("lună", "luni", "de luni"), + romanianUnit("săptămână", "săptămâni", "de săptămâni"), + romanianUnit("zi", "zile", "de zile"), + romanianUnit("oră", "ore", "de ore"), + romanianUnit("minut", "minute", "de minute"), + romanianUnit("secundă", "secunde", "de secunde"), + romanianUnit("milisecundă", "milisecunde", "de milisecunde"), + "," + ), + ru: slavicLanguage( + ["лет", "год", "года"], + ["месяцев", "месяц", "месяца"], + ["недель", "неделя", "недели"], + ["дней", "день", "дня"], + ["часов", "час", "часа"], + ["минут", "минута", "минуты"], + ["секунд", "секунда", "секунды"], + ["миллисекунд", "миллисекунда", "миллисекунды"] + ), + sq: language( + onesUnit(["vit", "vjet"]), + "muaj", + "javë", + "ditë", + "orë", + function (c) { + return "minut" + (c === 1 ? "ë" : "a"); + }, + function (c) { + return "sekond" + (c === 1 ? "ë" : "a"); + }, + function (c) { + return "milisekond" + (c === 1 ? "ë" : "a"); + }, + "," + ), + sr: slavicLanguage( + ["години", "година", "године"], + ["месеци", "месец", "месеца"], + ["недељи", "недеља", "недеље"], + ["дани", "дан", "дана"], + ["сати", "сат", "сата"], + ["минута", "минут", "минута"], + ["секунди", "секунда", "секунде"], + ["милисекунди", "милисекунда", "милисекунде"] + ), + sr_Latn: slavicLanguage( + ["godini", "godina", "godine"], + ["meseci", "mesec", "meseca"], + ["nedelji", "nedelja", "nedelje"], + ["dani", "dan", "dana"], + ["sati", "sat", "sata"], + ["minuta", "minut", "minuta"], + ["sekundi", "sekunda", "sekunde"], + ["milisekundi", "milisekunda", "milisekunde"] + ), + ta: onesLanguage( + ["வருடம்", "ஆண்டுகள்"], + ["மாதம்", "மாதங்கள்"], + ["வாரம்", "வாரங்கள்"], + ["நாள்", "நாட்கள்"], + ["மணி", "மணிநேரம்"], + ["நிமிடம்", "நிமிடங்கள்"], + ["வினாடி", "வினாடிகள்"], + ["மில்லி விநாடி", "மில்லி விநாடிகள்"] + ), + te: onesLanguage( + ["సంవత్సరం", "సంవత్సరాల"], + ["నెల", "నెలల"], + ["వారం", "వారాలు"], + ["రోజు", "రోజులు"], + ["గంట", "గంటలు"], + ["నిమిషం", "నిమిషాలు"], + ["సెకను", "సెకన్లు"], + ["మిల్లీసెకన్", "మిల్లీసెకన్లు"] + ), + uk: slavicLanguage( + ["років", "рік", "роки"], + ["місяців", "місяць", "місяці"], + ["тижнів", "тиждень", "тижні"], + ["днів", "день", "дні"], + ["годин", "година", "години"], + ["хвилин", "хвилина", "хвилини"], + ["секунд", "секунда", "секунди"], + ["мілісекунд", "мілісекунда", "мілісекунди"] + ), + ur: language( + "سال", + onesUnit(["مہینہ", "مہینے"]), + onesUnit(["ہفتہ", "ہفتے"]), + "دن", + onesUnit(["گھنٹہ", "گھنٹے"]), + "منٹ", + "سیکنڈ", + "ملی سیکنڈ" + ), + sk: language( + function (c) { + return ["rok", "roky", "roky", "rokov"][getCzechOrSlovakForm(c)]; + }, + function (c) { + return ["mesiac", "mesiace", "mesiace", "mesiacov"][ + getCzechOrSlovakForm(c) + ]; + }, + function (c) { + return ["týždeň", "týždne", "týždne", "týždňov"][ + getCzechOrSlovakForm(c) + ]; + }, + function (c) { + return ["deň", "dni", "dni", "dní"][getCzechOrSlovakForm(c)]; + }, + function (c) { + return ["hodina", "hodiny", "hodiny", "hodín"][ + getCzechOrSlovakForm(c) + ]; + }, + function (c) { + return ["minúta", "minúty", "minúty", "minút"][ + getCzechOrSlovakForm(c) + ]; + }, + function (c) { + return ["sekunda", "sekundy", "sekundy", "sekúnd"][ + getCzechOrSlovakForm(c) + ]; + }, + function (c) { + return ["milisekunda", "milisekundy", "milisekundy", "milisekúnd"][ + getCzechOrSlovakForm(c) + ]; + }, + "," + ), + sl: language( + function (c) { + if (c % 10 === 1) { + return "leto"; + } else if (c % 100 === 2) { + return "leti"; + } else if ( + c % 100 === 3 || + c % 100 === 4 || + (Math.floor(c) !== c && c % 100 <= 5) + ) { + return "leta"; + } else { + return "let"; + } + }, + function (c) { + if (c % 10 === 1) { + return "mesec"; + } else if (c % 100 === 2 || (Math.floor(c) !== c && c % 100 <= 5)) { + return "meseca"; + } else if (c % 10 === 3 || c % 10 === 4) { + return "mesece"; + } else { + return "mesecev"; + } + }, + function (c) { + if (c % 10 === 1) { + return "teden"; + } else if (c % 10 === 2 || (Math.floor(c) !== c && c % 100 <= 4)) { + return "tedna"; + } else if (c % 10 === 3 || c % 10 === 4) { + return "tedne"; + } else { + return "tednov"; + } + }, + function (c) { + return c % 100 === 1 ? "dan" : "dni"; + }, + function (c) { + if (c % 10 === 1) { + return "ura"; + } else if (c % 100 === 2) { + return "uri"; + } else if (c % 10 === 3 || c % 10 === 4 || Math.floor(c) !== c) { + return "ure"; + } else { + return "ur"; + } + }, + function (c) { + if (c % 10 === 1) { + return "minuta"; + } else if (c % 10 === 2) { + return "minuti"; + } else if ( + c % 10 === 3 || + c % 10 === 4 || + (Math.floor(c) !== c && c % 100 <= 4) + ) { + return "minute"; + } else { + return "minut"; + } + }, + function (c) { + if (c % 10 === 1) { + return "sekunda"; + } else if (c % 100 === 2) { + return "sekundi"; + } else if (c % 100 === 3 || c % 100 === 4 || Math.floor(c) !== c) { + return "sekunde"; + } else { + return "sekund"; + } + }, + function (c) { + if (c % 10 === 1) { + return "milisekunda"; + } else if (c % 100 === 2) { + return "milisekundi"; + } else if (c % 100 === 3 || c % 100 === 4 || Math.floor(c) !== c) { + return "milisekunde"; + } else { + return "milisekund"; + } + }, + "," + ), + sv: onesLanguage( + ["år", "år"], + ["månad", "månader"], + ["vecka", "veckor"], + ["dag", "dagar"], + ["timme", "timmar"], + ["minut", "minuter"], + ["sekund", "sekunder"], + ["millisekund", "millisekunder"], + "," + ), + sw: assign( + onesLanguage( + ["mwaka", "miaka"], + ["mwezi", "miezi"], + ["wiki", "wiki"], + ["siku", "masiku"], + ["saa", "masaa"], + ["dakika", "dakika"], + ["sekunde", "sekunde"], + ["milisekunde", "milisekunde"] + ), + { _numberFirst: true } + ), + tr: language( + "yıl", + "ay", + "hafta", + "gün", + "saat", + "dakika", + "saniye", + "milisaniye", + "," + ), + th: language( + "ปี", + "เดือน", + "สัปดาห์", + "วัน", + "ชั่วโมง", + "นาที", + "วินาที", + "มิลลิวินาที" + ), + uz: language( + "yil", + "oy", + "hafta", + "kun", + "soat", + "minut", + "sekund", + "millisekund" + ), + uz_CYR: language( + "йил", + "ой", + "ҳафта", + "кун", + "соат", + "минут", + "секунд", + "миллисекунд" + ), + vi: language( + "năm", + "tháng", + "tuần", + "ngày", + "giờ", + "phút", + "giây", + "mili giây", + "," + ), + zh_CN: language("年", "个月", "周", "天", "小时", "分钟", "秒", "毫秒"), + zh_TW: language("年", "個月", "周", "天", "小時", "分鐘", "秒", "毫秒") + }; + + /** + * Helper function for creating language definitions. + * + * @internal + * @param {Unit} y + * @param {Unit} mo + * @param {Unit} w + * @param {Unit} d + * @param {Unit} h + * @param {Unit} m + * @param {Unit} s + * @param {Unit} ms + * @param {string} [decimal] + * @returns {Language} + */ + function language(y, mo, w, d, h, m, s, ms, decimal) { + /** @type {Language} */ + var result = { y: y, mo: mo, w: w, d: d, h: h, m: m, s: s, ms: ms }; + if (decimal) { + result.decimal = decimal; + } + return result; + } + + /** + * @internal + * @param {[string, string]} unit + * @returns {(c: number) => string} + */ + function onesUnit(unit) { + return function (c) { + return c === 1 ? unit[0] : unit[1]; + }; + } + + /** + * Helper for generating languages where the word varies if the count is 1. + * + * @internal + * @param {[string, string]} y + * @param {[string, string]} mo + * @param {[string, string]} w + * @param {[string, string]} d + * @param {[string, string]} h + * @param {[string, string]} m + * @param {[string, string]} s + * @param {[string, string]} ms + * @param {string} [decimal] + * @returns {Language} + */ + function onesLanguage(y, mo, w, d, h, m, s, ms, decimal) { + return language( + onesUnit(y), + onesUnit(mo), + onesUnit(w), + onesUnit(d), + onesUnit(h), + onesUnit(m), + onesUnit(s), + onesUnit(ms), + decimal + ); + } + + /** + * Romanian uses "de" before the noun for numbers >= 20 (when not ending in 01-19). + * See: https://en.wikipedia.org/wiki/Romanian_numbers#Preposition_de + * + * @internal + * @param {string} single + * @param {string} plural + * @param {string} pluralWithDe + * @returns {(c: number) => string} + */ + function romanianUnit(single, plural, pluralWithDe) { + return function (c) { + if (c === 1) { + return single; + } + if (Math.floor(c) !== c || c === 0) { + return plural; + } + var remainder = c % 100; + if (remainder >= 1 && remainder <= 19) { + return plural; + } + return pluralWithDe; + }; + } + + /** + * @internal + * @param {[string, string, string]} unit + * @returns {(c: number) => string} + */ + function slavicUnit(unit) { + return function (c) { + if (Math.floor(c) !== c) { + return unit[2]; + } + if ( + (c % 100 >= 5 && c % 100 <= 20) || + (c % 10 >= 5 && c % 10 <= 9) || + c % 10 === 0 + ) { + return unit[0]; + } + if (c % 10 === 1) { + return unit[1]; + } + if (c > 1) { + return unit[2]; + } + return unit[1]; + }; + } + + /** + * Helper for generating Slavic languages. + * + * @internal + * @param {[string, string, string]} y + * @param {[string, string, string]} mo + * @param {[string, string, string]} w + * @param {[string, string, string]} d + * @param {[string, string, string]} h + * @param {[string, string, string]} m + * @param {[string, string, string]} s + * @param {[string, string, string]} ms + * @returns {Language} + */ + function slavicLanguage(y, mo, w, d, h, m, s, ms) { + return language( + slavicUnit(y), + slavicUnit(mo), + slavicUnit(w), + slavicUnit(d), + slavicUnit(h), + slavicUnit(m), + slavicUnit(s), + slavicUnit(ms), + "," + ); + } + + /** + * Helper function for Arabic. + * + * @internal + * @param {number} c + * @returns {0 | 1 | 2} + */ + function getArabicForm(c) { + if (c === 2) { + return 1; + } + if (c > 2 && c < 11) { + return 2; + } + return 0; + } + + /** + * Helper function for Polish. + * + * @internal + * @param {number} c + * @returns {0 | 1 | 2 | 3} + */ + function getPolishForm(c) { + if (c === 1) { + return 0; + } + if (Math.floor(c) !== c) { + return 1; + } + if (c % 10 >= 2 && c % 10 <= 4 && !(c % 100 > 10 && c % 100 < 20)) { + return 2; + } + return 3; + } + + /** + * Helper function for Czech or Slovak. + * + * @internal + * @param {number} c + * @returns {0 | 1 | 2 | 3} + */ + function getCzechOrSlovakForm(c) { + if (c === 1) { + return 0; + } + if (Math.floor(c) !== c) { + return 1; + } + if (c % 10 >= 2 && c % 10 <= 4 && c % 100 < 10) { + return 2; + } + return 3; + } + + /** + * Helper function for Lithuanian. + * + * @internal + * @param {number} c + * @returns {0 | 1 | 2} + */ + function getLithuanianForm(c) { + if (c % 10 === 1 && c % 100 !== 11) { + return 0; + } + if ( + Math.floor(c) !== c || + (c % 10 >= 2 && c % 100 > 20) || + (c % 10 >= 2 && c % 100 < 10) + ) { + return 1; + } + return 2; + } + + /** + * Helper function for Latvian. + * + * @internal + * @param {number} c + * @returns {boolean} + */ + function getLatvianForm(c) { + return c % 10 === 1 && c % 100 !== 11; + } + + /** + * @internal + * @template T + * @param {T} obj + * @param {keyof T} key + * @returns {boolean} + */ + function has(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); + } + + /** + * @internal + * @param {Pick} options + * @throws {Error} Throws an error if language is not found. + * @returns {Language} + */ + function getLanguage(options) { + var possibleLanguages = [options.language]; + + if (has(options, "fallbacks")) { + if (isArray(options.fallbacks) && options.fallbacks.length) { + possibleLanguages = possibleLanguages.concat(options.fallbacks); + } else { + throw new Error( + "fallbacks must be an array with at least one element" + ); + } + } + + for (var i = 0; i < possibleLanguages.length; i++) { + var languageToTry = possibleLanguages[i]; + if (has(options.languages, languageToTry)) { + return options.languages[languageToTry]; + } + if (has(LANGUAGES, languageToTry)) { + return LANGUAGES[languageToTry]; + } + } + + throw new Error("No language found."); + } + + /** + * @internal + * @param {Piece} piece + * @param {Language} language + * @param {Pick} options + */ + function renderPiece(piece, language, options) { + var unitName = piece.unitName; + var unitCount = piece.unitCount; + + var spacer = options.spacer; + var maxDecimalPoints = options.maxDecimalPoints; + + /** @type {string} */ + var decimal; + if (typeof options.decimal === "string") { + decimal = options.decimal; + } else if (typeof language.decimal === "string") { + decimal = language.decimal; + } else { + decimal = "."; + } + + /** @type {undefined | DigitReplacements} */ + var digitReplacements; + if ("digitReplacements" in options) { + digitReplacements = options.digitReplacements; + } else if ("_digitReplacements" in language) { + digitReplacements = language._digitReplacements; + } + + /** @type {string} */ + var formattedCount; + var normalizedUnitCount = + maxDecimalPoints === void 0 + ? unitCount + : Math.floor(unitCount * Math.pow(10, maxDecimalPoints)) / + Math.pow(10, maxDecimalPoints); + var countStr = normalizedUnitCount.toString(); + + if (language._hideCountIf2 && unitCount === 2) { + formattedCount = ""; + spacer = ""; + } else if (digitReplacements) { + formattedCount = ""; + for (var i = 0; i < countStr.length; i++) { + var character = countStr[i]; + if (character === ".") { + formattedCount += decimal; + } else { + var asNumber = Number(character); + var digitReplacement = digitReplacements[asNumber]; + formattedCount += + digitReplacement === void 0 ? character : digitReplacement; + } + } + } else { + formattedCount = countStr.replace(".", decimal); + } + + var languageWord = language[unitName]; + var word; + if (typeof languageWord === "function") { + word = languageWord(unitCount); + } else { + word = languageWord; + } + + if (language._numberFirst) { + return word + spacer + formattedCount; + } + return formattedCount + spacer + word; + } + + /** + * @internal + * @typedef {Object} Piece + * @prop {UnitName} unitName + * @prop {number} unitCount + */ + + /** + * @internal + * @param {number} ms + * @param {Pick} options + * @returns {Piece[]} + */ + function getPieces(ms, options) { + /** @type {UnitName} */ + var unitName; + + /** @type {number} */ + var i; + + /** @type {number} */ + var unitCount; + + /** @type {number} */ + var msRemaining; + + var units = options.units; + var unitMeasures = options.unitMeasures; + var largest = + typeof options.largest === "number" ? options.largest : Infinity; + + if (!units.length) return []; + + // Get the counts for each unit. Doesn't round or truncate anything. + // For example, might create an object like `{ y: 7, m: 6, w: 0, d: 5, h: 23.99 }`. + /** @type {Partial>} */ + var unitCounts = {}; + msRemaining = ms; + for (i = 0; i < units.length; i++) { + unitName = units[i]; + var unitMs = unitMeasures[unitName]; + + var isLast = i === units.length - 1; + unitCount = isLast + ? msRemaining / unitMs + : Math.floor(msRemaining / unitMs); + unitCounts[unitName] = unitCount; + + msRemaining -= unitCount * unitMs; + } + + if (options.round) { + // Update counts based on the `largest` option. + // For example, if `largest === 2` and `unitCount` is `{ y: 7, m: 6, w: 0, d: 5, h: 23.99 }`, + // updates to something like `{ y: 7, m: 6.2 }`. + var unitsRemainingBeforeRound = largest; + for (i = 0; i < units.length; i++) { + unitName = units[i]; + unitCount = /** @type {number} */ (unitCounts[unitName]); + + if (unitCount === 0) continue; + + unitsRemainingBeforeRound--; + + // "Take" the rest of the units into this one. + if (unitsRemainingBeforeRound === 0) { + for (var j = i + 1; j < units.length; j++) { + var smallerUnitName = units[j]; + var smallerUnitCount = /** @type {number} */ ( + unitCounts[smallerUnitName] + ); + unitCounts[unitName] = + /** @type {number} */ (unitCounts[unitName]) + + (smallerUnitCount * unitMeasures[smallerUnitName]) / + unitMeasures[unitName]; + unitCounts[smallerUnitName] = 0; + } + break; + } + } + + // Round the last piece (which should be the only non-integer). + // + // This can be a little tricky if the last piece "bubbles up" to a larger + // unit. For example, "3 days, 23.99 hours" should be rounded to "4 days". + // It can also require multiple passes. For example, "6 days, 23.99 hours" + // should become "1 week". + for (i = units.length - 1; i >= 0; i--) { + unitName = units[i]; + unitCount = /** @type {number} */ (unitCounts[unitName]); + + if (unitCount === 0) continue; + + var rounded = Math.round(unitCount); + unitCounts[unitName] = rounded; + + if (i === 0) break; + + var previousUnitName = units[i - 1]; + var previousUnitMs = unitMeasures[previousUnitName]; + var amountOfPreviousUnit = Math.floor( + (rounded * unitMeasures[unitName]) / previousUnitMs + ); + if (amountOfPreviousUnit) { + unitCounts[previousUnitName] = + /** @type {number} */ (unitCounts[previousUnitName]) + + amountOfPreviousUnit; + unitCounts[unitName] = 0; + } else { + break; + } + } + } + + /** @type {Piece[]} */ + var result = []; + for (i = 0; i < units.length && result.length < largest; i++) { + unitName = units[i]; + unitCount = /** @type {number} */ (unitCounts[unitName]); + if (unitCount) { + result.push({ unitName: unitName, unitCount: unitCount }); + } + } + return result; + } + + /** + * @internal + * @param {Piece[]} pieces + * @param {Pick} options + * @returns {string} + */ + function formatPieces(pieces, options) { + var language = getLanguage(options); + + if (!pieces.length) { + var units = options.units; + var smallestUnitName = units[units.length - 1]; + return renderPiece( + { unitName: smallestUnitName, unitCount: 0 }, + language, + options + ); + } + + var conjunction = options.conjunction; + var serialComma = options.serialComma; + + var delimiter; + if (has(options, "delimiter")) { + delimiter = options.delimiter; + } else if (has(language, "delimiter")) { + delimiter = language.delimiter; + } else { + delimiter = ", "; + } + + /** @type {string[]} */ + var renderedPieces = []; + for (var i = 0; i < pieces.length; i++) { + renderedPieces.push(renderPiece(pieces[i], language, options)); + } + + if (!conjunction || pieces.length === 1) { + return renderedPieces.join(delimiter); + } + + if (pieces.length === 2) { + return renderedPieces.join(conjunction); + } + + return ( + renderedPieces.slice(0, -1).join(delimiter) + + (serialComma ? "," : "") + + conjunction + + renderedPieces.slice(-1) + ); + } + + /** + * Create a humanizer, which lets you change the default options. + * + * @param {Options} [passedOptions] Options to customize the humanizer + * @returns {Humanizer} A function that humanizes durations + */ + function humanizer(passedOptions) { + /** + * @param {number} ms + * @param {Options} [humanizerOptions] + * @returns {string} + */ + var result = /** @type {Humanizer} */ ( + function humanizer(ms, humanizerOptions) { + // Make sure we have a positive number. + // + // Has the nice side-effect of converting things to numbers. For example, + // converts `"123"` and `Number(123)` to `123`. + ms = Math.abs(ms); + + var options = /** @type {NormalizedOptions} */ ( + assign({}, result, humanizerOptions || {}) + ); + + var pieces = getPieces(ms, options); + + return formatPieces(pieces, options); + } + ); + + return assign( + result, + { + language: "en", + spacer: " ", + conjunction: "", + serialComma: true, + units: ["y", "mo", "w", "d", "h", "m", "s"], + languages: {}, + round: false, + unitMeasures: { + y: 31557600000, + mo: 2629800000, + w: 604800000, + d: 86400000, + h: 3600000, + m: 60000, + s: 1000, + ms: 1 + } + }, + passedOptions + ); + } + + /** + * Humanize a duration. + * + * This is a wrapper around the default humanizer. + */ + var humanizeDuration = assign(humanizer({}), { + /** + * Get a list of supported languages. + * + * @returns {string[]} An array of language codes + */ + getSupportedLanguages: function getSupportedLanguages() { + var result = []; + for (var language in LANGUAGES) { + if (has(LANGUAGES, language) && language !== "gr") { + result.push(language); + } + } + return result; + }, + humanizer: humanizer + }); + + // @ts-expect-error + if (typeof define === "function" && define.amd) { + // @ts-expect-error + define(function () { + return humanizeDuration; + }); + } else if (typeof module !== "undefined" && module.exports) { + module.exports = humanizeDuration; + } else { + global.humanizeDuration = humanizeDuration; + } + } +)(this); diff --git a/test/humanizer.js b/test/humanizer.js new file mode 100644 index 0000000..2d966ed --- /dev/null +++ b/test/humanizer.js @@ -0,0 +1,336 @@ +const humanizeDuration = require(".."); +const humanizer = humanizeDuration.humanizer; +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); +const ms = require("ms"); + +describe("humanizer", function () { + it("humanizes English when passed no arguments", function () { + const h = humanizer(); + + assert.strictEqual(h(1000), "1 second"); + }); + + it("humanizes English when passed an empty object", function () { + const h = humanizer({}); + + assert.strictEqual(h(1000), "1 second"); + }); + + it("can change the delimiter", function () { + const h = humanizer({ delimiter: "+" }); + + assert.strictEqual(h(0), "0 seconds"); + assert.strictEqual(h(1000), "1 second"); + assert.strictEqual(h(363000), "6 minutes+3 seconds"); + }); + + it("can change the spacer", function () { + const h = humanizer({ spacer: " whole " }); + + assert.strictEqual(h(0), "0 whole seconds"); + assert.strictEqual(h(1000), "1 whole second"); + assert.strictEqual(h(260040000), "3 whole days, 14 whole minutes"); + }); + + it("can use a conjunction", function () { + const h = humanizer({ conjunction: " and " }); + + assert.strictEqual(h(0), "0 seconds"); + assert.strictEqual(h(1000), "1 second"); + assert.strictEqual(h(260040000), "3 days and 14 minutes"); + assert.strictEqual(h(10874000), "3 hours, 1 minute, and 14 seconds"); + }); + + it("can use a conjunction without a serial comma", function () { + const h = humanizer({ + conjunction: " & ", + serialComma: false, + }); + + assert.strictEqual(h(1000), "1 second"); + assert.strictEqual(h(260040000), "3 days & 14 minutes"); + assert.strictEqual(h(10874000), "3 hours, 1 minute & 14 seconds"); + }); + + it("can change the units", function () { + const h = humanizer({ units: ["d"] }); + + assert.strictEqual(h(0), "0 days"); + assert.strictEqual(h(ms("6h")), "0.25 days"); + assert.strictEqual(h(ms("7d")), "7 days"); + }); + + it("can overwrite the unit measures in the initializer", function () { + const h = humanizer({ + unitMeasures: { + y: 10512000000, + mo: 864000000, + w: 144000000, + d: 28800000, + h: 3600000, + m: 60000, + s: 1000, + ms: 1, + }, + }); + + assert.strictEqual(h(1000), "1 second"); + assert.strictEqual(h(3600000), "1 hour"); + assert.strictEqual(h(28800000), "1 day"); + assert.strictEqual(h(144000000), "1 week"); + }); + + it("can change the decimal", function () { + const h = humanizer({ + units: ["s"], + decimal: "what", + }); + + assert.strictEqual(h(1234), "1what234 seconds"); + assert.strictEqual( + h(1234, { + decimal: "!!", + }), + "1!!234 seconds" + ); + }); + + it("can do simple rounding", function () { + const h = humanizer({ round: true }); + + assert.strictEqual(h(0), "0 seconds"); + assert.strictEqual(h(499), "0 seconds"); + assert.strictEqual(h(500), "1 second"); + assert.strictEqual(h(1000), "1 second"); + assert.strictEqual(h(1499), "1 second"); + assert.strictEqual(h(1500), "2 seconds"); + assert.strictEqual(h(1500), "2 seconds"); + assert.strictEqual(h(121499), "2 minutes, 1 second"); + assert.strictEqual(h(121500), "2 minutes, 2 seconds"); + }); + + it('can do rounding with the "units" option', function () { + const h = humanizer({ round: true }); + + assert.strictEqual( + h(86364000, { units: ["y", "mo", "w", "d", "h"] }), + "1 day" + ); + assert.strictEqual( + h(1209564000, { units: ["y", "mo", "w", "d", "h"] }), + "2 weeks" + ); + assert.strictEqual(h(3692131200000, { units: ["y", "mo"] }), "117 years"); + assert.strictEqual( + h(3692131200001, { units: ["y", "mo", "w", "d", "h", "m"] }), + "116 years, 11 months, 4 weeks, 1 day, 4 hours, 30 minutes" + ); + }); + + it('can do rounding with the "largest" option', function () { + const h = humanizer({ round: true }); + + assert.strictEqual(h(3692131200000, { largest: 1 }), "117 years"); + assert.strictEqual(h(3692131200000, { largest: 2 }), "117 years"); + assert.strictEqual( + h(3692131200001, { largest: 100 }), + "116 years, 11 months, 4 weeks, 1 day, 4 hours, 30 minutes" + ); + assert.strictEqual(h(2838550, { largest: 3 }), "47 minutes, 19 seconds"); + }); + + it('can do rounding with the "maxDecimalPoints" option', function () { + var h = humanizer({ maxDecimalPoints: 2 }); + + assert.strictEqual(h(8123.456789), "8.12 seconds"); + h.maxDecimalPoints = 3; + assert.strictEqual(h(8123.456789), "8.123 seconds"); + assert.strictEqual(h(8000), "8 seconds"); + + h.maxDecimalPoints = 6; + assert.strictEqual(h(8123.45), "8.12345 seconds"); + + h.maxDecimalPoints = 6; + assert.strictEqual(h(8000), "8 seconds"); + + h.maxDecimalPoints = 0; + assert.strictEqual(h(7123.456), "7 seconds"); + h.maxDecimalPoints = 2; + assert.strictEqual(h(7999), "7.99 seconds"); + h.maxDecimalPoints = 3; + assert.strictEqual(h(7999), "7.999 seconds"); + }); + + it("can ask for the largest units", function () { + const h = humanizer({ largest: 2 }); + + assert.strictEqual(h(0), "0 seconds"); + assert.strictEqual(h(1000), "1 second"); + assert.strictEqual(h(2000), "2 seconds"); + assert.strictEqual(h(540360012), "6 days, 6 hours"); + assert.strictEqual( + h(540360012, { largest: 3 }), + "6 days, 6 hours, 6 minutes" + ); + assert.strictEqual( + h(540360012, { largest: 100 }), + "6 days, 6 hours, 6 minutes, 0.012 seconds" + ); + }); + + it("has properties which can be modified", function () { + const h = humanizer(); + + assert.strictEqual(h(363000), "6 minutes, 3 seconds"); + + h.delimiter = "+"; + assert.strictEqual(h(363000), "6 minutes+3 seconds"); + + h.language = "es"; + assert.strictEqual(h(363000), "6 minutos+3 segundos"); + + h.units = ["m"]; + assert.strictEqual(h(363000), "6,05 minutos"); + }); + + it("is a named function", function () { + assert.strictEqual(humanizer().name, "humanizer"); + }); + + it("can add a new language", function () { + const h = humanizer({ language: "cool language" }); + h.languages["cool language"] = { + y: () => "y", + mo: () => "mo", + w: () => "w", + d: () => "d", + h: () => "h", + m: () => "m", + s: () => "s", + ms: () => "ms", + delimiter: "--", + }; + + assert.strictEqual(h(1000), "1 s"); + assert.strictEqual(h(61000), "1 m--1 s"); + + assert.strictEqual(h(61000, { delimiter: "&&" }), "1 m&&1 s"); + + assert.strictEqual( + h(1000, { + language: "es", + }), + "1 segundo" + ); + + const anotherH = humanizer({ + language: "cool language", + }); + assert.throws(() => { + anotherH(1000); + }, Error); + }); + + it("can overwrite an existing language", function () { + const h = humanizer({ language: "en" }); + + assert.strictEqual(h(1000), "1 second"); + + h.languages.en = { + y: () => "y", + mo: () => "mo", + w: () => "w", + d: () => "d", + h: () => "h", + m: () => "m", + s: () => "s", + ms: () => "ms", + }; + + assert.strictEqual(h(1000), "1 s"); + assert.strictEqual(h(15600000), "4 h, 20 m"); + + const anotherH = humanizer({ language: "en" }); + + assert.strictEqual(anotherH(1000), "1 second"); + }); + + it("can overwrite the languages property in the initializer", function () { + const h = humanizer({ + languages: { + en: { + y: () => "y", + mo: () => "mo", + w: () => "w", + d: () => "d", + h: () => "h", + m: () => "m", + s: () => "s", + ms: () => "ms", + decimal: "!", + }, + }, + }); + + assert.strictEqual(h(1000), "1 s"); + assert.strictEqual(h(15600000), "4 h, 20 m"); + assert.strictEqual(h(1000, { language: "es" }), "1 segundo"); + assert.strictEqual(h(71750), "1 m, 11!75 s"); + }); + + it('uses "." as a fallback for a missing decimal', function () { + const h = humanizer({ + languages: { + en: { + y: () => "y", + mo: () => "mo", + w: () => "w", + d: () => "d", + h: () => "h", + m: () => "m", + s: () => "s", + ms: () => "ms", + }, + }, + }); + + assert.strictEqual(h(71750), "1 m, 11.75 s"); + assert.strictEqual(h(71750, { decimal: "!" }), "1 m, 11!75 s"); + }); + + it("accepts fallback languages", function () { + const h = humanizer(); + + assert.strictEqual( + h(10000, { language: "es", fallbacks: ["en"] }), + "10 segundos" + ); + assert.strictEqual( + h(10000, { language: "BAD", fallbacks: ["BAD", "es"] }), + "10 segundos" + ); + assert.strictEqual( + h(10000, { language: "BAD", fallbacks: ["es", "fr"] }), + "10 segundos" + ); + }); + + it("can replace digits", function () { + const h = humanizer({ + digitReplacements: [ + "Zero", + "One", + "Two", + "Three", + "UNUSED", + "UNUSED", + "UNUSED", + "UNUSED", + "UNUSED", + "UNUSED", + ], + }); + assert.strictEqual(h(123), "Zero.OneTwoThree seconds"); + }); +});