{# set phrases to be used in the relative_time_period macro one list item per language, each time fraction contains a list with the singular, plural and abbriviated phrase combine contains the text to combine the last time fraction, and error the text to display on wrong date input #} {%- set _time_period_phrases = [ { 'language': 'en', 'phrases': { 'year': ['year', 'years', 'yr'], 'month': ['month', 'months', 'mth'], 'week': ['week', 'weeks', 'wk'], 'day': ['day', 'days', 'day'], 'hour': ['hour', 'hours', 'hr'], 'minute': ['minute', 'minutes', 'min'], 'second': ['second', 'seconds', 'sec'], 'millisecond': ['millisecond', 'milliseconds', 'ms'], 'combine': 'and', 'error': 'Invalid date', } }, { 'language': 'pl', 'phrases': { 'year': ['rok', 'lat', 'r'], 'month': ['miesiąc', 'miesięcy', 'msc'], 'week': ['tydzień', 'tygodni', 'tyg'], 'day': ['dzień', 'dni', 'dzień'], 'hour': ['godzina', 'godzin', 'godz'], 'minute': ['minuta', 'minut', 'min'], 'second': ['sekunda', 'sekund', 'sek'], 'millisecond': ['milisekunda', 'milisekund', 'ms'], 'combine': 'i', 'error': 'Niepoprawna data', } }, { 'language': 'fr', 'phrases': { 'year': ['année', 'années', 'an'], 'month': ['mois', 'mois', 'mois'], 'week': ['semaine', 'semaines', 'sem'], 'day': ['jour', 'jours', 'j'], 'hour': ['heure', 'heures', 'h'], 'minute': ['minute', 'minutes', 'min'], 'second': ['seconde', 'secondes', 'sec'], 'millisecond': ['milliseconde', 'millisecondes', 'ms'], 'combine': 'et', 'error': 'Date non valide', } }, { 'language': 'it', 'phrases': { 'year': ['anno', 'anni', 'aa'], 'month': ['mese', 'mesi', 'mm'], 'week': ['settimana', 'settimane', 'set'], 'day': ['giorno', 'giorni', 'gg'], 'hour': ['ora', 'ore', 'h'], 'minute': ['minuto', 'minuti', 'min'], 'second': ['secondo', 'secondi', 'sec'], 'millisecond': ['millisecondo', 'millisecondi', 'ms'], 'combine': 'e', 'error': 'Data non valida', } }, { 'language': 'nb', 'phrases': { 'year': ['år', 'år', 'år'], 'month': ['måned', 'måneder', 'mnd'], 'week': ['uke', 'uker', 'u'], 'day': ['dag', 'dager', 'd'], 'hour': ['time', 'timer', 't'], 'minute': ['minutt', 'minutter', 'min'], 'second': ['sekund', 'sekunder', 'sek'], 'millisecond': ['millisekund', 'millisekunder', 'ms'], 'combine': 'og', 'error': 'Ugyldig dato', } }, { 'language': 'nl', 'phrases': { 'year': ['jaar', 'jaar', 'jr'], 'month': ['maand', 'maanden', 'mnd'], 'week': ['week', 'weken', 'wk'], 'day': ['dag', 'dagen', 'dg'], 'hour': ['uur', 'uur', 'u'], 'minute': ['minuut', 'minuten', 'min'], 'second': ['seconde', 'seconden', 'sec'], 'millisecond': ['milliseconde', 'milliseconden', 'ms'], 'combine': 'en', 'error': 'Ongeldige datum', } }, { 'language': 'nn', 'phrases': { 'year': ['år', 'år', 'år'], 'month': ['månad', 'månader', 'mnd'], 'week': ['veke', 'veker', 'v'], 'day': ['dag', 'dagar', 'd'], 'hour': ['time', 'timar', 't'], 'minute': ['minutt', 'minutt', 'min'], 'second': ['sekund', 'sekund', 'sek'], 'millisecond': ['millisekund', 'millisekund', 'ms'], 'combine': 'og', 'error': 'Ugyldig dato', } }, { 'language': 'de', 'phrases': { 'year': ['Jahr', 'Jahre', 'J.'], 'month': ['Monat', 'Monate', 'M.'], 'week': ['Woche', 'Wochen', 'Wo.'], 'day': ['Tag', 'Tage', 'Tg.'], 'hour': ['Stunde', 'Stunden', 'Std.'], 'minute': ['Minute', 'Minuten', 'Min.'], 'second': ['Sekunde', 'Sekunden', 'Sek.'], 'millisecond': ['Millisekunde', 'Millisekunden', 'ms'], 'combine': 'und', 'error': 'Falsches Datum', } }, { 'language': 'pt', 'phrases': { 'year': ['ano', 'anos', 'aa'], 'month': ['mês', 'meses', 'mm'], 'week': ['semana', 'semanas', 'sem'], 'day': ['dia', 'dias', 'd'], 'hour': ['hora', 'horas', 'h'], 'minute': ['minuto', 'minutos', 'min'], 'second': ['segundo', 'segundos', 'seg'], 'millisecond': ['millissegundo', 'millissegundos', 'ms'], 'combine': 'e', 'error': 'Data Inválida', } }, { 'language': 'dk', 'phrases': { 'year': ['år', 'år', 'år'], 'month': ['måned', 'måneder', 'mnd'], 'week': ['uge', 'uger', 'uge'], 'day': ['dag', 'dage', 'dag'], 'hour': ['time', 'timer', 't.'], 'minute': ['minut', 'minuter', 'min.'], 'second': ['sekund', 'sekunder', 'sek.'], 'millisecond': ['millisekund', 'millisekunder', 'ms.'], 'combine': 'og', 'error': 'Ugyldig dato', } }, { 'language': 'sv', 'phrases': { 'year': ['år', 'år', 'år'], 'month': ['månad', 'månader', 'mån'], 'week': ['vecka', 'veckor', 'v'], 'day': ['dag', 'dagar', 'dag'], 'hour': ['timme', 'timmar', 'tim'], 'minute': ['minut', 'minuter', 'min'], 'second': ['sekund', 'sekunder', 'sek'], 'millisecond': ['millisekund', 'millisekunder', 'ms'], 'combine': 'och', 'error': 'Ogiltigt datum', } }, { 'language': 'cs', 'phrases': { 'year': ['rok', 'roky', 'rok'], 'month': ['měsíc', 'měsíce', 'měs'], 'week': ['týden', 'týdny', 'týd'], 'day': ['den', 'dny', 'd'], 'hour': ['hodina', 'hodiny', 'hod'], 'minute': ['minuta', 'minuty', 'min'], 'second': ['sekunda', 'sekundy', 'sek'], 'millisecond': ['millisekunda', 'millisekundy', 'ms'], 'combine': 'a', 'error': 'špatný datum' } }, { 'language': 'fi', 'phrases': { 'year': ['vuosi', 'vuotta', 'v'], 'month': ['kuukausi', 'kuukautta', 'kk'], 'week': ['viikko', 'viikkoa', 'vk'], 'day': ['päivä', 'päivää', 'pv'], 'hour': ['tunti', 'tuntia', 't'], 'minute': ['minuutti', 'minuuttia', 'min'], 'second': ['sekunti', 'sekuntia', 's'], 'millisecond': ['millisekunti', 'millisekuntia', 'ms'], 'combine': 'ja', 'error': 'Väärä päivämäärä', } }, { 'language': 'ru', 'phrases': { 'year': ['год', 'года', 'г'], 'month': ['месяц', 'месяцы', 'м'], 'week': ['неделя', 'недели', 'н'], 'day': ['день', 'дни', 'д'], 'hour': ['час', 'часы', 'ч'], 'minute': ['минута', 'минут', 'м'], 'second': ['секунд', 'секунды', 'с'], 'millisecond': ['милисекунд', 'милисекунды', 'мс'], 'combine': 'и', 'error': 'Неверная дата', } }, { 'language': 'uk', 'phrases': { 'year': ['рік', 'років', 'р'], 'month': ['місяць', 'місяців', 'м'], 'week': ['тиждень', 'тижнів', 'тижд'], 'day': ['день', 'днів', 'дн'], 'hour': ['годину', 'годин', 'год'], 'minute': ['хвилину', 'хвилин', 'хв'], 'second': ['секунду', 'секунд', 'сек'], 'millisecond': ['мілісекунду', 'мілісекунд', 'мсек'], 'combine': 'та', 'error': 'Недійсна дата', } }, ] -%} {# macro to convert the abbreviated input for the not_use and always_show lists to the full time part names #} {%- macro _abbr_to_full(input) -%} {# determine not_use list #} {%- set abbr_to_full = dict(yr='year', mth='month', wk='week', day='day', hr='hour', min='minute', sec='second', ms='millisecond') -%} {%- set list = input if input is list else (input | replace(' ', '')).split(',') -%} {%- if list | select('in', abbr_to_full) | list | count > 0 -%} {%- set ns = namespace(output=[]) -%} {%- for i in list -%} {%- set ns.output = ns.output + [abbr_to_full[i] | default(i)] -%} {%- endfor -%} {%- set list = ns.output -%} {%- endif -%} {{- list | select('in', abbr_to_full.values()) | list | to_json -}} {%- endmacro -%} {# macro to split a timedelta in years, months, weeks, days, hours, minutes, seconds and milliseconds used by the relative time plus macro, set up as a seperate macro so it can be reused #} {%- macro time_split(date, parts=8, compare_date=now(), not_use=[], always_show=['all'], time=true, round_mode='floor') -%} {#- set defaults for input if not entered #} {%- set date = date if date is datetime else date | as_datetime('invalid') -%} {%- set compare_date = compare_date if compare_date is datetime else compare_date | as_datetime('invalid') -%} {%- set time = time | bool(true) -%} {%- set parts = [parts | int(1), always_show | count] | max -%} {# 1: check if date input is correct #} {%- if date is datetime and compare_date is datetime -%} {# convert date input to local or date only #} {%- set date = date | as_local if time else (date | as_local).date() -%} {%- set compare_date = compare_date | as_local if time else (compare_date | as_local).date() -%} {# determine highest and lowest date #} {%- set date_max = [compare_date, date] | max -%} {%- set date_min = [compare_date, date] | min -%} {#- set time periods in seconds #} {%- set days_year = 365 + (1 if (compare_date.replace(month=3, day=1) - timedelta(days=1)).day == 29 else 0) -%} {%- set days_month = ((compare_date.replace(day=20) + timedelta(days=20)).replace(day=1) - timedelta(days=1)).day -%} {%- set dur = dict( year=days_year * 86400000, month=days_month * 86400000, week=604800000, day=86400000, hour=3600000, minute=60000, second=1000, millisecond=1, ) -%} {# make sure input for not_use and always_show is correct #} {%- if not not_use in [['millisecond'], []] -%} {%- set not_use = _abbr_to_full(not_use) | from_json -%} {%- endif -%} {%- set not_use = not_use + ['hour', 'minute', 'second', 'millisecond' ] if not time else not_use -%} {%- if always_show in [['all'], 'all'] -%} {%- set always_show = dur.keys() | list -%} {%- elif always_show != [] -%} {%- set always_show = _abbr_to_full(always_show) | from_json | reject('in' , not_use) | list -%} {%- endif -%} {%- set always_show = always_show | reject('in', not_use) | list -%} {%- set do_use = dur.keys() | reject('in', not_use) | list -%} {# determine number of milliseconds #} {%- set ms = ((date_max - date_min).total_seconds() * 1000) | round(0, 'ceil')-%} {# 2: Check if the number of milliseconds is more then the duration of the last time part to output #} {%- if do_use and ms < dur[do_use|last] -%} {{- {do_use | last: (ms / dur[do_use|last]) | round(0, round_mode)} | to_json -}} {%- else -%} {# check if it is needed to determine years #} {%- if ms >= dur.day * 365 -%} {#- set numer of years, and set highest date using this number of years #} {%- set yrs = date_max.year - date_min.year - (1 if date_max.replace(year=date_min.year) < date_min else 0) -%} {%- set date_max = date_max.replace(year=date_max.year - yrs) -%} {%- set ms = (date_max - date_min).total_seconds() * 1000 -%} {%- set ms_yrs = ms -%} {%- endif -%} {%- set yrs = yrs | default(0) -%} {# check if it is needed to determine months #} {%- set check_mth = ms >= dur.day * 28 and 'month' in do_use -%} {%- if check_mth -%} {#- set numer of months, and set highest date using this number of months #} {%- set ns = namespace(dm=date_max, mth=0) -%} {%- set dmd = ns.dm.day -%} {%- for m in range(1, 12) -%} {%- set dmm, dmy = ns.dm.month, ns.dm.year -%} {%- set day_max = (ns.dm.replace(day=1) - timedelta(days=1)).day -%} {%- set dm = ns.dm.replace( month=12 if dmm == 1 else dmm - 1, year=dmy - 1 if dmm == 1 else dmy, day= [dmd, day_max] | min ) -%} {%- if dm < date_min -%} {%- break -%} {%- else -%} {%- set ns.mth = ns.mth + 1 -%} {%- set ns.dm = dm -%} {%- endif -%} {%- endfor -%} {%- set date_max, mth = ns.dm, ns.mth -%} {%- set mth = mth + yrs * 12 if 'year' in not_use else mth -%} {%- set ms = (date_max - date_min).total_seconds() * 1000 -%} {%- endif -%} {%- set mth = mth | default(0) -%} {# prepare for other time parts #} {%- set date_max = date_max.replace(year=date_max.year + yrs) if 'year' in not_use and 'month' in not_use else date_max -%} {#- set other time period variables #} {%- set ms = ((date_max - date_min).total_seconds() * 1000 + extra_days | default(0) * dur.day) | round(0) -%} {%- set wks = (ms // dur.week) | int -%} {%- set days = ((ms % dur.week // dur.day) + (wks * dur.week / dur.day if 'week' in not_use else 0)) | int -%} {%- set hrs = ((ms % dur.day // dur.hour) + (days * dur.day / dur.hour if 'day' in not_use else 0)) | int -%} {%- set min = ((ms % dur.hour // dur.minute) + (hrs * dur.hour / dur.minute if 'hour' in not_use else 0)) | int -%} {%- set sec = ((ms % dur.minute // dur.second) + (min * dur.minute / dur.second if 'minute' in not_use else 0)) | int -%} {%- set mis = ((ms % dur.second) | round + (sec * dur.second if 'second' in not_use else 0)) | int -%} {# prepare dict with ouput #} {%- set output = dict(year=yrs, month=mth | default(0), week=wks, day=days, hour=hrs, minute=min, second=sec, millisecond=mis) -%} {%- set output = dict(output.items() | selectattr('0', 'in', do_use)) -%} {%- set keys = output.keys() | list -%} {%- set with_value = output.items() | rejectattr('1', 'eq', 0) | map(attribute='0') | list -%} {%- set first = with_value | first | default('millisecond') -%} {%- set first = keys | select('in', always_show | default([], true) + [first]) | first -%} {%- set to_use = keys[keys.index(first):] -%} {%- set to_output = to_use[:parts] -%} {%- set last = to_output | last |default('millisecond') -%} {# 3: check if there is anything left to use #} {%- if to_use -%} {%- set to_output = to_use[:parts] -%} {%- set always_return = to_output | last -%} {# check if all values for always_show are included #} {%- set as_check = always_show | reject('in', to_output) | list | count -%} {%- if always_show and as_check > 0 -%} {%- set to_reject = to_use | reject('in', to_output) | reject('in', always_show) | list -%} {%- set not_use = not_use + to_output[as_check*-1:] + to_reject -%} {%- set to_output = to_output | reject('in', not_use) | list + always_show -%} {%- set to_output = keys | select('in', to_output) | list -%} {%- set output = time_split(date, parts, compare_date, not_use, always_show, time, round_mode) | from_json -%} {%- endif -%} {# apply round if needed #} {%- if round_mode in ['common', 'ceil'] and last != 'millisecond' -%} {# determine first and last item with data #} {%- set ms = ms_yrs if last == 'year' else ms -%} {%- set remain = ms % dur[last] -%} {%- set remain_part = remain / dur[last] -%} {%- set to_round = 1 if remain_part >= 0.5 and round_mode == 'common' else remain_part | round(0, round_mode) -%} {%- set sec_to_add = ((dur[last] + (dur.day if last in ['year', 'month'] else 1) - remain) | round(0, 'ceil') * to_round) / 1000 -%} {%- set round_mode = 'floor' -%} {%- set date_max = [compare_date, date] | max + timedelta(seconds=sec_to_add) -%} {%- set date_min = [compare_date, date] | min -%} {%- set output = time_split(date_max, parts, date_min, not_use, always_show, time, round_mode) | from_json -%} {%- endif -%} {# output result #} {%- set zero_values = output.items() | selectattr('1', 'eq', 0) | map(attribute='0') | list -%} {%- set reject_list = zero_values | reject('in', always_show) | list -%} {{- dict(output.items() | selectattr('0', 'in', to_output) | rejectattr('0', 'in', reject_list)) | default({always_return: 0}, true) | to_json -}} {%- else -%} {{- dict(error='No time parts left to output') | to_json -}} {%- endif -%} {# 3 #} {%- endif -%} {# 2 #} {%- else -%} {{- dict(error='Invalid date input') | to_json -}} {%- endif -%} {# 1 #} {%- endmacro -%} {# macro to output a timedelta in a readable format #} {%- macro relative_time_plus(date, parts=1, abbr=false, language='en', compare_date=now(), not_use=['millisecond'], always_show=[], time=true, round_mode='floor') -%} {#- select correct phrases bases on language input #} {%- set phrases = _time_period_phrases -%} {%- set languages = phrases | map(attribute='language') | list -%} {%- set language = iif(language in languages, language, 'en') -%} {%- set phr = phrases | selectattr('language', 'eq', language) | map(attribute='phrases') | list | first -%} {%- set abbr = abbr | bool(false) -%} {# split timedelta #} {%- set time_parts = time_split(date, parts, compare_date, not_use, always_show, time, round_mode) | from_json -%} {# check for error #} {%- if 'error' in time_parts -%} {{- time_parts['error'] -}} {%- else -%} {# convert to phrases #} {%- set ns = namespace(phrases=[]) -%} {%- for i in time_parts.keys() -%} {%- set phr_abbr = phr[i][2] -%} {%- set phr_verb = phr[i][1] if time_parts[i] != 1 else phr[i][0] -%} {%- set phrase = '{} {}'.format(time_parts[i], phr_abbr if abbr else phr_verb) -%} {%- set ns.phrases = ns.phrases + [phrase] -%} {%- endfor -%} {#- join phrases in a string, using phr.combine for the last item #} {{- '{} {} {}'.format(ns.phrases[:-1] | join(', '), phr.combine, ns.phrases[-1]) if ns.phrases | count > 1 else ns.phrases | first -}} {%- endif -%} {%- endmacro -%}