
Rolling.pudu
Pudu167 lines7.0 KB
1/** @Log.Domain.Rolling.Module — rolling file names, periods, and retention */2module PuduLangLog.Domain.Rolling34import Std.Char as Char5import Std.List as List6import Std.Math as Math7import Std.Option as Option8import Std.Path as Path9import Std.Text as Text10import Std.Time.Format as Calendar11import PuduLangLog.Domain.Dates as Dates12import PuduLangLog as Log1314/** @Log.Domain.Rolling.Rolled — a file name read back into its period */15export type Rolled = { name: Str, period: Option[Int], sequence: Option[Int] }161718const MINUTE: Int = 60000192021export fn periodFormat(interval: Log.RollingInterval) -> Str {22 match interval {23 case Log.Infinite => ""24 case Log.Year => "yyyy"25 case Log.Month => "yyyyMM"26 case Log.Day => "yyyyMMdd"27 case Log.Hour => "yyyyMMddHH"28 case Log.Minute => "yyyyMMddHHmm"29 }30}31323334export fn checkpoint(interval: Log.RollingInterval, moment: &Log.Timestamp) -> Option[Int] {35 let parts = Calendar.partsOf(moment.millis + moment.offset * MINUTE)36 let start = match interval {37 case Log.Infinite => { return None }38 case Log.Year => Calendar.Parts{..parts, month: 1, day: 1, hour: 0, minute: 0, second: 0, milli: 0}39 case Log.Month => Calendar.Parts{..parts, day: 1, hour: 0, minute: 0, second: 0, milli: 0}40 case Log.Day => Calendar.Parts{..parts, hour: 0, minute: 0, second: 0, milli: 0}41 case Log.Hour => Calendar.Parts{..parts, minute: 0, second: 0, milli: 0}42 case Log.Minute => Calendar.Parts{..parts, second: 0, milli: 0}43 }44 Some(Calendar.millisOf(&start))45}464748export fn next(interval: Log.RollingInterval, moment: &Log.Timestamp) -> Option[Int] {49 let start = Calendar.partsOf(checkpoint(interval, moment) ?)50 let following = match interval {51 case Log.Year => Calendar.Parts{..start, year: start.year + 1}52 case Log.Month => Calendar.Parts{..start, month: start.month + 1}53 case Log.Day => Calendar.Parts{..start, day: start.day + 1}54 case Log.Hour => Calendar.Parts{..start, hour: start.hour + 1}55 case _ => Calendar.Parts{..start, minute: start.minute + 1}56 }57 Some(Calendar.millisOf(&following))58}59606162export fn fileName(path: Str, interval: Log.RollingInterval, moment: &Log.Timestamp, sequence: Option[Int]) -> Str {63 let period = match checkpoint(interval, moment) {64 case Some(start) => Dates.formatMoment(&Log.Timestamp{millis: start, offset: 0}, &Some(periodFormat(interval)))65 case None => ""66 }67 let suffix = match sequence {68 case Some(number) => "_" + Text.padLeft(show(number), 3, "0")69 case None => ""70 }71 let (stem, extension) = partsOf(path)72 stem + period + suffix + extension73}747576fn partsOf(path: Str) -> (Str, Str) {77 let (stem, extension) = Path.splitExtension(path)78 match extension {79 case Some(given) => (stem, "." + given)80 case None => (stem, "")81 }82}83848586export fn matches(path: Str, interval: Log.RollingInterval, name: Str) -> Option[Rolled] {87 let (stem, extension) = partsOf(Path.nameOf(path))88 if !name.startsWith(stem) || !name.endsWith(extension) || name.length() < stem.length() + extension.length() { return None }89 let middle = name.slice(stem.length(), name.length() - extension.length())90 let width = periodFormat(interval).length()91 let digits = middle.take(width)92 if digits.length() != width || (width > 0 && !allDigits(digits)) { return None }93 let rest = middle.drop(width)94 let sequence = if rest.isEmpty() {95 None96 } else {97 let number = rest.drop(1)98 if !rest.startsWith("_") || number.length() < 3 || !allDigits(number) { return None }99 Text.countOf(number)100 }101 Some(Rolled{name: name, period: periodOf(interval, digits), sequence: sequence})102}103104105fn allDigits(text: Str) -> Bool { !text.isEmpty() && Text.allChars(text, |character: Char| Char.isDigit(character)) }106107108109fn periodOf(interval: Log.RollingInterval, digits: Str) -> Option[Int] {110 if interval == Log.Infinite { return None }111 let year = Text.countOf(digits.take(4)) ?112 let month = if digits.length() >= 6 { Text.countOf(digits.slice(4, 6)) ? } else { 1 }113 let day = if digits.length() >= 8 { Text.countOf(digits.slice(6, 8)) ? } else { 1 }114 let hour = if digits.length() >= 10 { Text.countOf(digits.slice(8, 10)) ? } else { 0 }115 let minute = if digits.length() >= 12 { Text.countOf(digits.slice(10, 12)) ? } else { 0 }116 if day < 1 || day > Calendar.daysInMonth(year, month) || hour > 23 || minute > 59 { return None }117 Some(Calendar.millisOf(&Calendar.Parts{year: year, month: month, day: day, hour: hour, minute: minute, second: 0, milli: 0}))118}119120121export fn latestSequence(files: &Array[Rolled], period: Option[Int]) -> Option[Int] {122 var latest: Option[Int] = None123 for file in files {124 if file.period == period {125 if let Some(number) = file.sequence {126 latest = Some(Math.max(number, Option.unwrapOr(latest, 0)))127 }128 }129 }130 latest131}132133134135136fn orderOf(file: &Rolled) -> Str { numberKey(&file.period) + numberKey(&file.sequence) + file.name }137138139fn numberKey(number: &Option[Int]) -> Str {140 match number {141 case Some(value) => "1" + Text.padLeft(show(value), 20, "0")142 case None => "0"143 }144}145146147148149export fn retired(files: &Array[Rolled], current: Str, count: Option[Int], oldest: Option[Int]) -> Array[Str] {150 let others = List.sortOn(&files.filter(|file: Rolled| file.name.toLower() != current.toLower()), |file: Rolled| orderOf(&file)).reverse()151 var removed: Array[Str] = []152 var index = 0153 for file in others {154 let tooMany = match count {155 case Some(limit) => index >= limit - 1156 case None => false157 }158 let tooOld = match (oldest, file.period) {159 case (Some(limit), Some(started)) => started < limit160 case _ => false161 }162 if tooMany || tooOld || !removed.isEmpty() { removed = removed.push(file.name) }163 index = index + 1164 }165 removed166}167