{ debug, float, integer, intrinsics, list, string, type, ... }: let daysPerYear = 365; secondsPerMinute = 60; secondsPerHour = 60 * secondsPerMinute; secondsPerDay = 24 * secondsPerHour; yearsPerEra = 400; yearsPerCentury = 100; yearsPerCycle = 4; monthsPerYear = 12; dayOfWeekOfEraBegin = 0; #0: Monday, 6: Sunday DateTime = type "DateTime" { inherit secondsPerMinute secondsPerHour secondsPerDay daysPerYear monthsPerYear yearsPerEra yearsPerCentury yearsPerCycle; inherit after before from fromSet format formatDate formatDateTime formatYearMonth formatYearShortMonth formatISO8601 formatISO8601' getDayName getDayShortName getMonthName getMonthShortName parseDateTime parseISO8601 parseUnixTime tryParseISO8601; current = from (intrinsics.currentTime or 0); }; # DateTime -> DateTime -> bool: after = left: right: let left' = DateTime.expect left; right' = DateTime.expect right; orZero = value: if value != null then value else 0; lYear = left'.year; rYear = right'.year; lMonth = orZero left'.month; rMonth = orZero right'.month; lDay = orZero left'.day; rDay = orZero right'.day; lHour = orZero left'.hour; rHour = orZero right'.hour; lMinute = orZero left'.minute; rMinute = orZero right'.minute; lSecond = orZero left'.second; rSecond = orZero right'.second; in if lYear == rYear then if lMonth == rMonth then if lDay == rDay then if lHour == rHour then if lMinute == rMinute then lSecond > rSecond else lMinute > rMinute else lHour > rHour else lDay > rDay else lMonth > rMonth else lYear > rYear; # DateTime -> DateTime -> bool: before = left: right: let left' = DateTime.expect left; right' = DateTime.expect right; orZero = value: if value != null then value else 0; lYear = left'.year; rYear = right'.year; lMonth = orZero left'.month; rMonth = orZero right'.month; lDay = orZero left'.day; rDay = orZero right'.day; lHour = orZero left'.hour; rHour = orZero right'.hour; lMinute = orZero left'.minute; rMinute = orZero right'.minute; lSecond = orZero left'.second; rSecond = orZero right'.second; in if lYear == rYear then if lMonth == rMonth then if lDay == rDay then if lHour == rHour then if lMinute == rMinute then lSecond < rSecond else lMinute < rMinute else lHour < rHour else lDay < rDay else lMonth < rMonth else lYear < rYear; format#: D: ToDateTime @ D -> string -> string -> string = dateTime: language: format: let date = from dateTime; handle = token: if list.isInstanceOf token then let parts = string.match "%([-_0+^#])*([EO]?[A-Za-y%]|(:{0,3})z)" (list.head token); config = list.fold ( state: token: { "^" = state // { upper = true; }; "#" = state // { opposite = true; }; "-" = state // { padding = ""; }; "_" = state // { padding = " "; }; "0" = state // { padding = "0"; }; "+" = state // { padding = "0"; plus = true; }; }.${token} ) { opposite = false; padding = null; plus = false; upper = false; } (list.get parts 0); pad = value: length: default: let padding = if config.padding != null then config.padding else default; text = if value != null then string value else string.repeat "?" length; len = ( string.length text ) - length; padding' = string.repeat padding len; in if padding != "" && len > 0 then "${padding'}${text}" else if config.plus && len < 0 then "+${text}" else text; adjustCase = text: maybeLower: if maybeLower && config.opposite then string.toLowerCase text else if config.upper then string.toUpperCase text else text; control = list.get parts 1; suffix = list.get parts 2; suffix' = string.length suffix; modulo = y: x: x - ( x / y ) * y; mod7 = modulo 7; mod12 = modulo 12; mod60 = modulo 60; mod100 = modulo 100; quarter = ( ( date.month - 1 ) / 4 ) + 1; isAM = date.hour < 12; dayOfWeekMonday = date.dayOfWeek; dayOfWeekSunday = if date.dayOfWeek == 6 then 0 else date.dayOfWeek + 1; startOfWeek = date.dayOfYear - dayOfWeekMonday; startOfWeek' = date.dayOfYear - dayOfWeekSunday; mondayWeek = if startOfWeek <= 0 then 0 else (startOfWeek + 6) / 7; sundayWeek = if startOfWeek <= 0 then 0 else (startOfWeek' + 6) / 7; isoWeek = ( startOfWeek + 6 ) / 7 + 1; isoYear = if startOfWeek <= 0 then date.year - 1 else date.year; hour = pad date.hour 2 "0"; minute = pad date.minute 2 "0"; second = pad date.second 2 "0"; year = pad date.year 4 "0"; year' = pad (mod100 date.year) 2 "0"; month = pad date.month 2 "0"; day = pad date.day 2 "0"; day' = pad date.day 2 " "; dayShortName = adjustCase ( getDayShortName dayOfWeekMonday ) false; monthShortName = adjustCase ( getMonthShortName date.month ) false; zone = if date.zone != null then date.zone else 0; zoneSign = if zone < 0 then "-" else "+"; zone' = if zone < 0 then 0 - zone else zone; zoneSeconds = pad ( mod60 zone' ) 2 "0"; zoneMinutes = pad ( mod60 ( zone' / secondsPerMinute ) ) 2 "0"; zoneHours = pad ( mod60 ( zone' / secondsPerHour ) ) 2 "0"; zoneHours' = "${zoneSign}${zoneHours}"; zone0 = "${zoneSign}${pad ( zone' / secondsPerMinute ) 4 "0"}"; zone1 = "${zoneHours'}:${zoneMinutes}"; zone2 = "${zoneHours'}:${zoneMinutes}:${zoneSeconds}"; zone3 = if zoneSeconds != 0 then zone2 else if zoneMinutes != 0 then zone1 else zoneHours'; in if suffix == null then { "%" = "%"; "a" = dayShortName; "A" = adjustCase ( getDayName dayOfWeekMonday ) false; "b" = monthShortName; "B" = adjustCase ( getMonthName date.month ) false; "c" = "${dayShortName} ${monthShortName} ${day'} ${hour}:${minute}:${second} ${year}"; "C" = pad ( date.year / 100 ) 2 "0"; "d" = day; "D" = "${month}/${day}/${year'}"; "e" = day'; "F" = "${year}-${month}-${day}"; "g" = pad (mod100 isoYear) 2 "0"; "G" = pad isoYear 4 "0"; "h" = adjustCase ( getMonthShortName date.month ) false; "H" = hour; "I" = pad ( mod12 date.hour ) 2 "0"; "j" = pad date.dayOfYear 3 "0"; "k" = pad date.hour 2 " "; "l" = pad ( mod12 date.hour ) 2 " "; "m" = month; "M" = minute; "n" = "\n"; "N" = pad date.nanosecond 9 "0"; "p" = if isAM then "AM" else "PM"; "P" = if isAM then "am" else "pm"; "q" = pad quarter 1 "0"; "r" = null; # Locale 12-hour clock time, e.g. "%I:%M%S %p" "R" = "${hour}:${minute}"; "s" = pad date.unix 2 "0"; "S" = second; "t" = "\t"; "T" = "${hour}:${minute}:${second}"; "u" = pad ( dayOfWeekMonday + 1 ) 2 "0"; "U" = sundayWeek; "V" = pad isoWeek 2 ""; "w" = pad dayOfWeekSunday 2 "0"; "W" = mondayWeek; "x" = null; # Locale Date "X" = null; # Locale Time "y" = year'; "Y" = year; "z" = zone0; ":z" = zone1; "::z" = zone2; ":::z" = zone3; "Z" = null; # TZ }.${control} or "%${control}" else "" else token; in if date != null then list.fold ( result: token: "${result}${handle token}" ) "" ( string.split "%[-_0+^#]*([EO]?[A-Za-y%]|:{0,3}z)" format ) else debug.panic "format" "Invalid date: ${date}"; formatDate#: D -> string -> string # where D: ToDateTime = dateTime: language: let date = from dateTime; in if date != null then "${string date.day}. ${getMonthName date.month language} ${string date.year}" else debug.panic "formatDate" "Invalid date: ${date}"; formatDateTime#: D -> string -> string # where D: ToDateTime = dateTime: language: let date = from dateTime; pad#: int -> string = value: if value < 10 then "0${string value}" else string value; in if date != null then "${string date.day}. ${getMonthName date.month language} ${string date.year} ${pad date.hour}:${pad date.minute}:${pad date.second}" else debug.panic "formatDateTime" "Invalid date: ${date}"; formatISO8601#: D -> string -> string # where D: ToDateTime = dateTime: let date = from dateTime; pad#: int -> string = value: if value == null then "00" else if value < 10 then "0${string value}" else string value; in if date != null then "${string date.year}-${pad date.month}-${pad date.day}" else debug.panic "formatISO8601" "Invalid date: ${date}"; formatISO8601'#: D -> string -> string # where D: ToDateTime = dateTime: let date = from dateTime; pad#: int -> string = value: if value == null then "00" else if value < 10 then "0${string value}" else string value; in if date != null then "${string date.year}-${pad date.month}-${pad date.day}T${pad date.hour}:${pad date.minute}:${pad date.second}" else debug.panic "formatISO8601'" "Invalid date: ${date}"; formatYearMonth#: string -> string -> string = dateTime: language: let dateTime' = from dateTime; in if dateTime' != null then "${getMonthName dateTime'.month language} ${string dateTime'.year}" else debug.panic "formatYearMonth" "Invalid date: ${dateTime}"; formatYearShortMonth#: string -> string -> string = dateTime: language: let dateTime' = from dateTime; in if dateTime' != null then "${getMonthShortName dateTime'.month language} ${string dateTime'.year}" else debug.panic "formatYearShortMonth" "Invalid date: ${dateTime}"; from#: int | set | string -> DateTime = dateTime: ( type.matchPrimitiveOrPanic dateTime { int = parseUnixTime dateTime; set = fromSet dateTime; string = let dateTime' = tryParseISO8601 dateTime; in if dateTime' != null then parseISO8601 dateTime else parseDateTime dateTime; } ) // { __toString = let string' = value: if value == null then "00" else if value < 10 then "0${string value}" else string value; in { year, month, day, hour, minute, second, ... }: "${string year}-${string' month}-${string' day}T${string' hour}:${string' minute}:${string' second}"; }; fromSet#: # { # year: int, # month: int?, # day: int?, # dayOfWeek: int?, # dayOfYear: int?, # hour: int?, # minute: int?, # second: int?, # nanosecond: int?, # unix: int?, # zone: int?, # zoneName: string?, # } # -> DateTime: = { year, month ? null, day ? null, dayOfWeek ? null, dayOfYear ? null, hour ? null, minute ? null, second ? null, nanosecond ? null, zone ? null, zoneName ? null, unix ? null, ... } @ data: if integer.isInstanceOf year && integer.orNull month && integer.orNull day && integer.orNull dayOfWeek && integer.orNull dayOfYear && integer.orNull hour && integer.orNull minute && integer.orNull second && integer.orNull nanosecond && integer.orNull zone && string.orNull zoneName then DateTime.instanciate { inherit year month day hour minute second nanosecond zone zoneName; } else debug.panic "DateTime" { text = "Value cannot be a DateTime!"; inherit data; }; getDayName#: int -> string -> string = dayOfWeek: language: let days = { eng = [ "Monday" "Thuesday" "Wednesday" "Thursday" "Friday" "Saturday" "Sunday" ]; deu = [ "Montag" "Dienstag" "Mittwoch" "Donnerstag" "Freitag" "Samstag" "Sonntag" ]; }; days' = if language == null then days.eng else ( days.${language} or days.eng); in list.get days' ( dayOfWeek - 1 ); getDayShortName#: int -> string -> string = dayOfWeek: language: let days = { eng = [ "Mon" "Thu" "Wed" "Thu" "Fri" "Sat" "Sun" ]; deu = [ "Mo" "Di" "Mi" "Do" "Fr" "Sa" "So" ]; }; days' = if language == null then days.eng else ( days.${language} or days.eng); in list.get days' ( dayOfWeek - 1 ); getMonthName#: int -> string -> string = month: language: let months = { eng = [ "January" "February" "March" "April" "May" "June" "July" "August" "September" "October" "November" "December" ]; deu = [ "Januar" "Februar" "März" "April" "Mai" "Juni" "Juli" "August" "September" "Oktober" "November" "Dezember" ]; }; months' = if language == null then months.eng else ( months.${language} or months.eng); in list.get months' ( month - 1 ); getMonthShortName#: int -> string -> string = month: language: let months = { eng = [ "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec" ]; deu = [ "Jan" "Feb" "Mär" "Apr" "Mai" "Jun" "Jul" "Aug" "Sep" "Okt" "Nov" "Dez" ]; }; months' = if language == null then months.eng else ( months.${language} or months.eng); in list.get months' ( month - 1 ); parseDateTime#: # Y = "([0-9]{4})", # m = "([0-9]{2})", # d = "([0-9]{2})", # H = "([0-9]{2})", # M = "([0-9]{2})", # S = "([0-9]{2})" # @ "${Y}${m}${d}${H}${M}${S}" -> { year, month, day, hour, minute, second } = dateTime: let dateTime' = string.match "([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})" dateTime; field = list.get dateTime'; in if dateTime' != null then { year = integer (field 0); month = integer (field 1); day = integer (field 2); hour = integer (field 3); minute = integer (field 4); second = integer (field 5); } else null; parseISO8601#: string -> DateTime | ! = iso8601: let dateTime = tryParseISO8601 iso8601; in if dateTime != null then dateTime else debug.panic "parseISO8601" { text = "Cannot parse as ISO 8601-Date:"; data = iso8601; }; parseUnixTime#: int -> int | float | null | string -> DateTime = unix: zoneOrName: let zone = type.matchPrimitiveOrPanic zoneOrName { int = zoneOrName * secondsPerHour; float = float.floor ( zoneOrName * secondsPerHour ); null = 0; string = secondsPerHour * { # ToDo: https://en.wikipedia.org/wiki/List_of_time_zone_abbreviations cest = 2; cet = 1; utc = 0; }.${zoneOrName} or ( debug.panic "parseUnixTime" "Unknown Zone »${zoneOrName}«!" ); }; adjustedUnix = zone + unix; zoneName = if string.isInstanceOf zoneOrName then zoneOrName else null; correctYear#: int -> int = month: if month <= march then 1 else 0; shiftMonth#: int -> int = month: month + march + 1 - ( if month < ( monthsPerYear - march ) then 0 else monthsPerYear ); # For whatever reason, the months are from march (0) to february (11) # Therefor the month must be shifted to january (1) to december (12) and the year must be corrected. leapYearsPerEra = ( yearsPerEra / yearsPerCycle ) # + Regular leap years, e.g. 2004, 2008, 2012 - ( yearsPerEra / yearsPerCentury ) # - Except those divisible by 100, e.g. 1700, 1800, 1900 + ( yearsPerEra / yearsPerEra ); # + But those divisible by 400, e.g. 1600, 2000, 2400 leapYearsPerRegularCentury = yearsPerCentury / yearsPerCycle - 1; daysPerEra = yearsPerEra * daysPerYear + leapYearsPerEra; daysBetweenMarchZeroAndEpoch = 1970 * daysPerYear # Regular Days since 0000-01-01 + 17 # Leap Years in 1900–1970 + 3 * leapYearsPerRegularCentury # Leap Years in 1600–1900 + 4 * leapYearsPerEra # Leap Years in 0–1600 - 31 # Days January - 28; # Days in February of year 0 daysPerCycle = yearsPerCycle * daysPerYear + 1; daysPerCentury = yearsPerCentury * daysPerYear + leapYearsPerRegularCentury; daysFromMarchTillAugust = 31 + 30 + 31 + 30 + 31; march = 2; daysSinceEpoch = adjustedUnix / secondsPerDay; daysSinceMarchZero = daysSinceEpoch + daysBetweenMarchZeroAndEpoch; positiveEra = if daysSinceMarchZero >= 0 then daysSinceMarchZero else daysSinceMarchZero - daysPerEra + 1; era = positiveEra / daysPerEra; dayOfEra = daysSinceMarchZero - era * daysPerEra; yearOfEra = ( dayOfEra - dayOfEra / ( daysPerCycle - 1 ) + dayOfEra / daysPerCentury - dayOfEra / ( daysPerEra - 1 ) ) / daysPerYear; numberOfLeapYears = yearOfEra / yearsPerCycle - yearOfEra / yearsPerCentury; dayOfYear = dayOfEra - ( daysPerYear * yearOfEra + numberOfLeapYears ); month' = ( 5 * dayOfYear + march ) / daysFromMarchTillAugust; day = dayOfYear - ( daysFromMarchTillAugust * month' + march ) / 5 + 1; month = shiftMonth month'; year = yearOfEra + era * yearsPerEra + correctYear month; secondsToday = adjustedUnix - daysSinceEpoch * secondsPerDay; hour = secondsToday / secondsPerHour; secondsThisHour = secondsToday - hour * secondsPerHour; minute = secondsThisHour / secondsPerMinute; second = secondsThisHour - minute * secondsPerMinute; nanosecond = 0; modulo7 = x: x - ( x / 7 ) * 7; dayOfWeek = modulo7 ( dayOfEra + dayOfWeekOfEraBegin ); in DateTime { inherit unix year month day hour minute second nanosecond dayOfWeek dayOfYear zone zoneName; }; tryParseISO8601#: string -> DateTime | null = iso8601: let year = "([+-]?[0-9]{4})"; zone = "(Z|Z?([+-]?[0-9]{2}))?"; a = "([0-9]{1,2})"; # This neither matches milliseconds, intervals nor durations, but YYYYMM regex = "${year}(-?${a}(-?${a}([T_ ]?${a}(:?(${a}(:?${a})?))?)?)?)?${zone}"; matched = string.match regex iso8601; toInteger'#: string | null -> int | null = value: if value != null then integer value else null; in if string.isInstanceOf iso8601 && matched != null then DateTime { year = toInteger' ( list.get matched 0 ); month = toInteger' ( list.get matched 2 ); day = toInteger' ( list.get matched 4 ); dayOfWeek = null; dayOfYear = null; hour = toInteger' ( list.get matched 6 ); minute = toInteger' ( list.get matched 9 ); second = toInteger' ( list.get matched 11 ); nanosecond = 0; zone = toInteger' ( list.get matched 13 ); zoneName = null; } else null; in DateTime // { inherit DateTime; }