1package numfmt23import (4 "math"5 "strconv"6 "strings"7 "unicode/utf8"89 "src.rest/cat/012/internal/locale"10)1112// clock is a serial broken into the fields date and time patterns show.13type clock struct {14 toks []ptok15 year, month, day int16 weekday int // 0 for Sunday17 hour, minute, second int18 ampm bool // hours count 1 to 1219 elapsed int64 // whole seconds since day 0, for [h], [m] and [s]20 fracDigits int // decimals of a second shown21 sub int // the fraction of the second, in fracDigits digits22 names *locale.Names23}2425func newClock(v float64, toks []ptok, names *locale.Names) clock {26 c := clock{toks: toks, ampm: hasKind(toks, ptAMPM), fracDigits: secondDecimals(toks), names: names}27 unit := math.Pow(10, float64(c.fracDigits))28 ticks := int64(math.Round(v * 86400 * unit)) // in 10^-fracDigits seconds29 perDay := int64(86400 * unit)30 days := floorDiv(ticks, perDay)31 inDay := ticks - days*perDay32 secs := inDay / int64(unit)33 c.sub = int(inDay % int64(unit))34 c.elapsed = ticks / int64(unit)35 c.year, c.month, c.day = Civil(days)36 c.weekday = Weekday(days)37 c.hour, c.minute, c.second = int(secs/3600), int(secs/60%60), int(secs%60)38 return c39}4041// secondDecimals is how many decimals of a second a pattern shows: .0,42// .00 or .000 right after seconds.43func secondDecimals(toks []ptok) int {44 n := 045 for i, t := range toks {46 seconds := t.kind == ptSecond || t.kind == ptElapsed && t.s == "s"47 if !seconds || i+1 >= len(toks) || toks[i+1].kind != ptDot {48 continue49 }50 zeros := 051 for j := i + 2; j < len(toks) && toks[j].kind == ptDigit && toks[j].s == "0"; j++ {52 zeros++53 }54 n = max(n, min(zeros, 3))55 }56 return n57}5859// formatDate renders serial v as a date, time or duration, with the60// names of months and days in names' language.61func formatDate(v float64, toks []ptok, names *locale.Names) string {62 var b strings.Builder63 if v < 0 && hasKind(toks, ptElapsed) {64 b.WriteByte('-') // a negative duration65 v = -v66 }67 c := newClock(v, toks, names)68 for i := range toks {69 c.write(&b, i)70 }71 return b.String()72}7374// write writes token i of the pattern.75func (c *clock) write(b *strings.Builder, i int) {76 switch t := c.toks[i]; t.kind {77 case ptLit:78 b.WriteString(t.s)79 case ptYear:80 if t.n <= 2 {81 b.WriteString(padN(c.year%100, 2))82 } else {83 b.WriteString(strconv.Itoa(c.year))84 }85 case ptMonth:86 b.WriteString(c.monthText(t.n, i))87 case ptDay:88 b.WriteString(c.dayText(t.n))89 case ptHour:90 b.WriteString(padN(c.hour12(), t.n))91 case ptSecond:92 b.WriteString(padN(c.second, t.n))93 case ptElapsed:94 b.WriteString(c.elapsedText(t))95 case ptAMPM:96 b.WriteString(c.meridiem(t.s))97 case ptDot:98 b.WriteByte('.')99 if secondsFraction(c.toks, i) {100 b.WriteString(padN(c.sub, c.fracDigits))101 }102 case ptDigit:103 // Fraction-of-second digits are written with the dot.104 if !afterSecondsDot(c.toks, i) {105 b.WriteString(t.s)106 }107 case ptComma:108 b.WriteByte(',')109 case ptPercent:110 b.WriteByte('%')111 }112}113114// monthText writes m to mmmmm at token i: minutes (see isMinute), the115// month's number, or its name abbreviated, as its first letter, or116// spelled out, in the form a date with a day of the month takes where117// the language has one ("26 września", "wrzesień 2026").118func (c *clock) monthText(n, i int) string {119 switch {120 case n <= 2 && isMinute(c.toks, i):121 return padN(c.minute, n)122 case n <= 2:123 return padN(c.month, n)124 case n == 3:125 return c.names.Short[c.month-1]126 }127 name := c.names.Month(c.month, !hasDayOfMonth(c.toks))128 if n == 5 {129 r, _ := utf8.DecodeRuneInString(name)130 return strings.ToUpper(string(r))131 }132 return name133}134135// hasDayOfMonth reports whether a pattern shows the day of the month.136func hasDayOfMonth(toks []ptok) bool {137 for _, t := range toks {138 if t.kind == ptDay && t.n <= 2 {139 return true140 }141 }142 return false143}144145// dayText writes d to dddd: the day of the month or of the week.146func (c *clock) dayText(n int) string {147 switch n {148 case 1, 2:149 return padN(c.day, n)150 case 3:151 return c.names.DaysShort[c.weekday]152 }153 return c.names.Days[c.weekday]154}155156func (c *clock) hour12() int {157 if c.ampm {158 return (c.hour+11)%12 + 1159 }160 return c.hour161}162163// elapsedText writes [h], [m] or [s]: the whole duration in that unit.164func (c *clock) elapsedText(t ptok) string {165 switch t.s {166 case "h":167 return padN(int(c.elapsed/3600), t.n)168 case "m":169 return padN(int(c.elapsed/60), t.n)170 }171 return padN(int(c.elapsed), t.n)172}173174// meridiem writes AM/PM or A/P, or the language's words for them.175func (c *clock) meridiem(style string) string {176 pm := c.hour >= 12177 switch {178 case c.names.AM != "" && pm:179 return c.names.PM180 case c.names.AM != "":181 return c.names.AM182 case style == "A/P" && pm:183 return "P"184 case style == "A/P":185 return "A"186 case pm:187 return "PM"188 }189 return "AM"190}191192// secondsFraction reports whether the dot at i starts fractions of a193// second (s.00).194func secondsFraction(toks []ptok, i int) bool {195 return i > 0 && i+1 < len(toks) && toks[i+1].kind == ptDigit &&196 (toks[i-1].kind == ptSecond || toks[i-1].kind == ptElapsed && toks[i-1].s == "s")197}198199// afterSecondsDot reports whether the digit at i belongs to fractions of200// a second, which are written with the dot.201func afterSecondsDot(toks []ptok, i int) bool {202 j := i - 1203 for j >= 0 && toks[j].kind == ptDigit {204 j--205 }206 return j >= 0 && toks[j].kind == ptDot && secondsFraction(toks, j)207}208209// isMinute resolves m and mm: minutes right after hours or before210// seconds, otherwise the month.211func isMinute(toks []ptok, i int) bool {212 for j := i - 1; j >= 0; j-- {213 switch toks[j].kind {214 case ptHour:215 return true216 case ptElapsed:217 return toks[j].s == "h"218 case ptYear, ptMonth, ptDay, ptSecond:219 j = -1220 }221 }222 for j := i + 1; j < len(toks); j++ {223 switch toks[j].kind {224 case ptSecond:225 return true226 case ptElapsed:227 return toks[j].s == "s"228 case ptYear, ptMonth, ptDay, ptHour:229 return false230 }231 }232 return false233}234235func padN(v, n int) string {236 s := strconv.Itoa(v)237 if len(s) < n {238 s = strings.Repeat("0", n-len(s)) + s239 }240 return s241}