
Dates.pudu
Pudu228 lines9.1 KB
1/** @Log.Domain.Dates.Module — timestamp and duration format specifiers */2module PuduLangLog.Domain.Dates34import Std.Map as Map5import Std.Math as Math6import Std.Text as Text7import Std.Time.Format as Calendar8import PuduLangLog as Log91011const WEEKDAYS: Array[Str] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]121314const WEEKDAYS_FULL: Array[Str] = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]151617const MONTHS: Array[Str] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]181920const MONTHS_FULL: Array[Str] = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]212223const STANDARD: Map[Str, Str] = mapOf([24 ("d", "MM/dd/yyyy"), ("D", "dddd, dd MMMM yyyy"), ("f", "dddd, dd MMMM yyyy HH:mm"),25 ("F", "dddd, dd MMMM yyyy HH:mm:ss"), ("g", "MM/dd/yyyy HH:mm"), ("G", "MM/dd/yyyy HH:mm:ss"),26 ("m", "MMMM dd"), ("M", "MMMM dd"), ("o", "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffzzz"),27 ("O", "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffzzz"), ("r", "ddd, dd MMM yyyy HH':'mm':'ss 'GMT'"),28 ("R", "ddd, dd MMM yyyy HH':'mm':'ss 'GMT'"), ("s", "yyyy'-'MM'-'dd'T'HH':'mm':'ss"), ("t", "HH:mm"),29 ("T", "HH:mm:ss"), ("u", "yyyy'-'MM'-'dd HH':'mm':'ss'Z'"), ("U", "dddd, dd MMMM yyyy HH:mm:ss"),30 ("y", "yyyy MMMM"), ("Y", "yyyy MMMM")31 ])323334const UNIVERSAL: Array[Str] = ["r", "R", "u", "U"]353637const DEFAULT_MOMENT: Str = "MM/dd/yyyy HH:mm:ss zzz"383940const DAY: Int = 86400000414243const HOUR: Int = 3600000444546const MINUTE: Int = 6000047484950export fn formatMoment(moment: &Log.Timestamp, format: &Option[Str]) -> Str {51 let given = match format {52 case Some(text) => text53 case None => DEFAULT_MOMENT54 }55 match Map.get(&STANDARD, given) {56 case Some(pattern) => {57 let shown = if UNIVERSAL.contains(given) { Log.Timestamp{..*moment, offset: 0} } else { *moment }58 render(&shown, pattern)59 }60 case None => render(moment, given)61 }62}636465export fn roundTrip(moment: &Log.Timestamp) -> Str { formatMoment(moment, &Some("o")) }6667686970export fn formatSpan(millis: Int, format: &Option[Str]) -> Str {71 let given = match format {72 case Some(text) => text73 case None => "c"74 }75 let minus = if millis < 0 { "-" } else { "" }76 let size = Math.abs(millis)77 let days = size / DAY78 let hours = size % DAY / HOUR79 let minutes = size % HOUR / MINUTE80 let seconds = size % MINUTE / 100081 let fraction = Text.padLeft(show(size % 1000), 3, "0") + "0000"82 let clock = two(minutes) + ":" + two(seconds)83 if given == "c" {84 let dayPart = if days > 0 { show(days) + "." } else { "" }85 let tail = if size % 1000 > 0 { "." + fraction } else { "" }86 return minus + dayPart + two(hours) + ":" + clock + tail87 }88 if given == "g" {89 let dayPart = if days > 0 { show(days) + ":" } else { "" }90 let trimmed = Text.dropWhileEnd(fraction, |character: Char| character == '0')91 let tail = if trimmed.isEmpty() { "" } else { "." + trimmed }92 return minus + dayPart + show(hours) + ":" + clock + tail93 }94 if given == "G" { return minus + show(days) + ":" + two(hours) + ":" + clock + "." + fraction }95 renderSpan(days, hours, minutes, seconds, fraction, given)96}979899fn two(value: Int) -> Str { Text.padLeft(show(value), 2, "0") }100101102fn render(moment: &Log.Timestamp, pattern: Str) -> Str {103 let local = moment.millis + moment.offset * MINUTE104 let parts = Calendar.partsOf(local)105 let characters = pattern.chars()106 var out: Array[Str] = []107 var index = 0108 while index < characters.length() {109 let character = characters[index]110 let count = runOf(&characters, index)111 if character == '\'' || character == '"' {112 let end = closing(&characters, index + 1, character)113 out = out.push(pattern.slice(index + 1, end))114 index = end + 1115 } else if character == '\\' || character == '%' {116 if character == '\\' { out = out.push(pattern.slice(index + 1, index + 2)) }117 index = index + if character == '\\' { 2 } else { 1 }118 } else {119 out = out.push(field(character, count, moment, &parts, local))120 index = index + (if isSpecifier(character) { count } else { 1 })121 }122 }123 out.join("")124}125126127fn runOf(characters: &Array[Char], index: Int) -> Int {128 var end = index129 while end < characters.length() && characters[end] == characters[index] { end = end + 1 }130 end - index131}132133134fn closing(characters: &Array[Char], from: Int, quote: Char) -> Int {135 var end = from136 while end < characters.length() && characters[end] != quote { end = end + 1 }137 end138}139140141fn isSpecifier(character: Char) -> Bool { "dfFghHKmMstyz".contains(character.toText()) }142143144fn field(character: Char, count: Int, moment: &Log.Timestamp, parts: &Calendar.Parts, local: Int) -> Str {145 let pair = Math.min(count, 2)146 match character {147 case 'y' => if count <= 2 { padded(parts.year % 100, count) } else { padded(parts.year, count) }148 case 'M' => named(parts.month, count, &MONTHS, &MONTHS_FULL, parts.month - 1)149 case 'd' => named(parts.day, count, &WEEKDAYS, &WEEKDAYS_FULL, Calendar.weekdayOf(local))150 case 'H' => padded(parts.hour, pair)151 case 'h' => padded(twelveHour(parts.hour), pair)152 case 'm' => padded(parts.minute, pair)153 case 's' => padded(parts.second, pair)154 case 'f' => fractionOf(parts.milli, count, false)155 case 'F' => fractionOf(parts.milli, count, true)156 case 't' => (if parts.hour < 12 { "AM" } else { "PM" }).take(pair)157 case 'z' => offsetOf(moment.offset, count)158 case 'K' => offsetOf(moment.offset, 3)159 case 'g' => "A.D."160 case _ => character.toText()161 }162}163164165fn padded(value: Int, width: Int) -> Str { Text.padLeft(show(value), width, "0") }166167168fn twelveHour(hour: Int) -> Int {169 let wrapped = hour % 12170 if wrapped == 0 { 12 } else { wrapped }171}172173174fn named(value: Int, count: Int, short: &Array[Str], full: &Array[Str], position: Int) -> Str {175 if count <= 2 { return padded(value, count) }176 if count == 3 { return short[position] }177 full[position]178}179180181fn fractionOf(milli: Int, count: Int, trimmed: Bool) -> Str {182 let digits = (Text.padLeft(show(milli), 3, "0") + "0000").take(Math.min(count, 7))183 if trimmed { Text.dropWhileEnd(digits, |character: Char| character == '0') } else { digits }184}185186187fn offsetOf(offset: Int, count: Int) -> Str {188 let mark = if offset < 0 { "-" } else { "+" }189 let size = Math.abs(offset)190 if count == 1 { return mark + show(size / 60) }191 if count == 2 { return mark + two(size / 60) }192 mark + two(size / 60) + ":" + two(size % 60)193}194195196fn renderSpan(days: Int, hours: Int, minutes: Int, seconds: Int, fraction: Str, pattern: Str) -> Str {197 let characters = pattern.chars()198 var out: Array[Str] = []199 var index = 0200 while index < characters.length() {201 let character = characters[index]202 let count = runOf(&characters, index)203 if character == '\'' || character == '"' {204 let end = closing(&characters, index + 1, character)205 out = out.push(pattern.slice(index + 1, end))206 index = end + 1207 } else if character == '\\' {208 out = out.push(pattern.slice(index + 1, index + 2))209 index = index + 2210 } else {211 let pair = Math.min(count, 2)212 let digits = fraction.take(Math.min(count, 7))213 let (text, used) = match character {214 case 'd' => (padded(days, count), count)215 case 'h' => (padded(hours, pair), count)216 case 'm' => (padded(minutes, pair), count)217 case 's' => (padded(seconds, pair), count)218 case 'f' => (digits, count)219 case 'F' => (Text.dropWhileEnd(digits, |digit: Char| digit == '0'), count)220 case _ => (character.toText(), 1)221 }222 out = out.push(text)223 index = index + used224 }225 }226 out.join("")227}228