diff --git a/jsrc/bashPath.sc b/jsrc/bashPath.sc index 8867754..bb5132c 100755 --- a/jsrc/bashPath.sc +++ b/jsrc/bashPath.sc @@ -1,24 +1,28 @@ #!/usr/bin/env -S scala +//package vastblue.demo //> using scala "3.3.1" -//> using lib "org.vastblue::pallet::0.10.6" +//> using lib "org.vastblue::pallet::0.10.7" import vastblue.pallet.* -lazy val bashPath = where("bash").path +object BashPath { + val bashPath = where("bash").path -def main(args: Array[String]): Unit = - printf("userhome: [%s]\n", userhome) - import scala.sys.process.* - val progname = if (isWindows) { - "where.exe" - } else { - "which" + def main(args: Array[String]): Unit = { + printf("userhome: [%s]\n", userhome) + import scala.sys.process.* + val progname = if (isWindows) { + "where.exe" + } else { + "which" + } + val whereBash = Seq(progname, "bash").lazyLines_!.take(1).mkString + printf("first bash in path:\n%s\n", whereBash) + printf("%s\n", bashPath) + printf("%s\n", bashPath.realpath) + printf("%s\n", bashPath.toRealPath()) + printf("shellRoot: %s\n", shellRoot) + printf("sys root: %s\n", where("bash").posx.replaceAll("(/usr)?/bin/bash.*", "")) } - val whereBash = Seq(progname, "bash").lazyLines_!.take(1).mkString - printf("first bash in path:\n%s\n", whereBash) - printf("%s\n", bashPath) - printf("%s\n", bashPath.realpath) - printf("%s\n", bashPath.toRealPath()) - printf("posixroot: %s\n", posixroot) - printf("sys root: %s\n", where("bash").norm.replaceAll("(/usr)?/bin/bash.*", "")) +} diff --git a/jsrc/bashPathCli.sc b/jsrc/bashPathCli.sc index 7dc38ae..6687f22 100755 --- a/jsrc/bashPathCli.sc +++ b/jsrc/bashPathCli.sc @@ -1,7 +1,7 @@ #!/usr/bin/env -S scala-cli shebang //> using scala "3.3.1" -//> using lib "org.vastblue::pallet::0.10.6" +//> using lib "org.vastblue::pallet::0.10.7" import vastblue.pallet.* @@ -21,6 +21,6 @@ def main(args: Array[String]): Unit = printf("%s\n", bashPath.realpath) printf("%s\n", bashPath.toRealPath()) printf("posixroot: %s\n", posixroot) - printf("sys root: %s\n", where("bash").norm.replaceAll("(/usr)?/bin/bash.*", "")) + printf("sys root: %s\n", where("bash").posx.replaceAll("(/usr)?/bin/bash.*", "")) main(args) diff --git a/jsrc/chronoParse.sc b/jsrc/chronoParse.sc index cb8e74b..84ca42c 100644 --- a/jsrc/chronoParse.sc +++ b/jsrc/chronoParse.sc @@ -1,15 +1,23 @@ #!/usr/bin/env -S scala -deprecation -package vastblue.time +//package vastblue.time import vastblue.pallet.* -import vastblue.time.TimeDate.* +//import vastblue.time.TimeParser import java.time.LocalDateTime +import scala.runtime.RichInt object ChronoParse { - // by default, prefer US format, but swap month and day if unavoidable - // (e.g., 24/12/2022 incompatible with US format, not with Int'l format + var hook = 0 var monthFirst = true + lazy val testDataFile = "testdates.csv".path +// lazy val nowdate = now.toString("yyyy-MM-dd") + lazy val TimeZoneSplitter = "(.*:.*) ?([-+][0-9]{1,2}:00)$".r + lazy val BadDate: LocalDateTime = yyyyMMddHHmmssToDate(List(1900,01,01)) + lazy val now: LocalDateTime = LocalDateTime.now() + lazy val MonthNamesPattern = "(?i)(.*)(Jan[uary]*|Feb[ruary]*|Mar[ch]*|Apr[il]*|May|June?|July?|Aug[ust]*|Sep[tember]*|Oct[ober]*|Nov[ember]*|Dec[ember]*)(.*)".r + // by default, prefer US format, but swap month and day if unavoidable + // (e.g., 24/12/2022 invalid US format, valid Int'l format) def usage(m: String=""): Nothing = { _usage(m, Seq( "[] ; one datetime string per line", @@ -57,7 +65,7 @@ object ChronoParse { } if (verbose) printf("%04d : %s\n", i, row.mkString("|")) - val format = DateFlds(rawline) + val format = ChronoParse(rawline) if (verbose) { printf("rawlin: [%s]\n", rawline) printf(" format: [%s]\n", format.toString) @@ -69,7 +77,7 @@ object ChronoParse { } } - def convertTimestamps(files: Seq[Path]): Seq[DateFlds] = { + def convertTimestamps(files: Seq[Path]): Seq[ChronoParse] = { for { p <- files if p.isFile @@ -84,7 +92,7 @@ object ChronoParse { "" } if rawline.nonEmpty - dateflds = DateFlds(rawline) + dateflds = ChronoParse(rawline) if dateflds.valid } yield dateflds } @@ -110,319 +118,613 @@ object ChronoParse { } } - lazy val testDataFile = "testdates.csv".path - lazy val nowdate = now.toString("yyyy-MM-dd") + def toNum(str: String): Int = { + str match { + case n if n.matches("0\\d+") => + n.replaceAll("0+(.)", "$1").toInt + case n if (n.matches("\\d+")) => + n.toInt + case n if n.contains(".") => + Math.round(n.toDouble).toInt + case "-0" => 0 + case other => + sys.error(s"internal error A: toI($str)") + } } -// derive parse format for dates with numeric fields -// preparation must include converting names of day and month to numeric -object DateFlds { - lazy val TimeZoneSplitter = "(.*:.*) ?([-+][0-9]{1,2}:00)$".r - lazy val BadDate: DateTime = dateParser("1900-01-01") + // If month name present, convert to numeric equivalent. + // month-day order is also implied and must be captured. + // Return array of numbers plus month index (can be -1). + def numerifyNames(_cleanFields: Array[String]): (Int, Seq[String]) = { + var cleanFields = _cleanFields.filter { _.trim.nonEmpty } + //val clean = datestr.replaceAll("(?i)(Sun[day]*|Mon[day]*|Tue[sday]*|Wed[nesday]*|Thu[rsday]*|Fri[day]*|Sat[urday]*),? *", "") + //val cleanFields: Array[String] = clean.split("[-/,\\s]+") + val clean = cleanFields.mkString(" ") - def apply(rawdatetime: String): DateFlds = { - var valid = true - val numerified = numerifyNames(rawdatetime) // toss weekday name, convert month name to number - - // TODO: split into time and date near the outset, handle each separately - var (datetime, timezone) = numerified match { - case TimeZoneSplitter(time, zone) => - (time, zone) - case str => - (str, "") + val monthIndex = clean match { + case MonthNamesPattern(pre, monthName, post) => + val month: Int = monthAbbrev2Number(monthName) + //val numerified = s"$pre $month $post" + val midx = cleanFields.indexWhere( _.contains(monthName) ) +// val midx = cleanFields.indexWhere { (s: String) => s.matches("(?i).*[JFMASOND][aerpuco][nbrylgptvc][a-z]*.*") } + if (midx < 0) { + sys.error(s"internal error: failed to find index of month[$monthName] in [$clean]") + } + cleanFields(midx) = month.toString + midx + case _ => + -1 } - // TODO: use timezone info, including CST, etc + (monthIndex, cleanFields.toIndexedSeq) + } - /* - val timezone = datetime.replaceAll(".*:.* ?([-+][0-9]{1,2}:00)$", "$1").trim - if (timezone.length < datetime.length) { - datetime = datetime.stripSuffix(timezone).trim - } - */ - var numstrings = datetime.replaceAll("\\D+", " ").trim.split(" ").toIndexedSeq - val widenums = numstrings.filter { _.length >= 4 } - widenums.toList match { - case Nil => // no wide num fields - case year :: _ if year.length == 4 => - val i = numstrings.indexOf(year) - if (i > 3) { - val (left, rite) = numstrings.splitAt(i) - val newnumstrings = Seq(year) ++ left ++ rite.drop(1) - numstrings = newnumstrings.toIndexedSeq - } + def cleanPrep(rawdatetime: String): (Seq[String], String, String, Int, Int) = { + var monthIndex: Int = -1 + var yearIndex: Int = -1 + if (rawdatetime.endsWith(" 2020")) { hook += 1 - case ymd :: _ => - hook += 1 // maybe 20240213 or similar - var (y, m, d) = ("", "", "") - if (ymd.startsWith("2") && ymd.length == 8) { - // assume yyyy/mm/dd - y = ymd.take(4) - m = ymd.drop(4).take(2) - d = ymd.drop(6) - } else if (ymd.drop(4).matches("2[0-9]{3}") ){ - if (monthFirst) { - // assume mm/dd/yyyy - m = ymd.take(2) - d = ymd.drop(2).take(2) - y = ymd.drop(4) + } + val (datefields, timestring, timezone) = { + // toss weekday name, convert month name to number + val (cleandates: Array[String], cleantimes: Array[String], timezone: String) = { + val cleaned = rawdatetime. + replaceAll("([0-9])-", "$1/"). // remove all hyphens except minus signs + replaceAll("([a-zA-Z])([0-9])", "$1 $2"). // separate immediately adjacent numeric and alpha fields + replaceAll("([0-9])([a-zA-Z])", "$1 $2"). // ditto + replaceAll("([-])([AP]M)\\b", "$1 $2"). // separate hyphens from AM/PM fields (e.g., AM- or -AM) + replaceAll("(\\b[[AP]M])([-])", "$1 $2"). // ditto + replaceAll("([0-9])T([0-9])", "$1 $2"). // remove T separating date and time + // discard day-of-week + replaceAll("(?i)(Sun[day]*|Mon[day]*|Tue[sday]*|Wed[nesday]*|Thu[rsday]*|Fri[day]*|Sat[urday]*),? *", "") + + val splitRegex = if (cleaned.contains(":")) { + "[/,\\s]+" } else { - // assume dd/mm/yyyy - d = ymd.take(2) - m = ymd.drop(2).take(2) - y = ymd.drop(4) + "[-/,\\s]+" // also split on hyphens } - } - val newymd = Seq(y, m, d) - val newnumstrings: Seq[String] = { - val head: String = numstrings.head - if (head == ymd) { - val rite: Seq[String] = numstrings.tail - val (mid: Seq[String], tail: Seq[String]) = rite.splitAt(1) - val hrmin: String = mid.mkString - if (hrmin.matches("[0-9]{3,4}")) { - val (hr: String, min: String) = hrmin.splitAt(hrmin.length-2) - val hour = if (hr.length == 1) { - s"0$hr" - } else { - hr + val (dts, tms) = { + val ff = cleaned. + split(splitRegex). + filter { + case "-AM" | "AM" => false + case _ => true } - val lef: Seq[String] = newymd - val mid: Seq[String] = Seq(hour, min) - newymd ++ mid ++ tail - } else { - newymd ++ rite + ff.partition { + case s if s.contains(":") => + false // HH mm or ss + case s if s.matches("(?i)([AP]M)?[-+][0-9]{4}") => + false // time zone + case s if s.matches("[.][0-9]+") => + false // decimal time field + case s if timeZoneCodes.contains(s) => + false + case _ => + true // date } - } else { - val i = numstrings.indexOf(ymd) - val (left, rite) = numstrings.splitAt(i) - val newhrmin = rite.drop(1) - left ++ newymd ++ newhrmin } + val (times, zones) = tms.partition { (s: String) => + s.contains(":") // || s.matches("(?i)([AP]M)?[-+][0-9]{4}") + } + (dts, times, zones.mkString(" ")) } - numstrings = newnumstrings.toIndexedSeq + + val (_monthIndex, cleanFields) = numerifyNames(cleandates ++ cleantimes) + monthIndex = _monthIndex + + // separate into time and date, then handle each separately + val (datefields, timefields) = { + cleanFields.indexWhere(_.contains(":")) match { + case -1 => // no time fields + (cleanFields, Nil) + case num => + // TODO: date fields (e.g., year) can sometimes appear after time fields + var (dates, times) = cleanFields.splitAt(num) + val widefields: Seq[String] = times.filter { (s: String) => !s.startsWith("0") && !s.contains(":") && s.length == 4 } + if (widefields.nonEmpty) { + val yi: Int = widefields.indexWhere(_.startsWith("2")) + if (yi < 0) { + hook += 1 // unexpected? + } + val yy = widefields(yi) + if (yy.length == 4) { + // move year from times to dates + val yi = times.indexOf(yy) + dates = Seq(yy) ++ dates + yearIndex = 0 + times = { + val (a, b) = times.splitAt(yi) + a ++ b.drop(1) + } + } + } + (dates, times) + } + } + val timestring: String = timefields.mkString(" ") + + (datefields, timestring, timezone) } - - var nums = numstrings.map { ti(_) } - val timeOnly: Boolean = numstrings.size <= 4 && rawdatetime.matches("[0-9]{2}:[0-9]{2}.*") - if ( !timeOnly ) { - def adjustYear(year: Int): Unit = { - nums = nums.take(2) ++ Seq(year) ++ nums.drop(3) - numstrings = nums.map { - _.toString + (datefields, timestring, timezone, monthIndex, yearIndex) + } + + lazy val BadChrono = new ChronoParse(BadDate, "", "", Nil, false) + def isDigit(c: Char): Boolean = c >= '0' && c <= '9' + + /* + * ChronoParse constructor. + */ + def apply(_rawdatetime: String): ChronoParse = { + if (!isPossibleDateString(_rawdatetime)) { + BadChrono + } else { + if (_rawdatetime.startsWith("10:")) { + hook += 1 + } + var valid = true + var confident = true + + val (datefields, timestring, timezone, _monthIndex, _yearIndex) = cleanPrep(_rawdatetime) + + var (monthIndex: Int, yearIndex: Int) = (_monthIndex, _yearIndex) + + // def timestring = s"$timestring $timezone" + val rawdatetime = s"${datefields.mkString(" ")} ${timestring} $timezone".trim + + var _datenumstrings: IndexedSeq[String] = Nil.toIndexedSeq + if (datefields.nonEmpty) { + setDatenums( + datefields.mkString(" ").replaceAll("\\D+", " ").trim.split(" +").toIndexedSeq + ) + } + + def setDatenums(newval: Seq[String]): Unit = { + if (newval.isEmpty) { + hook += 1 + } + val bad = newval.exists{ (s: String) => + s.trim.isEmpty || !s.matches("[0-9]+") + } + if (bad){ + hook += 1 } + _datenumstrings = newval.toIndexedSeq } - val dateFields = nums.take(3) - dateFields match { - case Seq(a, b, c) if a > 31 || b > 31 || c > 31 => - hook += 1 // the typical case where 4-digit year is provided - case Seq(a, b) => - // the problem case; assume no year provided - adjustYear(now.getYear) // no year provided, use current year - case Seq(mOrD, dOrM, relyear) => - // the problem case; assume M/d/y or d/M/y format - val y = now.getYear - val century = y - y % 100 - adjustYear(century + relyear) - case _ => - hook += 1 // huh? + def datenumstrings = _datenumstrings + + def swapDayAndMonth(dayIndex: Int, monthIndex: Int, monthStr: String, numstrings: IndexedSeq[String]): IndexedSeq[String] = { + val maxIndex = numstrings.length - 1 + assert(dayIndex >= 0 && monthIndex >= 0 && dayIndex <= maxIndex && monthIndex <= maxIndex) + val day = numstrings(dayIndex) + var newnumstrings = numstrings.updated(monthIndex, day) + newnumstrings = newnumstrings.updated(dayIndex, monthStr) + newnumstrings } - } - - val fields: Seq[(String, Int)] = numstrings.zipWithIndex - var (yval, mval, dval) = (0, 0, 0) - val farr = fields.toArray - var formats: Array[String] = farr.map { (s: String, i: Int) => - if (i < 3 && !timeOnly) { - ti(s) match { - case y if y > 31 || s.length == 4 => - yval = y - s.replaceAll(".", "y") - case d if d > 12 && s.length <= 2 => - dval = d - s.replaceAll(".", "d") - case _ => // can't resolve month without more context - s + + val widenums = datenumstrings.filter { _.length >= 4 } + widenums.toList match { + case Nil => // no wide num fields + case year :: _ if year.length == 4 && year.toInt >= 1000 => + yearIndex = datenumstrings.indexOf(year) + if (yearIndex > 3) { + val (left, rite) = datenumstrings.splitAt(yearIndex) + val newnumstrings = Seq(year) ++ left ++ rite.drop(1) + setDatenums(newnumstrings.toIndexedSeq) } - } else { - i match { - case 3 => s.replaceAll(".", "H") - case 4 => s.replaceAll(".", "m") - case 5 => s.replaceAll(".", "s") - case 6 => s.replaceAll(".", "Z") - case _ => - s // not expecting any more numeric fields + hook += 1 + case ymd :: _ => + hook += 1 // maybe 20240213 or similar + var (y, m, d) = ("", "", "") + if (ymd.toInt >= 1000 && ymd.length == 8) { + // assume yyyy/mm/dd + y = ymd.take(4) + m = ymd.drop(4).take(2) + d = ymd.drop(6) + } else if (ymd.drop(4).matches("2[0-9]{3}") ){ + if (monthFirst) { + // assume mm/dd/yyyy + m = ymd.take(2) + d = ymd.drop(2).take(2) + y = ymd.drop(4) + } else { + // assume dd/mm/yyyy + d = ymd.take(2) + m = ymd.drop(2).take(2) + y = ymd.drop(4) + } + } + val newymd = Seq(y, m, d) + val newnumstrings: Seq[String] = { + val head: String = datenumstrings.head + if (head == ymd) { + val rite: Seq[String] = datenumstrings.tail + val (mid: Seq[String], tail: Seq[String]) = rite.splitAt(1) + val hrmin: String = mid.mkString + if (hrmin.matches("[0-9]{3,4}")) { + val (hr: String, min: String) = hrmin.splitAt(hrmin.length-2) + val hour = if (hr.length == 1) { + s"0$hr" + } else { + hr + } + val lef: Seq[String] = newymd + val mid: Seq[String] = Seq(hour, min) + newymd ++ mid ++ tail + } else { + newymd ++ rite + } + } else { + val i = datenumstrings.indexOf(ymd) + val (left, rite) = datenumstrings.splitAt(i) + val newhrmin = rite.drop(1) + left ++ newymd ++ newhrmin + } } + setDatenums(newnumstrings.toIndexedSeq) } - } - def indexOf(s: String): Int = { - formats.indexWhere((fld: String) => - fld.startsWith(s) - ) - } - def numIndex: Int = { - formats.indexWhere((s: String) => s.matches("[0-9]+")) - } - def setFirstNum(s: String): Int = { - val i = numIndex - val numval = formats(i) - val numfmt = numval.replaceAll("[0-9]", s) - formats(i) = numfmt - ti(numval) - } - // if two yyyy-MM-dd fields already fixed, the third is implied - formats.take(3).map { _.distinct }.sorted match { - case Array(_, "M", "y") => dval = setFirstNum("d") - case Array(_, "d", "y") => mval = setFirstNum("M") - case Array(_, "M", "d") => yval = setFirstNum("y") - case _arr => - hook += 1 // more than one numeric fields, so not ready to resolve - } - hook += 1 - def is(s: String, v: String): Boolean = s.startsWith(v) - - val yidx = indexOf("y") - val didx = indexOf("d") - val midx = indexOf("M") - - def hasY = yidx >= 0 - def hasM = midx >= 0 - def hasD = didx >= 0 - def needsY = yidx < 0 - def needsM = midx < 0 - def needsD = didx < 0 - - def replaceFirstNumericField(s: String): Unit = { - val i = numIndex - if (i < 0) { - hook += 1 // no numerics found + + if (yearIndex > 2) { + new ChronoParse(BadDate, _rawdatetime, "", Nil, false) } else { - assert(i >= 0 && i < 3, s"internal error: $datetime [i: $i, s: $s]") - s match { - case "y" => - assert(yval == 0, s"yval: $yval") - yval = ti(formats(i)) - case "M" => - assert(mval == 0, s"mval: $mval") - mval = ti(formats(i)) - case "d" => - if (dval > 0) { + if (monthIndex < 0) { + // TODO : wrap this as a return value + (yearIndex, monthFirst) match { + case (-1, _) => // not enough info + case (0, true) => // y-m-d + monthIndex = 1 + case (0, false) => // y-d-m + monthIndex = 2 + case (2, true) => // m-d-y + monthIndex = 0 + case (2, false) => // d-m-y + monthIndex = 1 + case (1, true) => // m-y-d // ambiguous! + confident = false + monthIndex = 0 + case (1, false) => // d-y-m // ambiguous! + confident = false + monthIndex = 2 + case _ => + hook += 1 // TODO + } + } + def centuryPrefix(year: Int = now.getYear): String = { + century(year).toString.take(2) + } + def century(y: Int = now.getYear): Int = { + (y - y % 100) + } + (monthIndex, yearIndex) match { + case (_, -1) => + datenumstrings.take(3) match { + case Seq(m: String, d: String, y:String) if m.length <= 2 & d.length <= 2 && y.length <= 2 => + if (monthFirst) { + val fullyear = s"${centuryPrefix()}$y" + setDatenums(datenumstrings.updated(2, fullyear)) + } + case _ => + // TODO verify this cannot happen (year index not initialized yet, so previous case is complete) hook += 1 } - assert(dval == 0, s"dval: $dval") - dval = ti(formats(i)) + + case (-1, _) => + hook += 1 + case (0, 2) | (1, 0) => // m-d-y | y-m-d (month precedes day) + val month = toNum(datenumstrings(monthIndex)) + if (!monthFirst && month <= 12) { + val dayIndex = monthIndex + 1 + // swap month and day, if preferred and possible + val day = datenumstrings(dayIndex) + var newnums = datenumstrings.updated(monthIndex, day) + newnums = newnums.updated(dayIndex, month.toString) + setDatenums(newnums) + } + case (1, 2) => // d-m-y + val month = toNum(datenumstrings(monthIndex)) + if (monthFirst && month <= 12) { + // swap month and day, if preferred and possible + val dayIndex = monthIndex - 1 + val swapped = swapDayAndMonth(dayIndex, monthIndex, month.toString, datenumstrings) + setDatenums(swapped) + // swap month and day, if preferred and possible + // val day = datenumstrings(dayIndex) + // setDatenums(datenumstrings.updated(monthIndex, day)) + // setDatenums(datenumstrings.updated(dayIndex, month.toString)) + } + case (m, y) => // d-m-y + hook += 1 // TODO + } + if (monthIndex >= 0) { + val month = toNum(datenumstrings(monthIndex)) + if (!monthFirst && month <= 12) { + // swap month and day, if preferred and possible + val day = datenumstrings(monthIndex + 1) + var newnums = datenumstrings.updated(monthIndex, day) + newnums = newnums.updated(monthIndex+1, month.toString) + setDatenums(newnums) + } + } + //var nums: Array[Int] = datefields.map { (s: String) => toI(s) } + var nums: Seq[Int] = datenumstrings.filter { _.trim.nonEmpty }.map { (numstr: String) => + if (!numstr.matches("[0-9]+")) { + hook += 1 + } + toNum(numstr) + } + + val timeOnly: Boolean = datenumstrings.size <= 4 && rawdatetime.matches("[0-9]{2}:[0-9]{2}.*") + if ( !timeOnly ) { + def adjustYear(year: Int): Unit = { + nums = nums.take(2) ++ Seq(year) ++ nums.drop(3) + val newnums = nums.map { _.toString } + setDatenums(newnums) + } + val dateFields = nums.take(3) + dateFields match { + case Seq(a, b, c) if a > 31 || b > 31 || c > 31 => + hook += 1 // the typical case where 4-digit year is provided + case Seq(a, b) => + // the problem case; assume no year provided + adjustYear(now.getYear) // no year provided, use current year + case Seq(mOrD, dOrM, relyear) => + // the problem case; assume M/d/y or d/M/y format + val y = now.getYear + val century = y - y % 100 + adjustYear(century + relyear) case _ => - sys.error(s"internal error: bad format indicator [$s]") + hook += 1 // huh? + } } - setFirstNum(s) - } - } - val needs = Seq(needsY, needsM, needsD) - (needsY, needsM, needsD) match { - case (false, false, true) => - replaceFirstNumericField("d") - case (false, true, false) => - replaceFirstNumericField("M") - case (true, false, false) => - replaceFirstNumericField("y") - - case (false, true, true) => - // has year, needs month and day - yidx match { - case 1 => - // might as well support bizarre formats (M-y-d or d-M-y) - if (monthFirst) { - replaceFirstNumericField("M") - replaceFirstNumericField("d") - } else { - replaceFirstNumericField("d") - replaceFirstNumericField("M") + val fields: Seq[(String, Int)] = datenumstrings.zipWithIndex + var (yval, mval, dval) = (0, 0, 0) + val farr = fields.toArray + var formats: Array[String] = farr.map { (s: String, i: Int) => + if (i < 3 && !timeOnly) { + toNum(s) match { + case y if y > 31 || s.length == 4 => + yval = y + s.replaceAll(".", "y") + case d if d > 12 && s.length <= 2 => + dval = d + s.replaceAll(".", "d") + case _ => // can't resolve month without more context + s + } + } else { + i match { + case 3 => s.replaceAll(".", "H") + case 4 => s.replaceAll(".", "m") + case 5 => s.replaceAll(".", "s") + case 6 => s.replaceAll(".", "Z") + case _ => + s // not expecting any more numeric fields + } + } } - case 0 | 2 => - // y-M-d - if (monthFirst) { - replaceFirstNumericField("M") - replaceFirstNumericField("d") - } else { - replaceFirstNumericField("d") - replaceFirstNumericField("M") + def indexOf(s: String): Int = { + formats.indexWhere((fld: String) => + fld.startsWith(s) + ) + } + def numIndex: Int = { + formats.indexWhere((s: String) => s.matches("[0-9]+")) } + def setFirstNum(s: String): Int = { + val i = numIndex + if (i < 0) { + hook += 1 + } + val numval = formats(i) + val numfmt = numval.replaceAll("[0-9]", s) + formats(i) = numfmt + toNum(numval) + } + // if two yyyy-MM-dd fields already fixed, the third is implied + formats.take(3).map { _.distinct }.sorted match { + case Array(_, "M", "y") => dval = setFirstNum("d") + case Array(_, "d", "y") => mval = setFirstNum("M") + case Array(_, "M", "d") => yval = setFirstNum("y") + case _arr => + hook += 1 // more than one numeric fields, so not ready to resolve + } + hook += 1 + def is(s: String, v: String): Boolean = s.startsWith(v) - } - case (true, true, false) => - // has day, needs month and year - didx match { - case 0 => - // d-M-y - replaceFirstNumericField("M") - replaceFirstNumericField("y") - case 2 => - // y-M-d - replaceFirstNumericField("y") - replaceFirstNumericField("M") - case 1 => - // AMBIGUOUS ... - if (monthFirst) { - // M-d-y + val yidx = indexOf("y") + val didx = indexOf("d") + val midx = indexOf("M") + + def hasY = yidx >= 0 + def hasM = midx >= 0 + def hasD = didx >= 0 + def needsY = yidx < 0 + def needsM = midx < 0 + def needsD = didx < 0 + + def replaceFirstNumericField(s: String): Unit = { + val i = numIndex + if (i < 0) { + hook += 1 // no numerics found + } else { + assert(i >= 0 && i < 3, s"internal error: $_rawdatetime [i: $i, s: $s]") + s match { + case "y" => + assert(yval == 0, s"yval: $yval") + yval = toNum(formats(i)) + case "M" => + assert(mval == 0, s"mval: $mval") + mval = toNum(formats(i)) + case "d" => + if (dval > 0) { + hook += 1 + } + assert(dval == 0, s"dval: $dval") + dval = toNum(formats(i)) + case _ => + sys.error(s"internal error: bad format indicator [$s]") + } + setFirstNum(s) + } + } + + val needs = Seq(needsY, needsM, needsD) + (needsY, needsM, needsD) match { + case (false, false, true) => replaceFirstNumericField("d") + case (false, true, false) => replaceFirstNumericField("M") - } else { - // d-M-y - replaceFirstNumericField("M") - replaceFirstNumericField("d") + case (true, false, false) => + replaceFirstNumericField("y") + + case (false, true, true) => + // has year, needs month and day + yidx match { + case 1 => + // might as well support bizarre formats (M-y-d or d-M-y) + if (monthFirst) { + replaceFirstNumericField("M") + replaceFirstNumericField("d") + } else { + replaceFirstNumericField("d") + replaceFirstNumericField("M") + } + case 0 | 2 => + // y-M-d + if (monthFirst) { + replaceFirstNumericField("M") + replaceFirstNumericField("d") + } else { + replaceFirstNumericField("d") + replaceFirstNumericField("M") + } + + } + case (true, true, false) => + // has day, needs month and year + didx match { + case 0 => + // d-M-y + replaceFirstNumericField("M") + replaceFirstNumericField("y") + case 2 => + // y-M-d + replaceFirstNumericField("y") + replaceFirstNumericField("M") + case 1 => + // AMBIGUOUS ... + if (monthFirst) { + // M-d-y + replaceFirstNumericField("d") + replaceFirstNumericField("M") + } else { + // d-M-y + replaceFirstNumericField("M") + replaceFirstNumericField("d") + } + } + case (false, false, false) => + hook += 1 // done + case (true, true, true) if timeOnly => + hook += 1 // done + case (yy, mm, dd) => + formats.toList match { + case a :: b :: Nil => + val (ta, tb) = (toNum(a), toNum(b)) + // interpret as missing day or missing year + // missing day if either field is > 31 + if (monthFirst && ta <= 12) { + mval = ta + dval = tb + } else { + mval = tb + dval = tb + } + if (mval > 31) { + // assume day is missing + yval = mval + mval = dval + dval = 1 // convention + } else if (dval > 31) { + // assume day is missing + yval = dval + dval = 1 // convention + } else { + if (mval > 12) { + // the above swap might make this superfluous + // swap month and day + val temp = mval + mval = dval + dval = temp + } + yval = now.getYear // supply missing year + } + // TODO: reorder based on legal field values, if appropriate + formats = Array("yyyy", "MM", "dd") + setDatenums(IndexedSeq(yval, mval, dval).map { _.toString }) + case _ => + if (datenumstrings.nonEmpty) { + sys.error(s"yy[$yy], mm[$mm], dd[$dd] datetime[$_rawdatetime], formats[${formats.mkString("|")}]") + } + } } - } - case (false, false, false) => - hook += 1 // done - case (true, true, true) if timeOnly => - hook += 1 // done - case (yy, mm, dd) => - formats.toList match { - case a :: b :: Nil => - val (ta, tb) = (ti(a), ti(b)) - // interpret as missing day or missing year - // missing day if either field is > 31 - if (monthFirst && ta <= 12) { - mval = ta - dval = tb - } else { - mval = tb - dval = tb - } - if (mval > 31) { - // assume day is missing - yval = mval - mval = dval - dval = 1 // convention - } else if (dval > 31) { - // assume day is missing - yval = dval - dval = 1 // convention - } else { - if (mval > 12) { - // the above swap might make this superfluous - // swap month and day - val temp = mval - mval = dval - dval = temp + if (datenumstrings.endsWith("2019") ){ + hook += 1 + } + + val bareformats = formats.map { _.distinct }.toList + nums = { + val tdnums = (datenumstrings ++ timestring.split("[+: ]+")) + tdnums.filter { _.trim.nonEmpty }.map { toNum(_) } + } + def ymd(iy: Int, im: Int, id: Int, tail: List[String]): LocalDateTime = { + if (iy <0 || im <0 || id <0) { + hook += 1 + } else if (nums.size < 3) { + hook += 1 } - yval = now.getYear // supply missing year + val standardOrder = List(nums(iy), nums(im), nums(id)) ++ nums.drop(3) + yyyyMMddHHmmssToDate(standardOrder) } - // TODO: reorder based on legal field values, if appropriate - formats = Array("yyyy", "MM", "dd") - numstrings = IndexedSeq(yval, mval, dval).map { _.toString } - case _ => - sys.error(s"yy[$yy], mm[$mm], dd[$dd] datetime[$datetime], formats[${formats.mkString("|")}]") + val dateTime: LocalDateTime = bareformats match { + case "d" :: "M" :: "y" :: tail => ymd(2,1,0, tail) + case "M" :: "d" :: "y" :: tail => ymd(2,0,1, tail) + case "d" :: "y" :: "M" :: tail => ymd(1,2,0, tail) + case "M" :: "y" :: "d" :: tail => ymd(1,0,2, tail) + case "y" :: "d" :: "M" :: tail => ymd(0,2,1, tail) + case "y" :: "M" :: "d" :: tail => ymd(0,1,2, tail) + case other => + valid = false + BadDate + } + new ChronoParse(dateTime, _rawdatetime, timezone, formats.toSeq, valid) } } - if (numstrings.endsWith("2019") ){ - hook += 1 - } + } + + def validYear(y: Int): Boolean = y > 0 && y < 2500 + def validMonth(m: Int): Boolean = m > 0 && m <= 12 + def validDay(d: Int): Boolean = d > 0 && d <= 31 - def fromStandardOrder(so: List[Int]): LocalDateTime = { - so match { + def validYmd(ymd: Seq[Int]): Boolean = { + val Seq(y: Int, m: Int, d: Int) = ymd + validYear(y) && validMonth(m) && validDay(d) + } + def validTimeFields(nums: Seq[Int]): Boolean = { + nums.forall { (n: Int) => n >= 0 && n <= 59 } + } + def yyyyMMddHHmmssToDate(so: List[Int]): LocalDateTime = { + if (!validYmd(so.take(3))) { + BadDate + } else if(!validTimeFields(so.drop(3))) { + BadDate + } else { + so.take(7) match { case yr :: mo :: dy :: hr :: mn :: sc :: nano :: Nil => - LocalDateTime.of(yr, mo, dy, hr, mn, sc, nano) + if (hr > 12 || mn > 12 || sc > 12) { + BadDate + } else { + LocalDateTime.of(yr, mo, dy, hr, mn, sc, nano) + } case yr :: mo :: dy :: hr :: mn :: sc :: Nil => if (sc > 59 || mn > 59 || hr > 59) { hook += 1 @@ -431,6 +733,9 @@ object DateFlds { case yr :: mo :: dy :: hr :: mn :: Nil => LocalDateTime.of(yr, mo, dy, hr, mn, 0) case yr :: mo :: dy :: hr :: Nil => + if (hr > 23) { + hook += 1 + } LocalDateTime.of(yr, mo, dy, hr, 0, 0) case yr :: mo :: dy :: Nil => if (mo > 12) { @@ -438,36 +743,287 @@ object DateFlds { } LocalDateTime.of(yr, mo, dy, 0, 0, 0) case other => - sys.error(s"not enough date-time fields: [${so.mkString("|")}]") + //sys.error(s"not enough date-time fields: [${so.mkString("|")}]") + BadDate } } + } - val bareformats = formats.map { _.distinct }.toList - nums = numstrings.map { ti(_) }.toIndexedSeq - def ymd(iy: Int, im: Int, id: Int, tail: List[String]): LocalDateTime = { - if (iy <0 || im <0 || id <0) { - hook += 1 - } else if (nums.size < 3) { - hook += 1 + lazy val timeZoneCodes = Set( + "ACDT", // Australian Central Daylight Saving Time UTC+10:30 + "ACST", // Australian Central Standard Time UTC+09:30 + "ACT", // Acre Time UTC−05 + "ACT", // ASEAN Common Time (proposed) UTC+08:00 + "ACWST", // Australian Central Western Standard Time (unofficial) UTC+08:45 + "ADT", // Atlantic Daylight Time UTC−03 + "AEDT", // Australian Eastern Daylight Saving Time UTC+11 + "AEST", // Australian Eastern Standard Time UTC+10 + "AET", // Australian Eastern Time UTC+10 / UTC+11 + "AEST", // Australian Eastern Time UTC+10 / UTC+11, + "AEDT", // Australian Eastern Time UTC+10 / UTC+11 + "AFT", // Afghanistan Time UTC+04:30 + "AKDT", // Alaska Daylight Time UTC−08 + "AKST", // Alaska Standard Time UTC−09 + "ALMT", // Alma-Ata Time[1] UTC+06 + "AMST", // Amazon Summer Time (Brazil)[2] UTC−03 + "AMT", // Amazon Time (Brazil)[3] UTC−04 + "AMT", // Armenia Time UTC+04 + "ANAT", // Anadyr Time[4] UTC+12 + "AQTT", // Aqtobe Time[5] UTC+05 + "ART", // Argentina Time UTC−03 + "AST", // Arabia Standard Time UTC+03 + "AST", // Atlantic Standard Time UTC−04 + "AWST", // Australian Western Standard Time UTC+08 + "AZOST", // Azores Summer Time UTC±00 + "AZOT", // Azores Standard Time UTC−01 + "AZT", // Azerbaijan Time UTC+04 + "BNT", // Brunei Time UTC+08 + "BIOT", // British Indian Ocean Time UTC+06 + "BIT", // Baker Island Time UTC−12 + "BOT", // Bolivia Time UTC−04 + "BRST", // Brasília Summer Time UTC−02 + "BRT", // Brasília Time UTC−03 + "BST", // Bangladesh Standard Time UTC+06 + "BST", // Bougainville Standard Time[6] UTC+11 + "BST", // British Summer Time (British Standard Time from Mar 1968 to Oct 1971) UTC+01 + "BTT", // Bhutan Time UTC+06 + "CAT", // Central Africa Time UTC+02 + "CCT", // Cocos Islands Time UTC+06:30 + "CDT", // Central Daylight Time (North America) UTC−05 + "CDT", // Cuba Daylight Time[7] UTC−04 + "CEST", // Central European Summer Time UTC+02 + "CET", // Central European Time UTC+01 + "CHADT", // Chatham Daylight Time UTC+13:45 + "CHAST", // Chatham Standard Time UTC+12:45 + "CHOT", // Choibalsan Standard Time UTC+08 + "CHOST", // Choibalsan Summer Time UTC+09 + "CHST", // Chamorro Standard Time UTC+10 + "CHUT", // Chuuk Time UTC+10 + "CIST", // Clipperton Island Standard Time UTC−08 + "CKT", // Cook Island Time UTC−10 + "CLST", // Chile Summer Time UTC−03 + "CLT", // Chile Standard Time UTC−04 + "COST", // Colombia Summer Time UTC−04 + "COT", // Colombia Time UTC−05 + "CST", // Central Standard Time (North America) UTC−06 + "CST", // China Standard Time UTC+08 + "CST", // Cuba Standard Time UTC−05 + "CT", + "CST", + "CDT", // Central Time UTC−06 / UTC−05 + "CVT", // Cape Verde Time UTC−01 + "CWST", // Central Western Standard Time (Australia) unofficial UTC+08:45 + "CXT", // Christmas Island Time UTC+07 + "DAVT", // Davis Time UTC+07 + "DDUT", // Dumont d'Urville Time UTC+10 + "DFT", // AIX-specific equivalent of Central European Time[NB 1] UTC+01 + "EASST", // Easter Island Summer Time UTC−05 + "EAST", // Easter Island Standard Time UTC−06 + "EAT", // East Africa Time UTC+03 + "ECT", // Eastern Caribbean Time (does not recognise DST) UTC−04 + "ECT", // Ecuador Time UTC−05 + "EDT", // Eastern Daylight Time (North America) UTC−04 + "EEST", // Eastern European Summer Time UTC+03 + "EET", // Eastern European Time UTC+02 + "EGST", // Eastern Greenland Summer Time UTC±00 + "EGT", // Eastern Greenland Time UTC−01 + "EST", // Eastern Standard Time (North America) UTC−05 + "ET", + "EST", + "EDT", // Eastern Time (North America) UTC−05 / UTC−04 + "FET", // Further-eastern European Time UTC+03 + "FJT", // Fiji Time UTC+12 + "FKST", // Falkland Islands Summer Time UTC−03 + "FKT", // Falkland Islands Time UTC−04 + "FNT", // Fernando de Noronha Time UTC−02 + "GALT", // Galápagos Time UTC−06 + "GAMT", // Gambier Islands Time UTC−09 + "GET", // Georgia Standard Time UTC+04 + "GFT", // French Guiana Time UTC−03 + "GILT", // Gilbert Island Time UTC+12 + "GIT", // Gambier Island Time UTC−09 + "GMT", // Greenwich Mean Time UTC±00 + "GST", // South Georgia and the South Sandwich Islands Time UTC−02 + "GST", // Gulf Standard Time UTC+04 + "GYT", // Guyana Time UTC−04 + "HDT", // Hawaii–Aleutian Daylight Time UTC−09 + "HAEC", // Heure Avancée d'Europe Centrale French-language name for CEST UTC+02 + "HST", // Hawaii–Aleutian Standard Time UTC−10 + "HKT", // Hong Kong Time UTC+08 + "HMT", // Heard and McDonald Islands Time UTC+05 + "HOVST", // Hovd Summer Time (not used from 2017-present) UTC+08 + "HOVT", // Hovd Time UTC+07 + "ICT", // Indochina Time UTC+07 + "IDLW", // International Date Line West time zone UTC−12 + "IDT", // Israel Daylight Time UTC+03 + "IOT", // Indian Ocean Time UTC+03 + "IRDT", // Iran Daylight Time UTC+04:30 + "IRKT", // Irkutsk Time UTC+08 + "IRST", // Iran Standard Time UTC+03:30 + "IST", // Indian Standard Time UTC+05:30 + "IST", // Irish Standard Time[8] UTC+01 + "IST", // Israel Standard Time UTC+02 + "JST", // Japan Standard Time UTC+09 + "KALT", // Kaliningrad Time UTC+02 + "KGT", // Kyrgyzstan Time UTC+06 + "KOST", // Kosrae Time UTC+11 + "KRAT", // Krasnoyarsk Time UTC+07 + "KST", // Korea Standard Time UTC+09 + "LHST", // Lord Howe Standard Time UTC+10:30 + "LHST", // Lord Howe Summer Time UTC+11 + "LINT", // Line Islands Time UTC+14 + "MAGT", // Magadan Time UTC+12 + "MART", // Marquesas Islands Time UTC−09:30 + "MAWT", // Mawson Station Time UTC+05 + "MDT", // Mountain Daylight Time (North America) UTC−06 + "MET", // Middle European Time (same zone as CET) UTC+01 + "MEST", // Middle European Summer Time (same zone as CEST) UTC+02 + "MHT", // Marshall Islands Time UTC+12 + "MIST", // Macquarie Island Station Time UTC+11 + "MIT", // Marquesas Islands Time UTC−09:30 + "MMT", // Myanmar Standard Time UTC+06:30 + "MSK", // Moscow Time UTC+03 + "MST", // Malaysia Standard Time UTC+08 + "MST", // Mountain Standard Time (North America) UTC−07 + "MUT", // Mauritius Time UTC+04 + "MVT", // Maldives Time UTC+05 + "MYT", // Malaysia Time UTC+08 + "NCT", // New Caledonia Time UTC+11 + "NDT", // Newfoundland Daylight Time UTC−02:30 + "NFT", // Norfolk Island Time UTC+11 + "NOVT", // Novosibirsk Time [9] UTC+07 + "NPT", // Nepal Time UTC+05:45 + "NST", // Newfoundland Standard Time UTC−03:30 + "NT", // Newfoundland Time UTC−03:30 + "NUT", // Niue Time UTC−11 + "NZDT", // New Zealand Daylight Time UTC+13 + "NZST", // New Zealand Standard Time UTC+12 + "OMST", // Omsk Time UTC+06 + "ORAT", // Oral Time UTC+05 + "PDT", // Pacific Daylight Time (North America) UTC−07 + "PET", // Peru Time UTC−05 + "PETT", // Kamchatka Time UTC+12 + "PGT", // Papua New Guinea Time UTC+10 + "PHOT", // Phoenix Island Time UTC+13 + "PHT", // Philippine Time UTC+08 + "PHST", // Philippine Standard Time UTC+08 + "PKT", // Pakistan Standard Time UTC+05 + "PMDT", // Saint Pierre and Miquelon Daylight Time UTC−02 + "PMST", // Saint Pierre and Miquelon Standard Time UTC−03 + "PONT", // Pohnpei Standard Time UTC+11 + "PST", // Pacific Standard Time (North America) UTC−08 + "PWT", // Palau Time[10] UTC+09 + "PYST", // Paraguay Summer Time[11] UTC−03 + "PYT", // Paraguay Time[12] UTC−04 + "RET", // Réunion Time UTC+04 + "ROTT", // Rothera Research Station Time UTC−03 + "SAKT", // Sakhalin Island Time UTC+11 + "SAMT", // Samara Time UTC+04 + "SAST", // South African Standard Time UTC+02 + "SBT", // Solomon Islands Time UTC+11 + "SCT", // Seychelles Time UTC+04 + "SDT", // Samoa Daylight Time UTC−10 + "SGT", // Singapore Time UTC+08 + "SLST", // Sri Lanka Standard Time UTC+05:30 + "SRET", // Srednekolymsk Time UTC+11 + "SRT", // Suriname Time UTC−03 + "SST", // Samoa Standard Time UTC−11 + "SST", // Singapore Standard Time UTC+08 + "SYOT", // Showa Station Time UTC+03 + "TAHT", // Tahiti Time UTC−10 + "THA", // Thailand Standard Time UTC+07 + "TFT", // French Southern and Antarctic Time[13] UTC+05 + "TJT", // Tajikistan Time UTC+05 + "TKT", // Tokelau Time UTC+13 + "TLT", // Timor Leste Time UTC+09 + "TMT", // Turkmenistan Time UTC+05 + "TRT", // Turkey Time UTC+03 + "TOT", // Tonga Time UTC+13 + "TST", // Taiwan Standard Time UTC+08 + "TVT", // Tuvalu Time UTC+12 + "ULAST", // Ulaanbaatar Summer Time UTC+09 + "ULAT", // Ulaanbaatar Standard Time UTC+08 + "UTC", // Coordinated Universal Time UTC±00 + "UYST", // Uruguay Summer Time UTC−02 + "UYT", // Uruguay Standard Time UTC−03 + "UZT", // Uzbekistan Time UTC+05 + "VET", // Venezuelan Standard Time UTC−04 + "VLAT", // Vladivostok Time UTC+10 + "VOLT", // Volgograd Time UTC+03 + "VOST", // Vostok Station Time UTC+06 + "VUT", // Vanuatu Time UTC+11 + "WAKT", // Wake Island Time UTC+12 + "WAST", // West Africa Summer Time UTC+02 + "WAT", // West Africa Time UTC+01 + "WEST", // Western European Summer Time UTC+01 + "WET", // Western European Time UTC±00 + "WIB", // Western Indonesian Time UTC+07 + "WIT", // Eastern Indonesian Time UTC+09 + "WITA", // Central Indonesia Time UTC+08 + "WGST", // West Greenland Summer Time[14] UTC−02 + "WGT", // West Greenland Time[15] UTC−03 + "WST", // Western Standard Time UTC+08 + "YAKT", // Yakutsk Time UTC+09 + "YEKT", // Yekaterinburg Time + ) + lazy val NumberPattern = "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)".r + + // quick heuristic rejection of non-date strings + def isPossibleDateString(s: String): Boolean = { + + def isDecimalNum: Boolean = NumberPattern.matches(s) && s.replaceAll("[^.]+", "").length == 1 + + if (s.length < 4 || s.length > 35 || isDecimalNum ) { + false + } else { + val lc = s.toLowerCase. + replaceAll("[jfmasond][aerpuco][nbrylgptvc][uaryrchilestmbo]{0,6}", "1"). + replaceAll("[mtwfs][ouehra][neduit][daysneru]{0,4}", "2") + + var digits = 0 + var nondigits = 0 + var bogus = 0 + val validchars = lc.filter { (c: Char) => + c match { + case c if c >= '0' && c <= '9' => + digits += 1 + true + case ':' | '-' | '.' | '/' => + nondigits += 1 + true + case _ => + bogus += 1 + false + } } - val standardOrder = List(nums(iy), nums(im), nums(id)) ++ nums.drop(3) - fromStandardOrder(standardOrder) + val density = 100.0 * validchars.size.toDouble / s.length.toDouble + val proportion = 100.0 * nondigits.toDouble / (digits+1.0) + digits >= 3 && digits <= 19 && density >= 30.0 && proportion < 35.0 } - val dateTime: LocalDateTime = bareformats match { - case "d" :: "M" :: "y" :: tail => ymd(2,1,0, tail) - case "M" :: "d" :: "y" :: tail => ymd(2,0,1, tail) - case "d" :: "y" :: "M" :: tail => ymd(1,2,0, tail) - case "M" :: "y" :: "d" :: tail => ymd(1,0,2, tail) - case "y" :: "d" :: "M" :: tail => ymd(0,2,1, tail) - case "y" :: "M" :: "d" :: tail => ymd(0,1,2, tail) - case other => - valid = false - BadDate + } + def monthAbbrev2Number(name: String): Int = { + name.toLowerCase.substring(0, 3) match { + case "jan" => 1 + case "feb" => 2 + case "mar" => 3 + case "apr" => 4 + case "may" => 5 + case "jun" => 6 + case "jul" => 7 + case "aug" => 8 + case "sep" => 9 + case "oct" => 10 + case "nov" => 11 + case "dec" => 12 + case _ => + hook += 1 + -1 } - new DateFlds(dateTime, rawdatetime, numerified, timezone, formats, valid) } } -case class DateFlds(dateTime: LocalDateTime, rawdatetime: String, numerified: String, timezone: String, formats: Seq[String], valid: Boolean) { +// TODO: use timezone info, including CST, etc +case class ChronoParse(dateTime: LocalDateTime, rawdatetime: String, timezone: String, formats: Seq[String], valid: Boolean) { + import vastblue.time.TimeDate.* override def toString: String = dateTime.toString("yyyy-MM-dd HH:mm:ss") } diff --git a/jsrc/cmdInfo.sc b/jsrc/cmdInfo.sc index d031d84..0d1f74e 100755 --- a/jsrc/cmdInfo.sc +++ b/jsrc/cmdInfo.sc @@ -1,13 +1,13 @@ #!/usr/bin/env -S scala -import vastblue.pallet._ +import vastblue.pallet.* def main(args: Array[String]): Unit = { printf("# args [%s]\n", args.toSeq.mkString("|")) for ((arg, i) <- args.zipWithIndex) { printf("A: args(%d) == [%s]\n", i, arg) } - val argv = prepArgs(args.toSeq) + val argv = prepArgv(args) printf("\n# argv [%s]\n", argv.toSeq.mkString("|")) for ((arg, i) <- argv.zipWithIndex) { diff --git a/jsrc/csvWriteRead.sc b/jsrc/csvWriteRead.sc index 09662d0..3b00e20 100644 --- a/jsrc/csvWriteRead.sc +++ b/jsrc/csvWriteRead.sc @@ -1,5 +1,5 @@ #!/usr/bin/env -S scala -cp target/scala-3.3.1/classes -package vastblue.demo +//package vastblue.examples import vastblue.pallet.* diff --git a/jsrc/demo.sc b/jsrc/demo.sc index 0aa612f..a946950 100755 --- a/jsrc/demo.sc +++ b/jsrc/demo.sc @@ -1,5 +1,5 @@ #!/usr/bin/env -S scala @./atFile -package vastblue.demo +//package vastblue.examples import vastblue.pallet.* import vastblue.MainArgs diff --git a/jsrc/dummyCmdline.sc b/jsrc/dummyCmdline.sc index be8d169..764ffad 100755 --- a/jsrc/dummyCmdline.sc +++ b/jsrc/dummyCmdline.sc @@ -1,23 +1,26 @@ #!/usr/bin/env -S scala +//package vastblue.demo -import vastblue.ScriptInfo.legalMainClass +import vastblue.Script.* -// test code to skip java portion of "sun.command.line" property -// especially legalMainClass(s) -def main(args: Array[String]): Unit = - for ((arg, i) <- dummyArgs.zipWithIndex) { - val legal = legalMainClass(arg) - if (legal) { - printf(" %2d: [%s] : [%s]\n", i, legal, arg) +object DummyCmdline { + // test code to skip java portion of "sun.command.line" property + // especially validScriptOrClassName(s) + def main(args: Array[String]): Unit = + for ((arg, i) <- dummyArgs.zipWithIndex) { + val legal = validScriptOrClassName(arg) + if (legal) { + printf(" %2d: [%s] : [%s]\n", i, legal, arg) + } + } + val scriptProp = "jsrc/globArg.sc" + val scriptArgs = dummyArgs.dropWhile(s => !s.endsWith(scriptProp) && !validScriptOrClassName(s)) + printf("\n") + for (arg <- scriptArgs) { + printf(" [%s]\n", arg) } - } - val scriptProp = "jsrc/globArg.sc" - val scriptArgs = dummyArgs.dropWhile(s => !s.endsWith(scriptProp) && !legalMainClass(s)) - printf("\n") - for (arg <- scriptArgs) { - printf(" [%s]\n", arg) - } -lazy val dummyArgs: Array[String] = - """C:\opt\jdk\bin\java.exe^@-Dscala.home=C:/opt/scala^@-classpath^@C:/opt/scala/lib/scala-library-2.13.10.jar;C:/opt/scala/lib/scala3-library_3-3.3.1.jar;C:/opt/scala/lib/scala-asm-9.5.0-scala-1.jar;C:/opt/scala/lib/compiler-interface-1.3.5.jar;C:/opt/scala/lib/scala3-interfaces-3.3.1.jar;C:/opt/scala/lib/scala3-compiler_3-3.3.1.jar;C:/opt/scala/lib/tasty-core_3-3.3.1.jar;C:/opt/scala/lib/scala3-staging_3-3.3.1.jar;C:/opt/scala/lib/scala3-tasty-inspector_3-3.3.1.jar;C:/opt/scala/lib/jline-reader-3.19.0.jar;C:/opt/scala/lib/jline-terminal-3.19.0.jar;C:/opt/scala/lib/jline-terminal-jna-3.19.0.jar;C:/opt/scala/lib/jna-5.3.1.jar;;^@dotty.tools.MainGenericRunner^@-classpath^@C:/opt/scala/lib/scala-library-2.13.10.jar;C:/opt/scala/lib/scala3-library_3-3.3.1.jar;C:/opt/scala/lib/scala-asm-9.5.0-scala-1.jar;C:/opt/scala/lib/compiler-interface-1.3.5.jar;C:/opt/scala/lib/scala3-interfaces-3.3.1.jar;C:/opt/scala/lib/scala3-compiler_3-3.3.1.jar;C:/opt/scala/lib/tasty-core_3-3.3.1.jar;C:/opt/scala/lib/scala3-staging_3-3.3.1.jar;C:/opt/scala/lib/scala3-tasty-inspector_3-3.3.1.jar;C:/opt/scala/lib/jline-reader-3.19.0.jar;C:/opt/scala/lib/jline-terminal-3.19.0.jar;C:/opt/scala/lib/jline-terminal-jna-3.19.0.jar;C:/opt/scala/lib/jna-5.3.1.jar;;^@-cp^@target/scala-3.3.1/classes^@jsrc/globArg.sc^@*.sc""" - .split("\\^@") + lazy val dummyArgs: Array[String] = + """C:\opt\jdk\bin\java.exe^@-Dscala.home=C:/opt/scala^@-classpath^@C:/opt/scala/lib/scala-library-2.13.10.jar;C:/opt/scala/lib/scala3-library_3-3.3.1.jar;C:/opt/scala/lib/scala-asm-9.5.0-scala-1.jar;C:/opt/scala/lib/compiler-interface-1.3.5.jar;C:/opt/scala/lib/scala3-interfaces-3.3.1.jar;C:/opt/scala/lib/scala3-compiler_3-3.3.1.jar;C:/opt/scala/lib/tasty-core_3-3.3.1.jar;C:/opt/scala/lib/scala3-staging_3-3.3.1.jar;C:/opt/scala/lib/scala3-tasty-inspector_3-3.3.1.jar;C:/opt/scala/lib/jline-reader-3.19.0.jar;C:/opt/scala/lib/jline-terminal-3.19.0.jar;C:/opt/scala/lib/jline-terminal-jna-3.19.0.jar;C:/opt/scala/lib/jna-5.3.1.jar;;^@dotty.tools.MainGenericRunner^@-classpath^@C:/opt/scala/lib/scala-library-2.13.10.jar;C:/opt/scala/lib/scala3-library_3-3.3.1.jar;C:/opt/scala/lib/scala-asm-9.5.0-scala-1.jar;C:/opt/scala/lib/compiler-interface-1.3.5.jar;C:/opt/scala/lib/scala3-interfaces-3.3.1.jar;C:/opt/scala/lib/scala3-compiler_3-3.3.1.jar;C:/opt/scala/lib/tasty-core_3-3.3.1.jar;C:/opt/scala/lib/scala3-staging_3-3.3.1.jar;C:/opt/scala/lib/scala3-tasty-inspector_3-3.3.1.jar;C:/opt/scala/lib/jline-reader-3.19.0.jar;C:/opt/scala/lib/jline-terminal-3.19.0.jar;C:/opt/scala/lib/jline-terminal-jna-3.19.0.jar;C:/opt/scala/lib/jna-5.3.1.jar;;^@-cp^@target/scala-3.3.1/classes^@jsrc/globArg.sc^@*.sc""" + .split("\\^@") +} diff --git a/jsrc/find.sc b/jsrc/find.sc index 49b0087..28a7162 100755 --- a/jsrc/find.sc +++ b/jsrc/find.sc @@ -1,5 +1,5 @@ #!/usr/bin/env -S scala @${HOME}/.scala3cp -package vastblue.demo +//package vastblue.examples // hash bang line error on OSX/Darwin due to non-gnu /usr/bin/env // portable way to set classpath: diff --git a/jsrc/fstabCli.sc b/jsrc/fstabCli.sc index c1bf2d1..dcac9b1 100755 --- a/jsrc/fstabCli.sc +++ b/jsrc/fstabCli.sc @@ -1,16 +1,20 @@ #!/usr/bin/env -S scala-cli shebang //> using scala "3.3.1" -//> using lib "org.vastblue::pallet::0.10.6" +//> using lib "org.vastblue::unifile::0.3.1" +//> using lib "org.vastblue::pallet::0.10.7" -import vastblue.pallet._ +//import vastblue.pallet.* +import vastblue.unifile.* object FstabCli { def main(args: Array[String]): Unit = { - // `posixroot` is the native path corresponding to "/" + // `shellRoot` is the native path corresponding to "/" // display the native path and lines.size of /etc/fstab val p = Paths.get("/etc/fstab") // format: off + printf("env: %-10s| shellRoot: %-12s| %-22s| %d lines\n", + uname("-o"), shellRoot, p.posx, p.lines.size) printf("env: %-10s| posixroot: %-12s| %-22s| %d lines\n", uname("-o"), posixroot, p.posx, p.lines.size) } diff --git a/jsrc/globArg.sc b/jsrc/globArg.sc index c2eebcc..30a34a3 100755 --- a/jsrc/globArg.sc +++ b/jsrc/globArg.sc @@ -1,5 +1,5 @@ #!/usr/bin/env -S scala -cp target/scala-3.3.1/classes -package vastblue.demo +//package vastblue.examples import vastblue.pallet.* diff --git a/jsrc/isWinJunctionPoint.sc b/jsrc/isWinJunctionPoint.sc index d60cf2b..0fa37dd 100644 --- a/jsrc/isWinJunctionPoint.sc +++ b/jsrc/isWinJunctionPoint.sc @@ -1,12 +1,12 @@ #!/usr/bin/env -S scala -package vastblue.demo +//package vastblue.examples import vastblue.pallet.* object IsWinJunctionPoint { def main(args: Array[String]): Unit = for (arg <- args){ - val (flag, target) = vastblue.Stuff.isWindowsJunction(arg) + val (flag, target) = vastblue.file.Util.isWindowsJunction(arg) if (flag) { printf("points to [%s]\n", target) } else { diff --git a/jsrc/mainName.sc b/jsrc/mainName.sc index ec7e360..9710934 100755 --- a/jsrc/mainName.sc +++ b/jsrc/mainName.sc @@ -1,5 +1,5 @@ #!/usr/bin/env -S scala @./atFile -deprecation -package vastblue.demo +//package vastblue.examples import vastblue.pallet.* diff --git a/jsrc/ownPid.sc b/jsrc/ownPid.sc index b8d5e21..600b711 100755 --- a/jsrc/ownPid.sc +++ b/jsrc/ownPid.sc @@ -1,5 +1,5 @@ #!/usr/bin/env -S scala @./.scala3cp -deprecation -package vastblue +//package vastblue import vastblue.pallet._ import vastblue.MainArgs @@ -7,7 +7,7 @@ import vastblue.MainArgs object OwnPid { def main(args: Array[String]): Unit = { try { - val argv = MainArgs.prepArgs(args.toSeq) + val argv = MainArgs.prepArgv(args.toSeq) for ((arg, i) <- argv.zipWithIndex) { printf("%2d: [%s]\n", i, arg) } diff --git a/jsrc/palletRef.sc b/jsrc/palletRef.sc index 9b6bdf1..08a7a76 100755 --- a/jsrc/palletRef.sc +++ b/jsrc/palletRef.sc @@ -1,7 +1,7 @@ #!/usr/bin/env -S scala-cli shebang //> using scala "3.3.1" -//> using lib "org.vastblue::pallet::0.10.6" +//> using lib "org.vastblue::pallet::0.10.7" import vastblue.pallet._ diff --git a/jsrc/palletRefCli.sc b/jsrc/palletRefCli.sc index bd0ec4d..061bcab 100755 --- a/jsrc/palletRefCli.sc +++ b/jsrc/palletRefCli.sc @@ -1,7 +1,7 @@ #!/usr/bin/env -S scala-cli shebang //> using scala "3.3.1" -//> using lib "org.vastblue::pallet::0.10.6" +//> using lib "org.vastblue::pallet::0.10.7" import vastblue.pallet.* diff --git a/jsrc/parseDates.sc b/jsrc/parseDates.sc deleted file mode 100644 index 29ce2e1..0000000 --- a/jsrc/parseDates.sc +++ /dev/null @@ -1,351 +0,0 @@ -#!/usr/bin/env -S scala-cli shebang -deprecation - -//> using dep "org.vastblue:pallet_3:0.10.6" -//> using dep "org.vastblue:unifile_3:0.3.0" -//> using dep "org.simpleflatmapper:sfm-csv-jre6:8.2.3" -//> using dep "io.github.chronoscala::chronoscala::2.0.10" -//> using dep "com.github.sisyphsu:dateparser:1.0.11" - -import vastblue.pallet.* -import vastblue.time.TimeDate.* -import vastblue.time.TimeParser -import com.github.sisyphsu.dateparser.* -import java.time.LocalDateTime -import java.time.format.DateTimeParseException - -ParseDates.main(args) -object ParseDates { - var monthFirst = true // by default, prefer US format - - def usage(m: String=""): Nothing = { - _usage(m, Seq( - "[] ; one datetime string per line", - "[-test | -flds] ; verify testdate.csv conversions", - "[-df] ; prefer day-first format (non-US)", - "by default, op == \"-flds\"", - )) - } - var (op, verbose, infiles) = ("", false, Vector.empty[Path]) - - def main(args: Array[String]): Unit = { - parseArgs(args.toSeq) - DateParserUtils.preferMonthFirst(monthFirst) - try { - op match { - case "" => - verifyFields(testDataFile) - case "-test" => - verifyConversions(testDataFile) - case "-file" => - for (p <- infiles) { - convertEntries(p) - } - } - } catch { - case t: Throwable => - showLimitedStack(t) - sys.exit(3) - } - } - - def parseArgs(args: Seq[String]): Unit = { - eachArg(args.toSeq, usage) { - case f if f.path.isFile => - assert(op.isEmpty, s"op[$op] but also specified file [$f]") - op = "-file" - infiles :+= f.path - case "-v" => - verbose = true - case "-df" => - monthFirst = false - case "-test" | "-flds" => - if (!testDataFile.isFile) { - usage(s"not found: ${testDataFile.posx}") - } else { - op = thisArg - } - case arg => - usage(s"unrecognized arg [$arg]") - } - } - def verifyFields(file: Path): Unit = { - if (file.isFile) { - val rows = file.csvRows.drop(1) // discard column names - eprintf("%d rows\n", rows.size) - for ((row, i) <- rows.zipWithIndex){ - printf("%04d : %s\n", i, row.mkString("|")) - val Seq(expected, rawline) = row - val format = DateFlds(rawline) - printf("rawlin: [%s]\n", rawline) - printf(" format: [%s]\n", format) - printf(" expect: [%s]\n", expected) - } - } - } - def verifyConversions(file: Path): Unit = { - if (file.isFile) { - val rows = file.csvRows.drop(1) // discard column names - for (row <- rows){ - val Seq(expected, rawline) = row - val dateTime = newDateParser(rawline) - val dtstr = dateTime.toString("yyyy-MM-dd HH:mm:ss") - if (dtstr != expected) { - printf("rawlin: [%s]\n", rawline) - printf(" result: [%s]\n", dtstr) - printf(" expect: [%s]\n", expected) - } - //printf("%s # [%s]\n", dtstr, rawline) - } - } - } - def convertEntries(file: Path): Unit = { - if (file.isFile) { - for (rawline <- file.lines) { - val dateTime = newDateParser(rawline) - val dtstr = dateTime.toString("yyyy-MM-dd HH:mm:ss") - printf("%s # [%s]\n", dtstr, rawline) - } - } - } - lazy val testDataFile = "testdates.csv".path - - lazy val nowdate = now.toString("yyyy-MM-dd") - - def newDateParser(rawline: String): LocalDateTime = { - try { - sysiphus(rawline) - } catch { - case e: java.time.format.DateTimeParseException => - var line = rawline.replaceAll("/{2,}", "/").replaceAll("[\"\\\\]", "") - parseDate(line) - } - } - /* - def fieldTypes(numstrings: List[String]): String = { - val nums: Seq[Int] = numstrings.map { (s: String) => Fld(s) } - val fields: Seq[(String, Int)] = numstrings.zipWithIndex.sortBy { case (str, i) => str } - nums match { - case a :: b :: c :: Nil if a > 1000 => - "yyyyMMdd" - case a :: b :: c :: Nil if a > 12 && c > 1000 => - "ddMMyyyy" - case a :: b :: c :: Nil if c > 1000 => - "MMddyyyy" - case a :: b :: c :: Nil if a > 12 && c > 1000 => - "ddMMyyyy" - } else if (num > 12) { - "d" // day - } else { - "m" // resolve ambiguity in favor of month - } - } - */ - def sysiphus(rawline: String): LocalDateTime = { - var line = rawline - val numstrings = line.split("\\D+").map { _.trim }.filter { _.nonEmpty } - val nums = numstrings.map { _.toInt } - if (verbose) { - printf("%d number fields [%s]\n", numstrings.size, numstrings.mkString("|")) - } - if (numstrings.length == 3) { - if (line.contains(":")) { - // time fields only - DateParserUtils.parseDateTime(s"$nowdate $line") - } else { - // date fields only - val Seq(a, b, c) = nums.toSeq - if (a < 1000) { - // year-first - } else if (a > 12) { - // day-first, swap first two fields - line = "%02d-%02d-%04d".format(b, a, c) - } else { - // assume month-first, but is ambiguous - } - DateParserUtils.parseDateTime(s"$line 00:00:00") - } - } else { - if (verbose) { - eprintf("== [%s]\n", rawline) - } - DateParserUtils.parseDateTime(line) - } - } - - // derive parse format for dates with numeric fields - // preparation must include converting names of day and month to numeric - case class DateFlds(rawdatetime: String) { - val datetime = numerifyNames(rawdatetime) - val numstrings = datetime.replaceAll("\\D+", " ").trim.split(" ").toIndexedSeq - val fields: Seq[(String, Int)] = numstrings.zipWithIndex - def hasTime = datetime.contains(":") - def hasDate = datetime.contains("-") || datetime.contains("/") - - var formats: Array[String] = fields.toArray.map { (s: String, i: Int) => - if (i < 3) { - s.toInt match { - case y if y > 31 || s.length == 4 => - s.replaceAll(".", "y") - case d if d > 12 && s.length <= 2 => - s.replaceAll(".", "d") - case _ => // can't resolve month without more context - s - } - } else { - i match { - case 3 => s.replaceAll(".", "H") - case 4 => s.replaceAll(".", "m") - case 5 => s.replaceAll(".", "s") - case 6 => s.replaceAll(".", "Z") - case _ => - s // not expecting any more numeric fields - } - } - } - - def is(s: String, v: String): Boolean = s.startsWith(v) - - val yidx = formats.indexWhere((s: String) => s.startsWith("y")) - val didx = formats.indexWhere((s: String) => s.startsWith("d")) - val midx = formats.indexWhere((s: String) => s.startsWith("M")) - - def hasY = yidx >= 0 - def hasM = midx >= 0 - def hasD = didx >= 0 - def needsY = yidx < 0 - def needsM = midx < 0 - def needsD = didx < 0 - - var (yval, mval, dval) = (0, 0, 0) - def replaceFirstNumericField(s: String): Unit = { - val i = formats.indexWhere((s: String) => s.matches("[0-9]+")) - assert(i < 3, s"internal error: $datetime [i: $i, s: $s]") - s match { - case "y" => - assert(yval < 1, s"yval: $yval") - yval = formats(i).toInt - case "M" => - assert(mval < 1, s"mval: $mval") - mval = formats(i).toInt - case "d" => - assert(dval < 1, s"dval: $dval") - dval = formats(i).toInt - case _ => - sys.error(s"internal error: bad format indicator [$s]") - } - formats(i) = formats(i).replaceAll("[0-9]", s) - } - - (needsY, needsM, needsD) match { - case (false, false, true) => - replaceFirstNumericField("d") - case (false, true, false) => - replaceFirstNumericField("M") - case (true, false, false) => - replaceFirstNumericField("y") - - case (false, true, true) => - // has year, needs month and day - yidx match { - case 0 => - // y-M-d - replaceFirstNumericField("M") - replaceFirstNumericField("d") - case 2 => - // d-M-y - replaceFirstNumericField("d") - replaceFirstNumericField("M") - } - case (true, true, false) => - // has day, needs month and year - didx match { - case 0 => - // d-M-y - replaceFirstNumericField("M") - replaceFirstNumericField("d") - case 2 => - // y-M-d - replaceFirstNumericField("d") - replaceFirstNumericField("M") - case 1 => - // AMBIGUOUS ... - if (monthFirst) { - // M-d-y - replaceFirstNumericField("d") - replaceFirstNumericField("M") - } else { - // d-M-y - replaceFirstNumericField("M") - replaceFirstNumericField("d") - } - } - case (true, true, true) => - // done with date fields - case (yy, mm, dd) => - sys.error(s"yy[$yy], mm[$mm], dd[$dd] datetime[$datetime], formats[${formats.mkString("|")}]") - } - override def toString = "%s : %s".format(formats.mkString("|"), datetime) - - /* - case (true, false, false) => - - case (false, false, true) => - formats(i) = m.map { _ => "M" } - case (m :: d :: y :: tail), i) if hasY && hasM => - formats(i) = d.map { _ => "d" } - case (m :: d :: y :: tail, i) if hasY => - // by convention (in US): - formats(0) = m.replaceAll(".", "M") - formats(1) = d.replaceAll(".", "d") - - case m :: d :: y :: tail if !hasY => - formats(2) = m.map { _ => "M" } - case m :: d :: y :: tail if hasY && hasM => - formats(1) = d.map { _ => "d" } - case m :: d :: y :: tail if hasY => - // by convention (in US): - formats(0) = m.replaceAll(".", "M") - formats(1) = d.replaceAll(".", "d") - - case m :: d :: y if y.startsWith("y") :: tail => - formats(0) = m.replaceAll(".", "M") - formats(1) = d.replaceAll(".", "M") - */ - /* - def hasy = fmts.contains("y") - for ((str, idx) <- fields) { - val width = str.length - val valu = str.toInt - (valu, idx) match { - case (y, 0 | 2) if y > 31 || width == 4 => - fmts(idx) = "y" * width - - case (d, _) if d > 12 && width <= 2 => - fmts(idx) = "d" * width - - case (m, 0 | 1) if m < 31 && width <= 2 => - fmts(idx) = "M" * width - - case unk => - if (width >= 4) { - "Z" - } else { - sys.error(s"datetime[$datetime] unknown field: [$unk]") - } - } - } - - override def toString: String = { - if (fmts.size <= 3) { - val delim = if (hasTime) { ":" } else { "-" } - fmts.mkString(delim) - } else { - val (dateff, timeff) = fmts.splitAt(3) - val datefmt = dateff.mkString("-") - val timefmt = timeff.mkString(":") - s"$datefmt $timefmt" - } - } - */ - } -} diff --git a/jsrc/pathStrings.sc b/jsrc/pathStrings.sc index 3bbfd8a..7f2c217 100755 --- a/jsrc/pathStrings.sc +++ b/jsrc/pathStrings.sc @@ -7,7 +7,7 @@ object PathStrings { if (args.isEmpty) { printf("usage: %s [ ...]\n", scriptPath.name) } else { - val argv = prepArgs(args.toSeq) + val argv = prepArgv(args.toSeq) for (a <- argv) { printf("========== arg[%s]\n", a) printf("stdpath [%s]\n", Paths.get(a).stdpath) diff --git a/jsrc/platform.sc b/jsrc/platform.sc index e405446..26e654e 100755 --- a/jsrc/platform.sc +++ b/jsrc/platform.sc @@ -1,16 +1,18 @@ #!/usr/bin/env -S scala @./atFile import vastblue.pallet._ -import vastblue.Platform +import vastblue.file.MountMapper def main(args: Array[String]): Unit = + /* if (isDarwin || args.contains("-verbose")) { - Platform.main(args.filter { _ != "-verbose" }) + MountMap.main(args.filter { _ != "-verbose" }) } + */ if (!isDarwin) { - val cygdrivePrefix = Platform.reverseMountMap.get("cygdrive").getOrElse("not-found") + val cygdrivePrefix = MountMapper.reverseMountMap.get("cygdrive").getOrElse("not-found") printf("cygdrivePrefix: [%s]\n", cygdrivePrefix) - for ((k, v) <- Platform.mountMap) { + for ((k, v) <- MountMapper.mountMap) { printf("%-22s: %s\n", k, v) } } diff --git a/jsrc/procCmdline.sc b/jsrc/procCmdline.sc index 9ce8b0b..045f642 100755 --- a/jsrc/procCmdline.sc +++ b/jsrc/procCmdline.sc @@ -1,5 +1,5 @@ #!/usr/bin/env -S scala -deprecation -cp target/scala-3.3.1/classes -package vastblue.demo +//package vastblue.examples import vastblue.pallet.* import vastblue.file.ProcfsPaths.* diff --git a/jsrc/sbt2cs.sc b/jsrc/sbt2cs.sc new file mode 100644 index 0000000..1ade88d --- /dev/null +++ b/jsrc/sbt2cs.sc @@ -0,0 +1,208 @@ +#!/usr/bin/env -S scala +//package vast.apps + +import vastblue.unifile.* + +/* + * Options: + * + fetch and list dependency file paths + * + convert sbt dependencies to scala-cli "using dep" format + * + * Example conversion from sbt dependency format to scala-cli lib format: + * + * from: + * org.vastblue %% unifile % 0.3.1 + * org.vastblue %% pallet % 0.10.7 + * org.scalanlp %% breeze-viz % 2.1.0 + * org.scalanlp %% breeze % 2.1.0 + * org.scala-lang.modules %% scala-xml % 2.2.0 + * org.scala-lang.modules %% scala-swing % 3.0.0 + * net.ruippeixotog %% scala-scraper % 3.1.1 + * dev.ludovic.netlib % blas % 3.0.3 + * com.github.fommil.netlib % all % 1.1.2 + * com.github.darrenjw %% scala-glm % 0.8 + * + * to: + * //> using dep "org.vastblue::pallet::0.10.7" + * //> using dep "org.vastblue::unifile::0.3.1" + * //> using dep "org.scalanlp::breeze-viz::2.1.0" + * //> using dep "org.scalanlp::breeze::2.1.0" + * //> using dep "org.scala-lang.modules::scala-xml::2.2.0" + * //> using dep "org.scala-lang.modules::scala-swing::3.0.0" + * //> using dep "net.ruippeixotog::scala-scraper::3.1.1" + * //> using dep "dev.ludovic.netlib:blas:3.0.3" + * //> using dep "com.github.fommil.netlib:all:1.1.2" + * //> using dep "com.github.darrenjw::scala-glm::0.8" +*/ +object Sbt2cs { + def usage(m: String=""): Nothing = { + _usage(m, Seq( + "[]", + s"[-sbt [] ; default: ${defaultSbtSource}", + s"[-cli] ; convert sbt deps to scala-cli format", + s"[-fetch] ; fetch and list lib jars", + )) + } + var (op, inputFile) = ("", "") + + def main(args: Array[String]): Unit = { + try { + parseArgs(args.toSeq) + val entries = if (inputFile.nonEmpty && inputFile.path.isFile) { + val infile = inputFile.path + val cleaned = extractDeps(infile) + preProcessDeps(cleaned) + } else { + preProcessDeps(testData) + } + val deps = entries.map { SbtDep(_) } + op match { + case "" | "-fetch" => + for (dep <- deps) { + for (line <- fetch(dep)) { + printf("%s\n", line.posx) + } + } + case "-cli" => + for (line <- deps) { + printf("%s\n", asDep(line)) + } + } + } catch { + case t: Throwable => + showLimitedStack(t) + sys.exit(1) + } + } + + lazy val testData = """ +org.scalanlp %% breeze % 2.1.0 +org.scala-lang.modules %% scala-xml % 2.2.0 +org.scalanlp %% breeze % 2.1.0 +org.scalanlp %% breeze-viz % 2.1.0 +dev.ludovic.netlib % blas % 3.0.3 +com.github.fommil.netlib % all % 1.1.2 +com.github.darrenjw %% scala-glm % 0.8 +org.scala-lang.modules %% scala-swing % 3.0.0 +net.ruippeixotog %% scala-scraper % 3.1.1 +org.vastblue %% unifile % 0.3.1 +org.vastblue %% pallet % 0.10.7 +""".trim.split("[\r\n]+").toList.filter { _.nonEmpty } + + def parseArgs(args: Seq[String]): Unit = { + eachArg(args, usage) { + case "-cli" | "-fetch" => + op = thisArg + case "-sbt" => + if (peekNext.nonEmpty && !peekNext.startsWith("-") ) { + inputFile = consumeNext + if (!inputFile.path.isFile) { + usage(s"not found: ${inputFile}") + } + } else { + inputFile = defaultSbtSource + } + case f if !f.startsWith("-") && f.path.isFile => + inputFile = f + case arg => + usage(s"unrecognized arg [$arg]") + } + } + + val defaultSbtSource = "/opt/ue/project/Dependencies.scala" + + lazy val cs: String = find("cs") + lazy val repos = Seq( + "https://artifacts.alfresco.com/nexus/content/repositories/public/", // ldtp + "https://mvnrepository.com/artifact/com.snowtide", + "http://maven.snowtide.com/releases", + ).mkString("|") + + def fetch(dep: SbtDep): Seq[String] = { + val depstr = dep.toString + if (dep.line.contains("ldtp")) { + val cmd = s"$cs fetch $depstr" + shellExec(cmd, Map("COURSIER_REPOSITORIES" -> repos)) + } else { + execLines("cs", "fetch", depstr) + } + } + + def asDep(image: SbtDep): String = { + s"//> using dep \"$image\"" + } + + case class SbtDep(_line: String) { + def doublePercent = _line.contains("%%") + val line = _line.replaceAll("[,\"]+", "") + val fields = line.split(" *%+ *").toList + val image = if (doublePercent) { + fields.mkString("::") + } else { + fields.mkString(":") + } + override def toString = image // s"//> using dep \"$image\"" + } + + def linesWithoutComments(p: Path): Seq[String] = { + var worklines = p.lines.map { line => + // avoid treating https://blah-blah as containing // comment + s" $line" + .replaceAll("[^:]//.*", "") + .replaceAll(" *(test|withSources).*", "") + .replaceAll("[()\"]+", "") + .trim + }.filter { test => + (test.contains("%") || test.trim.matches("^lazy val.*[0-9]")) && + !test.contains("printf") && + !test.contains("::Test") && + !test.contains("scalacheck") && + !test.contains("scalatest") && + !test.contains("junit") + } + worklines + } + + def extractDeps(infile: Path): Seq[String] = { + val filtered = linesWithoutComments(infile) + var (lazyvals, lines) = filtered.partition( _.startsWith("lazy val") ) + + var lazyvalMap = Map.empty[String, String] + lazyvals.foreach { (line: String) => + val Seq(name, value) = line.split(" *= *").map { _.replaceAll("^lazy val *", "").replaceAll("[\"()]+", "") }.toSeq + // printf("[%s], [%s]\n", name, value) + lazyvalMap += (name -> value) + } + val valnames = lazyvalMap.keySet + def applyMap(line: String): String = { + val mapKey = valnames.find( (name: String) => line.contains(name) ).getOrElse("") + if (mapKey.nonEmpty) { + val mapValue = lazyvalMap(mapKey) + line.replace(mapKey, mapValue) + } else { + line + } + } + + val cleaned = for { + line <- lines + fixed = applyMap(line) + } yield fixed + cleaned + } + + def preProcessDeps(cleaned: Seq[String]): Seq[String] = { + val sorted = cleaned.sortWith ((a, b) => + if (a.contains("unifile")) true else + if (a.contains("pallet")) true else + if (a.contains("vast")) true else + if (a.contains("apps")) true else + if (b.contains("unifile")) false else + if (b.contains("pallet")) false else + if (b.contains("vast")) false else + if (a.contains("apps")) false else + a > b // reverse (unifile before pallet) + ) + sorted.distinct + } +} diff --git a/jsrc/showMainArgs.sc b/jsrc/showMainArgs.sc index 092bd9d..643cebd 100755 --- a/jsrc/showMainArgs.sc +++ b/jsrc/showMainArgs.sc @@ -1,5 +1,5 @@ #!/usr/bin/env -S scala @./atFile -package vastblue +//package vastblue import vastblue.pallet._ @@ -7,7 +7,7 @@ def main(args: Array[String]): Unit = { for (arg <- args) { printf("arg [%s]\n", arg) } - val argv = prepArgs(args.toSeq) + val argv = prepArgv(args.toSeq) for ((arg, i) <- argv.zipWithIndex) { printf(" %2d: [%s]\n", i, arg) } diff --git a/jsrc/showPlatform.sc b/jsrc/showPlatform.sc index 0c5c386..2d896ea 100755 --- a/jsrc/showPlatform.sc +++ b/jsrc/showPlatform.sc @@ -1,8 +1,50 @@ #!/usr/bin/env -S scala @./atFile +//package vastblue.demo -import vastblue.pallet._ +import vastblue.Platform.{cygpathExe, cygpathM, etcdir, exeSuffix, findAllInPath, winshellBinDirs} +import vastblue.pallet.* -// display discovered aspects of the runtime environment -def main(args: Array[String]): Unit = { - vastblue.Platform.main(args) +object ShowPlatform { + // display discovered aspects of the runtime environment + def main(args:Array[String]):Unit = { + for (root <- winshellBinDirs){ + printf("available installed posix environment root: %s\n", root) + } + printf("whichExe [%s]\n", whichExe) + for( f <- findAllInPath("bash") ){ + printf("found: [%s]\n", f) + } + printf("scriptName [%s]\n", scriptName) + printf("scriptPath [%s]\n", scriptPath) + printf("osName [%s]\n", osName) + printf("osType [%s]\n", osType) + printf("shellRoot [%s]\n", shellRoot) + printf("unameLong [%s]\n", unameLong) + printf("unameShort [%s]\n", unameShort) + printf("javaHome [%s]\n", javaHome) + printf("exeSuffix [%s]\n", exeSuffix) + printf("isCygwin [%s]\n", isCygwin) + printf("isMsys [%s]\n", isMsys) + printf("isGitSdk [%s]\n", isGitSdk) + printf("bashPath [%s]\n", bashPath) + printf("bashExe [%s]\n", bashExe) + printf("cygpathExe [%s]\n", cygpathExe) + printf("whichExe [%s]\n", whichExe) + printf("hostname [%s]\n", hostname) + printf("etcdir [%s]\n", etcdir) + val ls = where("ls") + printf("which ls [%s]\n", ls.path.stdpath) + val lspath = cygpathM(ls) + printf("cygpath ls [%s]\n", lspath) + } + lazy val prognames = Seq( + "bash", + "cat", + "find", + "which", + "uname", + "ls", + "tr", + "ps", + ) } diff --git a/jsrc/testWinJunctionPoint.sc b/jsrc/testWinJunctionPoint.sc index 61dbdff..e2de960 100644 --- a/jsrc/testWinJunctionPoint.sc +++ b/jsrc/testWinJunctionPoint.sc @@ -1,5 +1,5 @@ #!/usr/bin/env -S scala -package vastblue.demo +//package vastblue.examples import vastblue.pallet.* @@ -13,7 +13,7 @@ object TestWinJunctionPoint { }.map { _.path }.filter { _.exists } for (arg <- args){ - val (flag, target) = vastblue.Stuff.isWindowsJunction(arg) + val (flag, target) = vastblue.file.Util.isWindowsJunction(arg) if (flag) { printf("points to [%s]\n", target) } else { diff --git a/jsrc/unameGreeting.sc b/jsrc/unameGreeting.sc index bca131c..0c2cbf0 100644 --- a/jsrc/unameGreeting.sc +++ b/jsrc/unameGreeting.sc @@ -1,7 +1,7 @@ #!/usr/bin/env -S scala //> using scala "3.3.1" -//> using lib "org.vastblue::pallet::0.10.6" +//> using lib "org.vastblue::pallet::0.10.7" import vastblue.pallet.* diff --git a/jsrc/wait.sc b/jsrc/wait.sc index 241612c..10db120 100755 --- a/jsrc/wait.sc +++ b/jsrc/wait.sc @@ -1,5 +1,5 @@ #!/usr/bin/env -S scala -package vastblue +//package vastblue import vastblue.pallet._ diff --git a/src/main/scala/Info.scala b/src/main/scala/pallet/Info.scala similarity index 100% rename from src/main/scala/Info.scala rename to src/main/scala/pallet/Info.scala diff --git a/src/main/scala/vastblue/CsvWriteRead.scala b/src/main/scala/vastblue/examples/CsvWriteRead.scala similarity index 97% rename from src/main/scala/vastblue/CsvWriteRead.scala rename to src/main/scala/vastblue/examples/CsvWriteRead.scala index cfa64d8..6291760 100644 --- a/src/main/scala/vastblue/CsvWriteRead.scala +++ b/src/main/scala/vastblue/examples/CsvWriteRead.scala @@ -1,5 +1,5 @@ //#!/usr/bin/env -S scala -cp target/scala-3.3.1/classes -package vastblue.demo +package vastblue.examples import vastblue.pallet.* diff --git a/src/main/scala/vastblue/ProcCmdline.scala b/src/main/scala/vastblue/examples/ProcCmdline.scala similarity index 87% rename from src/main/scala/vastblue/ProcCmdline.scala rename to src/main/scala/vastblue/examples/ProcCmdline.scala index 0e75717..500f16e 100644 --- a/src/main/scala/vastblue/ProcCmdline.scala +++ b/src/main/scala/vastblue/examples/ProcCmdline.scala @@ -1,5 +1,5 @@ -//#!/usr/bin/env -S scala -deprecation -cp target/scala-3.3.1/classes -package vastblue.demo +//#!/usr/bin/env -S scala -cp target/scala-3.3.1/classes +package vastblue.examples import vastblue.pallet.* import vastblue.file.ProcfsPaths.* diff --git a/src/main/scala/vastblue/file/ProcfsPaths.scala b/src/main/scala/vastblue/file/ProcfsPaths.scala deleted file mode 100644 index 4f8790f..0000000 --- a/src/main/scala/vastblue/file/ProcfsPaths.scala +++ /dev/null @@ -1,123 +0,0 @@ -package vastblue.file - -import vastblue.Platform.* -import vastblue.unifile.posx - -import java.io.File as JFile -import java.nio.file.Path as JPath -import java.nio.file.{Files as JFiles, Paths as JPaths} -import scala.collection.immutable.ListMap -import scala.util.control.Breaks.* -import java.io.{BufferedReader, FileReader} -import scala.util.Using -import scala.sys.process.* -import scala.jdk.CollectionConverters.* - -object ProcfsPaths { - private var hook = 0 - def hasProcfs: Boolean = _isLinux || _isWinshell - - def rootSeg(s: String): String = { - val posix = posx(s) - if (!posix.startsWith("/")) "" else posix.drop(1).split("/").head - } - def pathSegs(s: String): List[String] = { - val posix = posx(s) - if (posix.startsWith("/")) { - posix.drop(1).split("/").toList - } else { - "." :: posix.drop(1).split("/").toList - } - } - - def isProcfs(s: String): Boolean = hasProcfs && rootSeg(s) == "proc" - -// val procFiles = Seq( -// "/proc/cpuinfo", -// "/proc/devices", -// "/proc/filesystems", -// "/proc/loadavg", -// "/proc/meminfo", -// "/proc/misc", -// "/proc/partitions", -// "/proc/stat", -// "/proc/swaps", -// "/proc/uptime", -// "/proc/version", -// ) - - // List dirs && files below a procfs directory. - // conventions: - // list dirs first, then files - // append trailing slash to dirs - // - // Input and output are strings. - def procfsSubfiles(procdir: String): Seq[String] = { - if ((_isWinshell || _isLinux) && procdir.startsWith("/proc")) { - val dcmd = Seq(_findExe, procdir, "-maxdepth", "1", "-type", "d") - val dirs: Seq[String] = { - for { - dir <- dcmd.lazyLines_!.toSeq - if dir != procdir - if !procReject.contains(dir) - } yield s"$dir/" - }.toIndexedSeq - val files = Seq(_findExe, procdir, "-maxdepth", "1", "-type", "f").lazyLines_!.toSeq - dirs ++ files - } else { - Nil - } - } - lazy val procReject = Set( - "/proc/registry", - "/proc/registry32", - "/proc/registry64", - "/proc/sys", - ) - - def procfsExists(parent: String, basename: String, isDir: Boolean): Boolean = { - val ftype = if (isDir) "d" else "f" - val cmd = Seq(_findExe, parent, "-maxdepth", "1", "-type", ftype, "-name", basename) - cmd.lazyLines_!.nonEmpty - } - - // useful for converting /proc/12345/cmdline into command line - def catv(filepath: String): Array[String] = { - val cmd = Seq(_bashExe, "-c", s"[ -e $filepath ] && ${_catExe} -v $filepath") - cmd.lazyLines_!.mkString.split("\\^@") - } - - def cmdlines: Seq[(String, String)] = { - val cmd = Seq(_findExe, "/proc/", "-maxdepth", "2", "-type", "f", "-name", "cmdline") - val results = { - for { - pf <- cmd.lazyLines_! - cmdline = catv(pf).map { s => s"'$s'" }.mkString(" ") - } yield (pf, cmdline) - }.toList - results - } - - object Procfs { - def apply(path: String): Procfs = { - assert(path.startsWith("/")) - new Procfs(path) - } - } - class Procfs(val filepath: String) { - val segs: List[String] = pathSegs(filepath) - val basename = segs.last.takeWhile(_ != '/') // drop trailing slash - val directorySyntax = filepath.length > basename.length // true if has a trailing slash - - def parent: String = "/" + segs.init.mkString("/") - def exists: Boolean = procfsExists(parent, basename, directorySyntax) - def isFile: Boolean = exists && !directorySyntax - def isDir: Boolean = exists && directorySyntax - def isDirectory = isDir - - // format: off - def subfiles: Seq[Procfs] = if (isDir) procfsSubfiles(filepath).map { Procfs(_) } else Nil - - override def toString = filepath - } -} diff --git a/src/main/scala/vastblue/pallet.scala b/src/main/scala/vastblue/pallet.scala index 9ca7a0a..0cbf49c 100644 --- a/src/main/scala/vastblue/pallet.scala +++ b/src/main/scala/vastblue/pallet.scala @@ -14,9 +14,11 @@ object pallet extends vastblue.util.PathExtensions { //def Paths = vastblue.file.Paths def today = now def yesterday = now - 1.day - + + def posixroot: String = vastblue.Platform.posixroot + extension (p: Path) { - def lastModifiedTime = whenModified(p.toFile) + def lastModifiedTime = whenModified(p.toFile) def lastModSecondsDbl: Double = { secondsBetween(lastModifiedTime, now).toDouble } diff --git a/src/main/scala/vastblue/time/ChronoParse.scala b/src/main/scala/vastblue/time/ChronoParse.scala index f50b9cb..3dad244 100644 --- a/src/main/scala/vastblue/time/ChronoParse.scala +++ b/src/main/scala/vastblue/time/ChronoParse.scala @@ -2,8 +2,7 @@ package vastblue.time import vastblue.pallet.* -//import vastblue.time.TimeDate.* - +//import vastblue.time.TimeParser import java.time.LocalDateTime import scala.runtime.RichInt @@ -17,9 +16,8 @@ object ChronoParse { lazy val now: LocalDateTime = LocalDateTime.now() lazy val MonthNamesPattern = "(?i)(.*)(Jan[uary]*|Feb[ruary]*|Mar[ch]*|Apr[il]*|May|June?|July?|Aug[ust]*|Sep[tember]*|Oct[ober]*|Nov[ember]*|Dec[ember]*)(.*)".r - // by default, prefer US format, but swap month and day if unavoidable - // (e.g., 24/12/2022 incompatible with US format, not with Int'l format + // (e.g., 24/12/2022 invalid US format, valid Int'l format) def usage(m: String=""): Nothing = { _usage(m, Seq( "[] ; one datetime string per line", @@ -137,15 +135,15 @@ object ChronoParse { // If month name present, convert to numeric equivalent. // month-day order is also implied and must be captured. // Return array of numbers plus month index (can be -1). - private[vastblue] def numerifyNames(_cleanFields: Array[String]): (Int, Seq[String]) = { - var cleanFields = _cleanFields + def numerifyNames(_cleanFields: Array[String]): (Int, Seq[String]) = { + var cleanFields = _cleanFields.filter { _.trim.nonEmpty } //val clean = datestr.replaceAll("(?i)(Sun[day]*|Mon[day]*|Tue[sday]*|Wed[nesday]*|Thu[rsday]*|Fri[day]*|Sat[urday]*),? *", "") //val cleanFields: Array[String] = clean.split("[-/,\\s]+") val clean = cleanFields.mkString(" ") val monthIndex = clean match { case MonthNamesPattern(pre, monthName, post) => - val month: Int = TimeParser.monthAbbrev2Number(monthName) + val month: Int = monthAbbrev2Number(monthName) //val numerified = s"$pre $month $post" val midx = cleanFields.indexWhere( _.contains(monthName) ) // val midx = cleanFields.indexWhere { (s: String) => s.matches("(?i).*[JFMASOND][aerpuco][nbrylgptvc][a-z]*.*") } @@ -194,7 +192,7 @@ object ChronoParse { ff.partition { case s if s.contains(":") => false // HH mm or ss - case s if s.matches("[-+][0-9]{4}") => + case s if s.matches("(?i)([AP]M)?[-+][0-9]{4}") => false // time zone case s if s.matches("[.][0-9]+") => false // decimal time field @@ -204,7 +202,9 @@ object ChronoParse { true // date } } - val (times, zones) = tms.partition { _.contains(":") } + val (times, zones) = tms.partition { (s: String) => + s.contains(":") // || s.matches("(?i)([AP]M)?[-+][0-9]{4}") + } (dts, times, zones.mkString(" ")) } @@ -247,470 +247,508 @@ object ChronoParse { (datefields, timestring, timezone, monthIndex, yearIndex) } + lazy val BadChrono = new ChronoParse(BadDate, "", "", Nil, false) + def isDigit(c: Char): Boolean = c >= '0' && c <= '9' + /* * ChronoParse constructor. */ def apply(_rawdatetime: String): ChronoParse = { - if (_rawdatetime.startsWith("10:")) { - hook += 1 - } - var valid = true - var confident = true - - val (datefields, timestring, timezone, _monthIndex, _yearIndex) = cleanPrep(_rawdatetime) + if (!isPossibleDateString(_rawdatetime)) { + BadChrono + } else { + if (_rawdatetime.startsWith("10:")) { + hook += 1 + } + var valid = true + var confident = true - var (monthIndex: Int, yearIndex: Int) = (_monthIndex, _yearIndex) - -// def timestring = s"$timestring $timezone" - val rawdatetime = s"${datefields.mkString(" ")} ${timestring} $timezone".trim + val (datefields, timestring, timezone, _monthIndex, _yearIndex) = cleanPrep(_rawdatetime) - var _datenumstrings: IndexedSeq[String] = Nil.toIndexedSeq - if (datefields.nonEmpty) { - setDatenums( - datefields.mkString(" ").replaceAll("\\D+", " ").trim.split(" +").toIndexedSeq - ) - } + var (monthIndex: Int, yearIndex: Int) = (_monthIndex, _yearIndex) + + // def timestring = s"$timestring $timezone" + val rawdatetime = s"${datefields.mkString(" ")} ${timestring} $timezone".trim - def setDatenums(newval: Seq[String]): Unit = { - if (newval.isEmpty) { - hook += 1 - } - val bad = newval.exists{ (s: String) => - s.trim.isEmpty || !s.matches("[0-9]+") + var _datenumstrings: IndexedSeq[String] = Nil.toIndexedSeq + if (datefields.nonEmpty) { + setDatenums( + datefields.mkString(" ").replaceAll("\\D+", " ").trim.split(" +").toIndexedSeq + ) } - if (bad){ - hook += 1 + + def setDatenums(newval: Seq[String]): Unit = { + if (newval.isEmpty) { + hook += 1 + } + val bad = newval.exists{ (s: String) => + s.trim.isEmpty || !s.matches("[0-9]+") + } + if (bad){ + hook += 1 + } + _datenumstrings = newval.toIndexedSeq } - _datenumstrings = newval.toIndexedSeq - } - def datenumstrings = _datenumstrings - - def swapDayAndMonth(dayIndex: Int, monthIndex: Int, monthStr: String, numstrings: IndexedSeq[String]): IndexedSeq[String] = { - val maxIndex = numstrings.length - 1 - assert(dayIndex >= 0 && monthIndex >= 0 && dayIndex <= maxIndex && monthIndex <= maxIndex) - val day = numstrings(dayIndex) - var newnumstrings = numstrings.updated(monthIndex, day) - newnumstrings = newnumstrings.updated(dayIndex, monthStr) - newnumstrings - } + def datenumstrings = _datenumstrings - val widenums = datenumstrings.filter { _.length >= 4 } - widenums.toList match { - case Nil => // no wide num fields - case year :: _ if year.length == 4 => - yearIndex = datenumstrings.indexOf(year) - if (yearIndex > 3) { - val (left, rite) = datenumstrings.splitAt(yearIndex) - val newnumstrings = Seq(year) ++ left ++ rite.drop(1) - setDatenums(newnumstrings.toIndexedSeq) + def swapDayAndMonth(dayIndex: Int, monthIndex: Int, monthStr: String, numstrings: IndexedSeq[String]): IndexedSeq[String] = { + val maxIndex = numstrings.length - 1 + assert(dayIndex >= 0 && monthIndex >= 0 && dayIndex <= maxIndex && monthIndex <= maxIndex) + val day = numstrings(dayIndex) + var newnumstrings = numstrings.updated(monthIndex, day) + newnumstrings = newnumstrings.updated(dayIndex, monthStr) + newnumstrings } - hook += 1 - case ymd :: _ => - hook += 1 // maybe 20240213 or similar - var (y, m, d) = ("", "", "") - if (ymd.startsWith("2") && ymd.length == 8) { - // assume yyyy/mm/dd - y = ymd.take(4) - m = ymd.drop(4).take(2) - d = ymd.drop(6) - } else if (ymd.drop(4).matches("2[0-9]{3}") ){ - if (monthFirst) { - // assume mm/dd/yyyy - m = ymd.take(2) - d = ymd.drop(2).take(2) - y = ymd.drop(4) - } else { - // assume dd/mm/yyyy - d = ymd.take(2) - m = ymd.drop(2).take(2) - y = ymd.drop(4) + + val widenums = datenumstrings.filter { _.length >= 4 } + widenums.toList match { + case Nil => // no wide num fields + case year :: _ if year.length == 4 && year.toInt >= 1000 => + yearIndex = datenumstrings.indexOf(year) + if (yearIndex > 3) { + val (left, rite) = datenumstrings.splitAt(yearIndex) + val newnumstrings = Seq(year) ++ left ++ rite.drop(1) + setDatenums(newnumstrings.toIndexedSeq) } - } - val newymd = Seq(y, m, d) - val newnumstrings: Seq[String] = { - val head: String = datenumstrings.head - if (head == ymd) { - val rite: Seq[String] = datenumstrings.tail - val (mid: Seq[String], tail: Seq[String]) = rite.splitAt(1) - val hrmin: String = mid.mkString - if (hrmin.matches("[0-9]{3,4}")) { - val (hr: String, min: String) = hrmin.splitAt(hrmin.length-2) - val hour = if (hr.length == 1) { - s"0$hr" + hook += 1 + case ymd :: _ => + hook += 1 // maybe 20240213 or similar + var (y, m, d) = ("", "", "") + if (ymd.toInt >= 1000 && ymd.length == 8) { + // assume yyyy/mm/dd + y = ymd.take(4) + m = ymd.drop(4).take(2) + d = ymd.drop(6) + } else if (ymd.drop(4).matches("2[0-9]{3}") ){ + if (monthFirst) { + // assume mm/dd/yyyy + m = ymd.take(2) + d = ymd.drop(2).take(2) + y = ymd.drop(4) + } else { + // assume dd/mm/yyyy + d = ymd.take(2) + m = ymd.drop(2).take(2) + y = ymd.drop(4) + } + } + val newymd = Seq(y, m, d) + val newnumstrings: Seq[String] = { + val head: String = datenumstrings.head + if (head == ymd) { + val rite: Seq[String] = datenumstrings.tail + val (mid: Seq[String], tail: Seq[String]) = rite.splitAt(1) + val hrmin: String = mid.mkString + if (hrmin.matches("[0-9]{3,4}")) { + val (hr: String, min: String) = hrmin.splitAt(hrmin.length-2) + val hour = if (hr.length == 1) { + s"0$hr" + } else { + hr + } + val lef: Seq[String] = newymd + val mid: Seq[String] = Seq(hour, min) + newymd ++ mid ++ tail } else { - hr + newymd ++ rite } - val lef: Seq[String] = newymd - val mid: Seq[String] = Seq(hour, min) - newymd ++ mid ++ tail } else { - newymd ++ rite + val i = datenumstrings.indexOf(ymd) + val (left, rite) = datenumstrings.splitAt(i) + val newhrmin = rite.drop(1) + left ++ newymd ++ newhrmin } - } else { - val i = datenumstrings.indexOf(ymd) - val (left, rite) = datenumstrings.splitAt(i) - val newhrmin = rite.drop(1) - left ++ newymd ++ newhrmin } + setDatenums(newnumstrings.toIndexedSeq) } - setDatenums(newnumstrings.toIndexedSeq) - } - if (monthIndex < 0) { - assert(yearIndex <= 2, s"year index > 2: $yearIndex") // TODO : wrap this as a return value - (yearIndex, monthFirst) match { - case (-1, _) => // not enough info - case (0, true) => // y-m-d - monthIndex = 1 - case (0, false) => // y-d-m - monthIndex = 2 - case (2, true) => // m-d-y - monthIndex = 0 - case (2, false) => // d-m-y - monthIndex = 1 - case (1, true) => // m-y-d // ambiguous! - confident = false - monthIndex = 0 - case (1, false) => // d-y-m // ambiguous! - confident = false - monthIndex = 2 - case _ => - hook += 1 // TODO - } - } - def centuryPrefix(year: Int = now.getYear): String = { - century(year).toString.take(2) - } - def century(y: Int = now.getYear): Int = { - (y - y % 100) - } - (monthIndex, yearIndex) match { - case (_, -1) => - datenumstrings.take(3) match { - case Seq(m: String, d: String, y:String) if m.length <= 2 & d.length <= 2 && y.length <= 2 => - if (monthFirst) { - val fullyear = s"${centuryPrefix()}$y" - setDatenums(datenumstrings.updated(2, fullyear)) + + if (yearIndex > 2) { + new ChronoParse(BadDate, _rawdatetime, "", Nil, false) + } else { + if (monthIndex < 0) { + // TODO : wrap this as a return value + (yearIndex, monthFirst) match { + case (-1, _) => // not enough info + case (0, true) => // y-m-d + monthIndex = 1 + case (0, false) => // y-d-m + monthIndex = 2 + case (2, true) => // m-d-y + monthIndex = 0 + case (2, false) => // d-m-y + monthIndex = 1 + case (1, true) => // m-y-d // ambiguous! + confident = false + monthIndex = 0 + case (1, false) => // d-y-m // ambiguous! + confident = false + monthIndex = 2 + case _ => + hook += 1 // TODO } - case _ => - // TODO verify this cannot happen (year index not initialized yet, so previous case is complete) - hook += 1 } + def centuryPrefix(year: Int = now.getYear): String = { + century(year).toString.take(2) + } + def century(y: Int = now.getYear): Int = { + (y - y % 100) + } + (monthIndex, yearIndex) match { + case (_, -1) => + datenumstrings.take(3) match { + case Seq(m: String, d: String, y:String) if m.length <= 2 & d.length <= 2 && y.length <= 2 => + if (monthFirst) { + val fullyear = s"${centuryPrefix()}$y" + setDatenums(datenumstrings.updated(2, fullyear)) + } + case _ => + // TODO verify this cannot happen (year index not initialized yet, so previous case is complete) + hook += 1 + } - case (-1, _) => - hook += 1 - case (0, 2) | (1, 0) => // m-d-y | y-m-d (month precedes day) - val month = toNum(datenumstrings(monthIndex)) - if (!monthFirst && month <= 12) { - val dayIndex = monthIndex + 1 - // swap month and day, if preferred and possible - val day = datenumstrings(dayIndex) - var newnums = datenumstrings.updated(monthIndex, day) - newnums = newnums.updated(dayIndex, month.toString) - setDatenums(newnums) - } - case (1, 2) => // d-m-y - val month = toNum(datenumstrings(monthIndex)) - if (monthFirst && month <= 12) { - // swap month and day, if preferred and possible - val dayIndex = monthIndex - 1 - val swapped = swapDayAndMonth(dayIndex, monthIndex, month.toString, datenumstrings) - setDatenums(swapped) - // swap month and day, if preferred and possible -// val day = datenumstrings(dayIndex) -// setDatenums(datenumstrings.updated(monthIndex, day)) -// setDatenums(datenumstrings.updated(dayIndex, month.toString)) - } - case (m, y) => // d-m-y - hook += 1 // TODO - } - if (monthIndex >= 0) { - val month = toNum(datenumstrings(monthIndex)) - if (!monthFirst && month <= 12) { - // swap month and day, if preferred and possible - val day = datenumstrings(monthIndex + 1) - var newnums = datenumstrings.updated(monthIndex, day) - newnums = newnums.updated(monthIndex+1, month.toString) - setDatenums(newnums) - } - } - //var nums: Array[Int] = datefields.map { (s: String) => toI(s) } - var nums: Seq[Int] = datenumstrings.map { (numstr: String) => - if (!numstr.matches("[0-9]+")) { - hook += 1 - } - toNum(numstr) - } + case (-1, _) => + hook += 1 + case (0, 2) | (1, 0) => // m-d-y | y-m-d (month precedes day) + val month = toNum(datenumstrings(monthIndex)) + if (!monthFirst && month <= 12) { + val dayIndex = monthIndex + 1 + // swap month and day, if preferred and possible + val day = datenumstrings(dayIndex) + var newnums = datenumstrings.updated(monthIndex, day) + newnums = newnums.updated(dayIndex, month.toString) + setDatenums(newnums) + } + case (1, 2) => // d-m-y + val month = toNum(datenumstrings(monthIndex)) + if (monthFirst && month <= 12) { + // swap month and day, if preferred and possible + val dayIndex = monthIndex - 1 + val swapped = swapDayAndMonth(dayIndex, monthIndex, month.toString, datenumstrings) + setDatenums(swapped) + // swap month and day, if preferred and possible + // val day = datenumstrings(dayIndex) + // setDatenums(datenumstrings.updated(monthIndex, day)) + // setDatenums(datenumstrings.updated(dayIndex, month.toString)) + } + case (m, y) => // d-m-y + hook += 1 // TODO + } + if (monthIndex >= 0) { + val month = toNum(datenumstrings(monthIndex)) + if (!monthFirst && month <= 12) { + // swap month and day, if preferred and possible + val day = datenumstrings(monthIndex + 1) + var newnums = datenumstrings.updated(monthIndex, day) + newnums = newnums.updated(monthIndex+1, month.toString) + setDatenums(newnums) + } + } + //var nums: Array[Int] = datefields.map { (s: String) => toI(s) } + var nums: Seq[Int] = datenumstrings.filter { _.trim.nonEmpty }.map { (numstr: String) => + if (!numstr.matches("[0-9]+")) { + hook += 1 + } + toNum(numstr) + } - val timeOnly: Boolean = datenumstrings.size <= 4 && rawdatetime.matches("[0-9]{2}:[0-9]{2}.*") - if ( !timeOnly ) { - def adjustYear(year: Int): Unit = { - nums = nums.take(2) ++ Seq(year) ++ nums.drop(3) - val newnums = nums.map { _.toString } - setDatenums(newnums) - } - val dateFields = nums.take(3) - dateFields match { - case Seq(a, b, c) if a > 31 || b > 31 || c > 31 => - hook += 1 // the typical case where 4-digit year is provided - case Seq(a, b) => - // the problem case; assume no year provided - adjustYear(now.getYear) // no year provided, use current year - case Seq(mOrD, dOrM, relyear) => - // the problem case; assume M/d/y or d/M/y format - val y = now.getYear - val century = y - y % 100 - adjustYear(century + relyear) - case _ => - hook += 1 // huh? - } - } + val timeOnly: Boolean = datenumstrings.size <= 4 && rawdatetime.matches("[0-9]{2}:[0-9]{2}.*") + if ( !timeOnly ) { + def adjustYear(year: Int): Unit = { + nums = nums.take(2) ++ Seq(year) ++ nums.drop(3) + val newnums = nums.map { _.toString } + setDatenums(newnums) + } + val dateFields = nums.take(3) + dateFields match { + case Seq(a, b, c) if a > 31 || b > 31 || c > 31 => + hook += 1 // the typical case where 4-digit year is provided + case Seq(a, b) => + // the problem case; assume no year provided + adjustYear(now.getYear) // no year provided, use current year + case Seq(mOrD, dOrM, relyear) => + // the problem case; assume M/d/y or d/M/y format + val y = now.getYear + val century = y - y % 100 + adjustYear(century + relyear) + case _ => + hook += 1 // huh? + } + } - val fields: Seq[(String, Int)] = datenumstrings.zipWithIndex - var (yval, mval, dval) = (0, 0, 0) - val farr = fields.toArray - var formats: Array[String] = farr.map { (s: String, i: Int) => - if (i < 3 && !timeOnly) { - toNum(s) match { - case y if y > 31 || s.length == 4 => - yval = y - s.replaceAll(".", "y") - case d if d > 12 && s.length <= 2 => - dval = d - s.replaceAll(".", "d") - case _ => // can't resolve month without more context - s + val fields: Seq[(String, Int)] = datenumstrings.zipWithIndex + var (yval, mval, dval) = (0, 0, 0) + val farr = fields.toArray + var formats: Array[String] = farr.map { (s: String, i: Int) => + if (i < 3 && !timeOnly) { + toNum(s) match { + case y if y > 31 || s.length == 4 => + yval = y + s.replaceAll(".", "y") + case d if d > 12 && s.length <= 2 => + dval = d + s.replaceAll(".", "d") + case _ => // can't resolve month without more context + s + } + } else { + i match { + case 3 => s.replaceAll(".", "H") + case 4 => s.replaceAll(".", "m") + case 5 => s.replaceAll(".", "s") + case 6 => s.replaceAll(".", "Z") + case _ => + s // not expecting any more numeric fields + } + } } - } else { - i match { - case 3 => s.replaceAll(".", "H") - case 4 => s.replaceAll(".", "m") - case 5 => s.replaceAll(".", "s") - case 6 => s.replaceAll(".", "Z") - case _ => - s // not expecting any more numeric fields + def indexOf(s: String): Int = { + formats.indexWhere((fld: String) => + fld.startsWith(s) + ) } - } - } - def indexOf(s: String): Int = { - formats.indexWhere((fld: String) => - fld.startsWith(s) - ) - } - def numIndex: Int = { - formats.indexWhere((s: String) => s.matches("[0-9]+")) - } - def setFirstNum(s: String): Int = { - val i = numIndex - val numval = formats(i) - val numfmt = numval.replaceAll("[0-9]", s) - formats(i) = numfmt - toNum(numval) - } - // if two yyyy-MM-dd fields already fixed, the third is implied - formats.take(3).map { _.distinct }.sorted match { - case Array(_, "M", "y") => dval = setFirstNum("d") - case Array(_, "d", "y") => mval = setFirstNum("M") - case Array(_, "M", "d") => yval = setFirstNum("y") - case _arr => - hook += 1 // more than one numeric fields, so not ready to resolve - } - hook += 1 - def is(s: String, v: String): Boolean = s.startsWith(v) - - val yidx = indexOf("y") - val didx = indexOf("d") - val midx = indexOf("M") - - def hasY = yidx >= 0 - def hasM = midx >= 0 - def hasD = didx >= 0 - def needsY = yidx < 0 - def needsM = midx < 0 - def needsD = didx < 0 - - def replaceFirstNumericField(s: String): Unit = { - val i = numIndex - if (i < 0) { - hook += 1 // no numerics found - } else { - assert(i >= 0 && i < 3, s"internal error: $_rawdatetime [i: $i, s: $s]") - s match { - case "y" => - assert(yval == 0, s"yval: $yval") - yval = toNum(formats(i)) - case "M" => - assert(mval == 0, s"mval: $mval") - mval = toNum(formats(i)) - case "d" => - if (dval > 0) { - hook += 1 + def numIndex: Int = { + formats.indexWhere((s: String) => s.matches("[0-9]+")) + } + def setFirstNum(s: String): Int = { + val i = numIndex + if (i < 0) { + hook += 1 + } + val numval = formats(i) + val numfmt = numval.replaceAll("[0-9]", s) + formats(i) = numfmt + toNum(numval) + } + // if two yyyy-MM-dd fields already fixed, the third is implied + formats.take(3).map { _.distinct }.sorted match { + case Array(_, "M", "y") => dval = setFirstNum("d") + case Array(_, "d", "y") => mval = setFirstNum("M") + case Array(_, "M", "d") => yval = setFirstNum("y") + case _arr => + hook += 1 // more than one numeric fields, so not ready to resolve + } + hook += 1 + def is(s: String, v: String): Boolean = s.startsWith(v) + + val yidx = indexOf("y") + val didx = indexOf("d") + val midx = indexOf("M") + + def hasY = yidx >= 0 + def hasM = midx >= 0 + def hasD = didx >= 0 + def needsY = yidx < 0 + def needsM = midx < 0 + def needsD = didx < 0 + + def replaceFirstNumericField(s: String): Unit = { + val i = numIndex + if (i < 0) { + hook += 1 // no numerics found + } else { + assert(i >= 0 && i < 3, s"internal error: $_rawdatetime [i: $i, s: $s]") + s match { + case "y" => + assert(yval == 0, s"yval: $yval") + yval = toNum(formats(i)) + case "M" => + assert(mval == 0, s"mval: $mval") + mval = toNum(formats(i)) + case "d" => + if (dval > 0) { + hook += 1 + } + assert(dval == 0, s"dval: $dval") + dval = toNum(formats(i)) + case _ => + sys.error(s"internal error: bad format indicator [$s]") } - assert(dval == 0, s"dval: $dval") - dval = toNum(formats(i)) - case _ => - sys.error(s"internal error: bad format indicator [$s]") + setFirstNum(s) + } } - setFirstNum(s) - } - } - val needs = Seq(needsY, needsM, needsD) - (needsY, needsM, needsD) match { - case (false, false, true) => - replaceFirstNumericField("d") - case (false, true, false) => - replaceFirstNumericField("M") - case (true, false, false) => - replaceFirstNumericField("y") - - case (false, true, true) => - // has year, needs month and day - yidx match { - case 1 => - // might as well support bizarre formats (M-y-d or d-M-y) - if (monthFirst) { - replaceFirstNumericField("M") - replaceFirstNumericField("d") - } else { + val needs = Seq(needsY, needsM, needsD) + (needsY, needsM, needsD) match { + case (false, false, true) => replaceFirstNumericField("d") + case (false, true, false) => replaceFirstNumericField("M") + case (true, false, false) => + replaceFirstNumericField("y") + + case (false, true, true) => + // has year, needs month and day + yidx match { + case 1 => + // might as well support bizarre formats (M-y-d or d-M-y) + if (monthFirst) { + replaceFirstNumericField("M") + replaceFirstNumericField("d") + } else { + replaceFirstNumericField("d") + replaceFirstNumericField("M") + } + case 0 | 2 => + // y-M-d + if (monthFirst) { + replaceFirstNumericField("M") + replaceFirstNumericField("d") + } else { + replaceFirstNumericField("d") + replaceFirstNumericField("M") + } + + } + case (true, true, false) => + // has day, needs month and year + didx match { + case 0 => + // d-M-y + replaceFirstNumericField("M") + replaceFirstNumericField("y") + case 2 => + // y-M-d + replaceFirstNumericField("y") + replaceFirstNumericField("M") + case 1 => + // AMBIGUOUS ... + if (monthFirst) { + // M-d-y + replaceFirstNumericField("d") + replaceFirstNumericField("M") + } else { + // d-M-y + replaceFirstNumericField("M") + replaceFirstNumericField("d") + } + } + case (false, false, false) => + hook += 1 // done + case (true, true, true) if timeOnly => + hook += 1 // done + case (yy, mm, dd) => + formats.toList match { + case a :: b :: Nil => + val (ta, tb) = (toNum(a), toNum(b)) + // interpret as missing day or missing year + // missing day if either field is > 31 + if (monthFirst && ta <= 12) { + mval = ta + dval = tb + } else { + mval = tb + dval = tb + } + if (mval > 31) { + // assume day is missing + yval = mval + mval = dval + dval = 1 // convention + } else if (dval > 31) { + // assume day is missing + yval = dval + dval = 1 // convention + } else { + if (mval > 12) { + // the above swap might make this superfluous + // swap month and day + val temp = mval + mval = dval + dval = temp + } + yval = now.getYear // supply missing year + } + // TODO: reorder based on legal field values, if appropriate + formats = Array("yyyy", "MM", "dd") + setDatenums(IndexedSeq(yval, mval, dval).map { _.toString }) + case _ => + if (datenumstrings.nonEmpty) { + sys.error(s"yy[$yy], mm[$mm], dd[$dd] datetime[$_rawdatetime], formats[${formats.mkString("|")}]") + } + } } - case 0 | 2 => - // y-M-d - if (monthFirst) { - replaceFirstNumericField("M") - replaceFirstNumericField("d") - } else { - replaceFirstNumericField("d") - replaceFirstNumericField("M") + if (datenumstrings.endsWith("2019") ){ + hook += 1 } - } - case (true, true, false) => - // has day, needs month and year - didx match { - case 0 => - // d-M-y - replaceFirstNumericField("M") - replaceFirstNumericField("y") - case 2 => - // y-M-d - replaceFirstNumericField("y") - replaceFirstNumericField("M") - case 1 => - // AMBIGUOUS ... - if (monthFirst) { - // M-d-y - replaceFirstNumericField("d") - replaceFirstNumericField("M") - } else { - // d-M-y - replaceFirstNumericField("M") - replaceFirstNumericField("d") + val bareformats = formats.map { _.distinct }.toList + nums = { + val tdnums = (datenumstrings ++ timestring.split("[+: ]+")) + tdnums.filter { _.trim.nonEmpty }.map { toNum(_) } } - } - case (false, false, false) => - hook += 1 // done - case (true, true, true) if timeOnly => - hook += 1 // done - case (yy, mm, dd) => - formats.toList match { - case a :: b :: Nil => - val (ta, tb) = (toNum(a), toNum(b)) - // interpret as missing day or missing year - // missing day if either field is > 31 - if (monthFirst && ta <= 12) { - mval = ta - dval = tb - } else { - mval = tb - dval = tb - } - if (mval > 31) { - // assume day is missing - yval = mval - mval = dval - dval = 1 // convention - } else if (dval > 31) { - // assume day is missing - yval = dval - dval = 1 // convention - } else { - if (mval > 12) { - // the above swap might make this superfluous - // swap month and day - val temp = mval - mval = dval - dval = temp + def ymd(iy: Int, im: Int, id: Int, tail: List[String]): LocalDateTime = { + if (iy <0 || im <0 || id <0) { + hook += 1 + } else if (nums.size < 3) { + hook += 1 } - yval = now.getYear // supply missing year + val standardOrder = List(nums(iy), nums(im), nums(id)) ++ nums.drop(3) + yyyyMMddHHmmssToDate(standardOrder) } - // TODO: reorder based on legal field values, if appropriate - formats = Array("yyyy", "MM", "dd") - setDatenums(IndexedSeq(yval, mval, dval).map { _.toString }) - case _ => - if (datenumstrings.nonEmpty) { - sys.error(s"yy[$yy], mm[$mm], dd[$dd] datetime[$_rawdatetime], formats[${formats.mkString("|")}]") + val dateTime: LocalDateTime = bareformats match { + case "d" :: "M" :: "y" :: tail => ymd(2,1,0, tail) + case "M" :: "d" :: "y" :: tail => ymd(2,0,1, tail) + case "d" :: "y" :: "M" :: tail => ymd(1,2,0, tail) + case "M" :: "y" :: "d" :: tail => ymd(1,0,2, tail) + case "y" :: "d" :: "M" :: tail => ymd(0,2,1, tail) + case "y" :: "M" :: "d" :: tail => ymd(0,1,2, tail) + case other => + valid = false + BadDate } + new ChronoParse(dateTime, _rawdatetime, timezone, formats.toSeq, valid) } } - if (datenumstrings.endsWith("2019") ){ - hook += 1 - } + } + + def validYear(y: Int): Boolean = y > 0 && y < 2500 + def validMonth(m: Int): Boolean = m > 0 && m <= 12 + def validDay(d: Int): Boolean = d > 0 && d <= 31 - val bareformats = formats.map { _.distinct }.toList - nums = { - val tdnums = (datenumstrings ++ timestring.split("[+: ]+")) - tdnums.filter { _.trim.nonEmpty }.map { toNum(_) } - } - def ymd(iy: Int, im: Int, id: Int, tail: List[String]): LocalDateTime = { - if (iy <0 || im <0 || id <0) { - hook += 1 - } else if (nums.size < 3) { - hook += 1 - } - val standardOrder = List(nums(iy), nums(im), nums(id)) ++ nums.drop(3) - yyyyMMddHHmmssToDate(standardOrder) - } - val dateTime: LocalDateTime = bareformats match { - case "d" :: "M" :: "y" :: tail => ymd(2,1,0, tail) - case "M" :: "d" :: "y" :: tail => ymd(2,0,1, tail) - case "d" :: "y" :: "M" :: tail => ymd(1,2,0, tail) - case "M" :: "y" :: "d" :: tail => ymd(1,0,2, tail) - case "y" :: "d" :: "M" :: tail => ymd(0,2,1, tail) - case "y" :: "M" :: "d" :: tail => ymd(0,1,2, tail) - case other => - valid = false - BadDate - } - new ChronoParse(dateTime, _rawdatetime, timezone, formats.toSeq, valid) + def validYmd(ymd: Seq[Int]): Boolean = { + val Seq(y: Int, m: Int, d: Int) = ymd + validYear(y) && validMonth(m) && validDay(d) + } + def validTimeFields(nums: Seq[Int]): Boolean = { + nums.forall { (n: Int) => n >= 0 && n <= 59 } } - def yyyyMMddHHmmssToDate(so: List[Int]): LocalDateTime = { - so.take(7) match { - case yr :: mo :: dy :: hr :: mn :: sc :: nano :: Nil => - LocalDateTime.of(yr, mo, dy, hr, mn, sc, nano) - case yr :: mo :: dy :: hr :: mn :: sc :: Nil => - if (sc > 59 || mn > 59 || hr > 59) { - hook += 1 - } - LocalDateTime.of(yr, mo, dy, hr, mn, sc) - case yr :: mo :: dy :: hr :: mn :: Nil => - LocalDateTime.of(yr, mo, dy, hr, mn, 0) - case yr :: mo :: dy :: hr :: Nil => - if (hr > 23) { - hook += 1 - } - LocalDateTime.of(yr, mo, dy, hr, 0, 0) - case yr :: mo :: dy :: Nil => - if (mo > 12) { - hook += 1 + if (!validYmd(so.take(3))) { + BadDate + } else if(!validTimeFields(so.drop(3))) { + BadDate + } else { + so.take(7) match { + case yr :: mo :: dy :: hr :: mn :: sc :: nano :: Nil => + if (hr > 12 || mn > 12 || sc > 12) { + BadDate + } else { + LocalDateTime.of(yr, mo, dy, hr, mn, sc, nano) + } + case yr :: mo :: dy :: hr :: mn :: sc :: Nil => + if (sc > 59 || mn > 59 || hr > 59) { + hook += 1 + } + LocalDateTime.of(yr, mo, dy, hr, mn, sc) + case yr :: mo :: dy :: hr :: mn :: Nil => + LocalDateTime.of(yr, mo, dy, hr, mn, 0) + case yr :: mo :: dy :: hr :: Nil => + if (hr > 23) { + hook += 1 + } + LocalDateTime.of(yr, mo, dy, hr, 0, 0) + case yr :: mo :: dy :: Nil => + if (mo > 12) { + hook += 1 + } + LocalDateTime.of(yr, mo, dy, 0, 0, 0) + case other => + //sys.error(s"not enough date-time fields: [${so.mkString("|")}]") + BadDate } - LocalDateTime.of(yr, mo, dy, 0, 0, 0) - case other => - sys.error(s"not enough date-time fields: [${so.mkString("|")}]") } } + lazy val timeZoneCodes = Set( "ACDT", // Australian Central Daylight Saving Time UTC+10:30 "ACST", // Australian Central Standard Time UTC+09:30 @@ -928,8 +966,61 @@ object ChronoParse { "YAKT", // Yakutsk Time UTC+09 "YEKT", // Yekaterinburg Time ) -} + lazy val NumberPattern = "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)".r + + // quick heuristic rejection of non-date strings + def isPossibleDateString(s: String): Boolean = { + + def isDecimalNum: Boolean = NumberPattern.matches(s) && s.replaceAll("[^.]+", "").length == 1 + + if (s.length < 4 || s.length > 35 || isDecimalNum ) { + false + } else { + val lc = s.toLowerCase. + replaceAll("[jfmasond][aerpuco][nbrylgptvc][uaryrchilestmbo]{0,6}", "1"). + replaceAll("[mtwfs][ouehra][neduit][daysneru]{0,4}", "2") + var digits = 0 + var nondigits = 0 + var bogus = 0 + val validchars = lc.filter { (c: Char) => + c match { + case c if c >= '0' && c <= '9' => + digits += 1 + true + case ':' | '-' | '.' | '/' => + nondigits += 1 + true + case _ => + bogus += 1 + false + } + } + val density = 100.0 * validchars.size.toDouble / s.length.toDouble + val proportion = 100.0 * nondigits.toDouble / (digits+1.0) + digits >= 3 && digits <= 19 && density >= 30.0 && proportion < 35.0 + } + } + def monthAbbrev2Number(name: String): Int = { + name.toLowerCase.substring(0, 3) match { + case "jan" => 1 + case "feb" => 2 + case "mar" => 3 + case "apr" => 4 + case "may" => 5 + case "jun" => 6 + case "jul" => 7 + case "aug" => 8 + case "sep" => 9 + case "oct" => 10 + case "nov" => 11 + case "dec" => 12 + case _ => + hook += 1 + -1 + } + } +} // TODO: use timezone info, including CST, etc case class ChronoParse(dateTime: LocalDateTime, rawdatetime: String, timezone: String, formats: Seq[String], valid: Boolean) { diff --git a/src/main/scala/vastblue/time/TimeDate.scala b/src/main/scala/vastblue/time/TimeDate.scala index a2e36bf..aa1fe22 100644 --- a/src/main/scala/vastblue/time/TimeDate.scala +++ b/src/main/scala/vastblue/time/TimeDate.scala @@ -1,7 +1,7 @@ package vastblue.time import vastblue.pallet.* -import vastblue.time.TimeParser +//import vastblue.time.TimeParser import vastblue.time.ChronoParse import java.time.ZoneId @@ -239,8 +239,18 @@ object TimeDate extends vastblue.time.TimeExtensions { lazy val ThreeIntegerFields2 = """(\d{2,2})\D(\d{1,2})\D(\d{1,2})""".r def dateParser(inpDateStr: String, offset: Int = 0): DateTime = { - val flds = vastblue.time.ChronoParse(inpDateStr) - flds.dateTime // might be BadDate! + if (inpDateStr.trim.isEmpty) { + BadDate + } else { + def isDigit(c: Char): Boolean = c >= '0' && c <= '9' + val digitcount = inpDateStr.filter { (c: Char) => isDigit(c) }.size + if (digitcount < 3 || digitcount > 19) { + BadDate + } else { + val flds = vastblue.time.ChronoParse(inpDateStr) + flds.dateTime // might be BadDate! + } + } } private[vastblue] def _dateParser(inpDateStr: String, offset: Int = 0): DateTime = { if (inpDateStr.startsWith("31/05/2009")) { @@ -317,13 +327,15 @@ object TimeDate extends vastblue.time.TimeExtensions { parseDateString(fixed) } catch { case r: RuntimeException if r.getMessage.toLowerCase.contains("bad date format") => - if (TimeParser.debug) System.err.printf("e[%s]\n", r.getMessage) +// if (TimeParser.debug) System.err.printf("e[%s]\n", r.getMessage) BadDate case p: DateTimeParseException => - if (TimeParser.debug) System.err.printf("e[%s]\n", p.getMessage) +// if (TimeParser.debug) System.err.printf("e[%s]\n", p.getMessage) BadDate case e: Exception => - if (TimeParser.debug) System.err.printf("e[%s]\n", e.getMessage) +// if (TimeParser.debug) System.err.printf("e[%s]\n", e.getMessage) + BadDate +/* val mdate: TimeParser = TimeParser.parseDate(datestr).getOrElse(TimeParser.BadParsDate) // val timestamp = new DateTime(mdate.getEpoch) val standardFormat = mdate.toString(standardTimestampFormat) @@ -334,6 +346,7 @@ object TimeDate extends vastblue.time.TimeExtensions { val hours = (offset + extraHours).toLong timestamp.plusHours(hours) // format: on + */ } } } @@ -385,7 +398,7 @@ object TimeDate extends vastblue.time.TimeExtensions { val monthIndex = ff.indexWhere {(s: String) => s.matches("(?i).*[JFMASOND][aerpuco][nbrylgptvc][a-z]*.*")} if (monthIndex >= 0){ val monthName = ff(monthIndex) - val month: Int = TimeParser.monthAbbrev2Number(ff(monthIndex)) + val month: Int = ChronoParse.monthAbbrev2Number(ff(monthIndex)) val nwn = noweekdayName.replaceAll(monthName, "%02d ".format(month)) nwn } else { @@ -400,7 +413,7 @@ object TimeDate extends vastblue.time.TimeExtensions { if (!mstr.toLowerCase.matches("[a-z]{3}")) { hook += 1 } - val month = TimeParser.monthAbbrev2Number(mstr) + val month = ChronoParse.monthAbbrev2Number(mstr) ff = ff.drop(1) // format: off val (day, year, timestr, tz) = ff.toList match { diff --git a/src/main/scala/vastblue/time/TimeExtensions.scala b/src/main/scala/vastblue/time/TimeExtensions.scala index 4fb6dc4..c616897 100644 --- a/src/main/scala/vastblue/time/TimeExtensions.scala +++ b/src/main/scala/vastblue/time/TimeExtensions.scala @@ -103,20 +103,22 @@ trait TimeExtensions { def +(p: java.time.Period) = d.plus(p) def -(p: java.time.Period) = d.minus(p) - def minute = d.getMinute - def second = d.getSecond - def hour = d.getHour - def day = d.getDayOfMonth - def month = d.getMonth - def year = d.getYear + def year: Int = d.getYear + def month: Month = d.getMonth + def monthNum: Int = d.getMonth.getValue + def day: Int = d.getDayOfMonth + def hour: Int = d.getHour + def minute: Int = d.getMinute + def second: Int = d.getSecond def setHour(h: Int): LocalDateTime = d.plusHours((d.getHour + h).toLong) def setMinute(m: Int): LocalDateTime = d.plusMinutes((d.getMinute + m).toLong) def compare(that: DateTime): Int = d.getMillis() compare that.getMillis() - def dayOfYear = d.getDayOfYear - def getDayOfYear = d.getDayOfYear - def dayOfMonth = d.getDayOfMonth + def getDayOfYear: Int = d.getDayOfYear + def dayOfYear: Int = d.getDayOfYear + def dayOfMonth: Int = d.getDayOfMonth + def getDay: Int = d.getDayOfMonth def getDayOfMonth = d.getDayOfMonth def dayOfWeek: DayOfWeek = d.getDayOfWeek def getDayOfWeek: DayOfWeek = d.getDayOfWeek diff --git a/src/main/scala/vastblue/time/TimeParser.scala b/src/main/scala/vastblue/time/TimeParser.scala deleted file mode 100644 index 2d87161..0000000 --- a/src/main/scala/vastblue/time/TimeParser.scala +++ /dev/null @@ -1,997 +0,0 @@ -package vastblue.time - -/** - * This is useful for converting between a wide variety - * of Date and Time Strings and the TimeParser class. - */ -import vastblue.pallet.* -import java.io.{File => JFile} -import java.text.{DateFormat, SimpleDateFormat} -import java.util.{Calendar, Date, Locale} - -//import vastblue.time.TimeDate.{parseDateTime => coreParseDate} -import vastblue.time.TimeDate.* - -import scala.collection.immutable.* -import scala.util.matching.Regex -import scala.util.control.Breaks.* - -object TimeParser { - var verbose = ".verbose".path.isFile - var debug: Boolean = ".debug".path.isFile - var yearFirstFlag = true - var gcal = new java.util.GregorianCalendar() - - def reset(): Unit = { - // format: off - verbose = ".verbose".path.isFile - debug = ".debug".path.isFile - yearFirstFlag = true - gcal = new java.util.GregorianCalendar() - currentFormat = null // reserved for most recent runtime pattern - newFormats = scala.collection.immutable.Set[String]() - outfmt = dateTimeFormat // by default, show both date and time - // format: on - } - - lazy val MDY: Regex = """.*(\d{1,2})\D(\d{1,2})\D(\d{4}).*""".r - lazy val DayMonthYr: Regex = """(?i)(\d+)[^\d\w]+(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[^\d\w](\d{4})""".r - lazy val PackedIsoPattern: Regex = """.*\b(19\d{2}|20\d{2})(\d{2})(\d{2}).*""".r - - // initialize date patterns - lazy val yy: String = """(20\d{2})""" - lazy val nn: String = """\d{1,2}""" - lazy val dd: String = s"($nn)" - lazy val mm: String = s"(${firstThreeLettersPatternBuilder(monthNames)}|$nn)" - lazy val div: String = """[-\s/,]+""" - - lazy val YYMMddPtrn: Regex = ("""(?i).*""" + yy + div + mm + div + dd + """.*""").r // yyyy mm dd - lazy val MMDDyyPtrn: Regex = ("""(?i).*""" + mm + div + dd + div + yy + """.*""").r // mm dd yyyy - lazy val DDMMyyPtrn: Regex = ("""(?i).*""" + dd + div + mm + div + yy + """.*""").r // dd mm yyyy - - def isLeapYear(year: Int): Boolean = gcal.isLeapYear(year) - - lazy val YearPattern = yy.r - lazy val NumFieldPtrn = nn.r - lazy val DayPtrn = dd.r - lazy val MonthPattern = mm.r - - var currentFormat: SimpleDateFormat = null // reserved for most recent runtime pattern - - val dateOnlyFormat: DateFormat = new SimpleDateFormat("yyyy/MM/dd", Locale.US) // preferred output format 1: - val dateTimeFormat: DateFormat = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss", Locale.US) // preferred output format 2: - val dateTimeMsFormat: DateFormat = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss a", Locale.US) - var newFormats: Set[String] = scala.collection.immutable.Set[String]() - - val monthMap: Map[String, String] = Map( - "jan" -> "01", - "feb" -> "02", - "mar" -> "03", - "apr" -> "04", - "may" -> "05", - "jun" -> "06", - "jul" -> "07", - "aug" -> "08", - "sep" -> "09", - "oct" -> "10", - "nov" -> "11", - "dec" -> "12", - ) - - var outfmt: DateFormat = dateTimeFormat // by default, show both date and time - - def dateAndTime(): Unit = { outfmt = dateTimeFormat } - def dateOnly(): Unit = { outfmt = dateOnlyFormat } - - def setFormat(fmt: DateFormat): Unit = { outfmt = fmt } - def setFormat(fmt: String): Unit = { outfmt = new SimpleDateFormat(fmt) } - - def extractDateFromText(rawline: String): Option[TimeParser] = { - // debug: test before toLowerCase - val text = rawline.replaceAll("""[^-a-zA-Z:/_0-9\s]+""", " ").replaceAll("""\s+""", " ").trim - if (debug) { - text match { - case YYMMddPtrn(yy, mm, dd) if okYMD(yy, mm, dd) => - if (verbose) printf("ymd: [%s] [%s] [%s] [%s]\n", yy, mm, dd, text) - case MMDDyyPtrn(mm, dd, yy) if okYMD(yy, mm, dd) => - if (verbose) printf("mdy: [%s] [%s] [%s] [%s]\n", yy, mm, dd, text) - case DDMMyyPtrn(dd, mm, yy) if okYMD(yy, mm, dd) => - if (verbose) printf("dmy: [%s] [%s] [%s] [%s]\n", yy, mm, dd, text) - case _ => - printf("4-text[%s]\n", text) - } - } - def lc = text.toLowerCase.replaceAll("""\s+""", " ").replaceAll("""(\d)(st|nd|rd|th)\b""", "$1") - val result = if (lc.contains("@")) { - // ignore email - if (verbose) printf("ignore email[%s]\n", text) - None // Some(TimeParser("2019:01:01")) - } else if (lc.matches(""".*\bapprov.*""")) { - if (verbose) printf("ignore approval of minutes [%s]\n", text) - None - } else if (lc.matches(""".*\bletter.*""")) { - if (verbose) printf("ignore reference to letter [%s]\n", text) - None - } else { - lc.replaceAll("""\.""", "/") match { - case PackedIsoPattern(yy, mm, dd) if okYMD(yy, mm, dd) => - if (verbose) printf("iso: [%s] [%s] [%s] [%s]\n", yy, mm, dd, text) - Some(normalizedMdate(yy, mm, dd)) - case YYMMddPtrn(yy, mm, dd) if okYMD(yy, mm, dd) => - if (verbose) printf("ymd: [%s] [%s] [%s] [%s]\n", yy, mm, dd, text) - Some(normalizedMdate(yy, mm, dd)) - case MMDDyyPtrn(mm, dd, yy) if okYMD(yy, mm, dd) => - if (verbose) printf("mdy: [%s] [%s] [%s] [%s]\n", yy, mm, dd, text) - Some(normalizedMdate(yy, mm, dd)) - case DDMMyyPtrn(dd, mm, yy) if okYMD(yy, mm, dd) => - if (verbose) printf("dmy: [%s] [%s] [%s] [%s]\n", yy, mm, dd, text) - Some(normalizedMdate(yy, mm, dd)) - case YYMMddDensePattern(yy, mm, dd) if okYMD(yy, mm, dd) => - Some(normalizedMdate(yy, mm, dd)) - case _ => - None // Some(TimeParser("2019:02:02")) - } - } - result - } - def validMonth(mm: String): Boolean = { - val (_, valid) = monthToInt(mm) - valid - } - def validDay(dd: String): Boolean = { - val dnum = numStringToInt(dd) - dnum >= 1 && dnum <= 31 - } - def okYMD(yy: String, mm: String, dd: String): Boolean = { - validMonth(mm) && validDay(dd) && yy.matches("\\d{4}") - } - def monthToInt(mm: String): (Int, Boolean) = { - val num = mm match { - case DayPtrn(nn) => numStringToInt(nn) - case LcMonthPattern(mm) => monthName2Number(mm) - case _ => -1 - } - val valid = (num >= 1 && num <= 12) - (num, valid) - } - def normalizedMdate(yy: String, mm: String, dd: String): TimeParser = { - val (y, m, d) = numericFields(yy, mm, dd) - val normalized = "%04d/%02d/%02d".format(y, m, d) - if (verbose) printf("normalized: [%s]\n", normalized) - TimeParser(normalized) - } - lazy val YYMMddDensePattern: Regex = """.*(\b2[01]\d{2})(\d{2})(\d{2})\b.*""".r - lazy val LcMonthPattern: Regex = (s"(?i)${MonthPattern.toString}").r - - def numericFields(yy: String, mm: String, dd: String): (Int, Int, Int) = { - // val y = numStringToInt(yy) - // val d = numStringToInt(dd) - val monthNum = mm match { - case DayPtrn(nn) => numStringToInt(nn) - case LcMonthPattern(mm) => monthName2Number(mm) - - case _ => - val msg = s"error: month string [$mm] not matched:\nDayPtrn[%s]\nLcMonthPattern[%s]\n".format(DayPtrn, LcMonthPattern) - sys.error(msg) - } - val (year, month, day) = (numStringToInt(yy), monthNum, numStringToInt(dd)) - if (month < 1 || month > 12) eprintf("month[%s] converted to bogus month number [%d]\n".format(mm, month)) - assert(month >= 1 && month <= 12) - if (day < 1 || day > 31) eprintf("day[%s] converted to bogus day number [%d]\n".format(dd, day)) - assert(day >= 1 && day <= 31) - (year, month, day) - } - def numStringToInt(nn: String): Int = { - val withoutLeadingZeros = nn.replaceFirst("""^0*([1-9]\d+)""", """$1""") - if (verbose && nn.length != withoutLeadingZeros.length) { - printf("nn[%s]\nwithoutZeros[%s]\n", nn, withoutLeadingZeros) - } - withoutLeadingZeros.toInt // remove leading zeros before calling .toInt - } - lazy val monthNames: List[String] = List( - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ) -// lazy val weekdayNames: List[String] = List( -// "Sunday", -// "Monday", -// "Tuesday", -// "Wednesday", -// "Thursday", -// "Friday", -// "Saturday", -// ) -// lazy val monthAbbreviationsLowerCase: List[String] = monthNames.map { _.toLowerCase.substring(0, 3) } - def indexedLetters(idx: Int, list: List[String]): String = { - new String({ - var uniqChars = List[Char]() - for (cc <- list.map { _.charAt(idx) }.toArray) { - if (!uniqChars.contains(cc)) { - uniqChars ::= cc - } - } - uniqChars.reverse.toArray - }) - } - def firstThreeLettersPatternBuilder(list: List[String]): String = { - val cc0 = indexedLetters(0, list) - val cc1 = indexedLetters(1, list) - val cc2 = indexedLetters(2, list) - s"[$cc0][$cc1][$cc2][\\.\\w]*" - } - - def apply(): TimeParser = { - TimeParser(new Date) - } - - def apply(time: Long): TimeParser = { - new TimeParser(time) - } - - def apply(date: Date): TimeParser = { - TimeParser(date.getTime) - } - - def apply(tupleDate: ((Int, Int, Int), (Int, Int, Int))): TimeParser = { - val (date, time) = tupleDate - val (yy, mm, day) = date - val (hr, mn, sec) = time - apply("%04d/%02d/%02d %02d:%02d:%02d".format(yy, mm, day, hr, mn, sec)) - } - - lazy val BadParsDate = apply(-1L) - - def apply(datestr: String): TimeParser = { - parseDate(datestr).getOrElse(BadParsDate) - } - - def apply(yy: Int, mm: Int, dd: Int): TimeParser = { - // Calendar month is zero-based - val cal = java.util.Calendar.getInstance - cal.set(yy, mm - 1, dd) - apply(cal.getTime) - } - def apply(date: Any): TimeParser = { - date match { - case tt: Long => apply(tt) - case tt: Date => apply(tt) - case tt: String => apply(tt) - - case (yy: Int, mm: Int, dd: Int) => apply(yy, mm, dd) - - case _ => - eprintf("arg.class == [%s]\n".format(date.getClass.getName)) - sys.error(s"bad date constructor arg[$date]") - } - } - def adjustHour(twoDigitNumber: String, amflag: Boolean, pmflag: Boolean): String = { - var number: Int = if (twoDigitNumber.startsWith("0")) { - twoDigitNumber.substring(1).toInt - } else { - twoDigitNumber.toInt - } - if (amflag) { - if (number == 12) { - number = 0 - } - } else if (pmflag && number < 12) { - number += 12 - } - s"$number" - } - - - def monthAbbrev2Number(name: String): Int = { - name.toLowerCase.substring(0, 3) match { - case "jan" => 1 - case "feb" => 2 - case "mar" => 3 - case "apr" => 4 - case "may" => 5 - case "jun" => 6 - case "jul" => 7 - case "aug" => 8 - case "sep" => 9 - case "oct" => 10 - case "nov" => 11 - case "dec" => 12 - case _ => - hook += 1 - -1 - } - } - def monthName2Number(rawname: String): Int = { - val name = rawname.replaceAll("""(\d+)[a-zA-Z]{2}""", "$1") - if (name.length < 3) { printf("illegal month name parameter: name[%s] rawname[%s]", name, rawname) } - assert(name.length >= 3, s"illegal month name parameter: [$name]") - val abbr = name.toLowerCase.replaceAll("""[^a-zA-Z]+""", "").substring(0, 3) - val result = monthAbbrev2Number(abbr) - assert(result >= 1 && result <= 12) - result - } - - lazy val MonthNamePattern: Regex = """(?i)(.*)\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\b(.*)""".r - lazy val QuadrixBackIssuesFilenameFormat: Regex = """(?i)([jfmasond][aepuco][nbrylgptvc])\D?(\d{1,2})\D(\d{1,4})""".r - def toNumericFormat(dateStrRaw: String): String = { - // "Wed Apr 08 18:17:08 2009" - var dateStr = dateStrRaw.replaceAll("""\b(Sun|Mon|Tue|Wed|Thu|Fri|Sat)[a-z]*\b""", "").replaceAll("""\s*, \s*""", "").trim - dateStr = dateStr match { - case MDY(m, d, y) => - val (mo, da, yr) = (m.toInt, d.toInt, y.toInt) - "%4d-%02d-%02d".format(yr, mo, da) - case DayMonthYr(d, monthname, y) => - val monthNumber = monthName2Number(monthname) - val (yr, mo, da) = (y.toInt, monthNumber, d.toInt) - "%4d-%02d-%02d".format(yr, mo, da) - case MonthNamePattern(pre, _mid, _post) => - val mid = _mid.replaceAll("\\s+", "-").trim - val post = _post.trim - if (pre.isEmpty) { - val mon = wordMap(mid).trim - (s"$mon-$post").replaceAll("[-]+", "-").trim - } else if (post.isEmpty) { - s"${pre.trim}-${wordMap(mid.trim)}" - } else { - s"${pre.trim}-${wordMap(mid.trim)}-${post.trim}" - } - case QuadrixBackIssuesFilenameFormat(monthName, day, year2digit) => - val mo = "%02d".format(monthName2Number(monthName)) - val dy = "%02d".format(day.toInt) - val yr = s"20$year2digit" - s"${yr}-${mo}-${dy}" - - case _ => - sys.error(s"unsupported format[$dateStr") - } - dateStr.trim - } - def wordMap(_str: String): String = { - val str = _str.toLowerCase.trim - str match { - case "jan" | "feb" | "mar" | "apr" | "may" | "jun" | "jul" | "aug" | "sep" | "oct" | "nov" | "dec" => - monthMap(str) - case other if other.matches("""\d+""") => // numeric - other - case _ => - "" // junk - } - } - def reorderYearFirst(_ff: Array[String]): Array[String] = { - var ff = _ff - _ff.indexWhere((s: String) => s.length >= 4 && !s.contains(":") && !s.startsWith("0")) match { - case -1 => - // mostly - case yidx => - val yr = _ff(yidx) - val residue = _ff.take(yidx) ++ ff.drop(yidx+1) - ff = (Seq(yr) ++ residue).toArray - } - - if (ff(2).length == 4) { - // reorder first 3 from mm/dd/yyyy to yyyy/mm/dd - def zpad(s: String): String = { - if (s.length == 1) { - s"0$s" - } else { - s - } - } - val Array(f0, f1, f2) = ff.take(3) - ff(0) = f2 - ff(2) = zpad(f1) - ff(1) = zpad(f0) - } else if (ff.head.length == 4) { - // nothing to do - } else { - throw new RuntimeException("unknown format [%s]".format(ff.mkString(","))) - } - ff - } - def to24hourFormat(dateStrRaw: String): (String, String) = { - var dateStr = dateStrRaw - // standardize delimiters - if (dateStr.trim.isEmpty || dateStr == "null") { - throw new IllegalArgumentException("null dateStr argument") - } - val TZPattern = """(.*)\s([A-Z]{3})$""".r - val zone = dateStr.trim match { - case TZPattern(tstamp, zone) => - dateStr = tstamp - zone - case _ => - "" - } - val amflag = dateStr.endsWith(" AM") - val pmflag = dateStr.endsWith(" PM") - val ff = reorderYearFirst(dateStr.trim.split("""\D+""")) - ff.size match { - case 3 => - assert(!pmflag, "unexpected pmflag==true!") - assert(!amflag, "unexpected amflag==true!") - dateStr = ff.mkString("/") - case 5 => - ff(3) = adjustHour(ff(3), amflag, pmflag) - dateStr = s"${ff.slice(0, 3).mkString("/")} ${ff.slice(3, 5).mkString(":")}" - case 6 => - ff(3) = adjustHour(ff(3), amflag, pmflag) - dateStr = s"${ff.slice(0, 3).mkString("/")} ${ff.slice(3, 6).mkString(":")}" - case 7 => - ff(3) = adjustHour(ff(3), amflag, pmflag) - dateStr = s"${ff.slice(0, 3).mkString("/")} ${ff.slice(3, 6).mkString(":")}" // + "."+ff(6) - case _ => - if (ff.nonEmpty) { - eprintf("ff.size==%d\n".format(ff.size)) - ff.foreach { it => printf("[%s]\n", it) } - } - sys.error(s"dateStr[$dateStr]") - } - (dateStr, zone) - } - - def parseDate(date: Date): TimeParser = TimeParser(date) - def parseDate(date: TimeParser): TimeParser = TimeParser(date.getTime) - - def prepDateString(str: String): (Boolean, String, String) = { - if (str.contains(":") && str.matches(".* 2[0-9]{3}")) { - hook += 1 - } - var yearFirst = false - // format: off - var dateStr = str. - replaceAll("\"", "").trim. // remove quotes - replaceAll(",", "/"). - replaceAll("/", "-"). // cut number of formats in half - replaceFirst("(?i)^([a-z]{3,})(\\d.*)", "$1-$2") // separate apr12-14 into apr-12-14 - // format: on - - if (str.endsWith("PM")) { - hook += 1 - } - if (str.startsWith("08/04/")) { - hook += 1 - } - - if ("""(?i)\b[JFMASOND][aepuco][nbrylgptvc]""".r.findFirstIn(dateStr) != None) { - // assume year is either first or last (never in the middle). - // if Month is first, year must be last - val monthFirst = """^(?i)[JFMASOND][aepuco][nbrylgptvc]""".r.findFirstIn(dateStr) != None - yearFirst = !monthFirst - // must repair 2-digit year before throwing away monthFirst information - if (monthFirst && !dateStr.matches(".*\\d{4}")) { - val testYear = dateStr.replaceAll(".*\\D", "") - // warning: this code will expire in 2050! (bad workaround for a bad problem) - val yearString = testYear match { - case yy if yy > "50" => s"19${yy}" - case it => s"20${it}" - } - dateStr = dateStr.replaceAll("\\d+$", yearString) - } - dateStr = toNumericFormat(dateStr) - } - var zone = "" - - yearFirst = if (dateStr.contains(":")) { // dateStr.contains("AM") || dateStr.contains("PM") || dateStr.matches(".* [-+][0-9]{4}$")) { - val (dstr, zoneStr) = to24hourFormat(dateStr) - dateStr = dstr - zone = zoneStr - true // year is now first - } else { - // assume year first if year is not last - ("""\b[12]\d{3}$|\b[12]\d{3}\s""".r.findFirstIn(dateStr) == None) - } - if (debug) printf("yearFirst: %s\n", yearFirst) - (yearFirst, dateStr, zone) - } - - /** - * Test whether a String is a Date string. - */ - def isDate(text: String): Boolean = { - try { - if (text.matches(""".*\d\d.*""")) { - TimeParser(text) - true - } else { - // a date string requires at least 2 consecutive digits (the year) - // e.g.: Thu, Dec 8, 49 - false - } - } catch { - case _: Exception => - false - } - } - - lazy val legalCharacters: Set[Char] = "0123456789-:/ abcdefghijklmnopqrstuvwxyz.,+()".toSet // time zones can be in parentheses - - def tryFormat(dateStr: String, fmt: SimpleDateFormat): Option[TimeParser] = { - try { - val dt = fmt.parse(dateStr) - val pdate = TimeParser(dt) - Some(pdate) - } catch { - case ee: Exception => - None - } - } - - /** - * Parse date String. - */ - def parseDate(rawdate: String): Option[TimeParser] = { - if (rawdate.startsWith("08/04/")) { - hook += 1 - } - // do a case-insensitive test for legal characters - rawdate.find { cc => !legalCharacters.contains(cc.toLower) }.foreach { cc => - sys.error(s"illegal character [$cc] in date string[$rawdate]") - } - if (!rawdate.matches("""^(?i)[-:/\d\s,\.JanFebMrApyulgSOctNvDi]+$""")) { - throw new IllegalArgumentException(s"bogus date string[$rawdate]") - } - // standardize format to use hyphenated y-m-d rather than y/m/d - val (yearFirst, dateStr, zone) = prepDateString(rawdate) - - var parsOpt: Option[TimeParser] = None - - val dateFormats = relevantFormats(dateStr, yearFirst) - dateFormats.find { testfmt => - tryFormat(dateStr, testfmt).foreach { (pd: TimeParser) => - parsOpt = Some(pd) - // save most recent successful format (try it on first attempt next time) - currentFormat = testfmt - yearFirstFlag = yearFirst - if (debug) printf("<<< [%s], [%s] [%s]\n", dateStr, currentFormat.toPattern, parsOpt) - } - parsOpt.nonEmpty - } - if (debug) printf("date: [%s], yearFirst: %s, currentFormat: [%s]\n", rawdate, yearFirst, currentFormat) - if (parsOpt.isEmpty) { - parsOpt = Option(guessFormat(dateStr)) - } - parsOpt.foreach { mdate => - mdate.zone = zone - } - parsOpt - } - - // format: off - def selfFormat(str: String): String = { - str. - replaceAll("""[-+]\d\d\d\d\b""", "Z"). - replaceAll("""\b(\d\d\d\d)\b""", "yyyy"). - replaceAll(""" \d\d:""", " hh:"). - replaceAll(""" \d:""", " hh:"). - replaceAll("""h:\d\d\b""", "h:mm"). - replaceAll("""h:\d\b""", "h:mm"). - replaceAll("""m:\d\d\b""", "m:ss"). - replaceAll("""m:\d\b""", "m:ss"). - replaceAll("""mm(\d\d?:\d\d?)\b""", " hh:mm"). - replaceAll("""y([-/])\d\d\b""", "y$1MM"). - replaceAll("""y([-/])\d\b""", "y$1MM"). - replaceAll("""M([-/])\d\d\b""", "M$1dd"). - replaceAll("""M([-/])\d\b""", "M$1dd"). - replaceAll("""^\d\d([-/])""", "MM$1"). - replaceAll("""^\d([-/])""", "M$1"). - replaceAll("""M([-/])\d\d\b""", "M$1dd"). - replaceAll("""M([-/])\d\b""", "M$1d"). - replaceAll("""s\.\d+\b""", "s.S"). - replaceAll("""\b[SMTWF][uoera][neduit][a-z]*\b""", "EEE"). // day-of-week - replaceAll("""^\s*,\s*""", ""). - replaceAll("""\b(Jan[uary]*|Feb[uary]*|Mar[ch]*|Apr[ril]*|May|June?|July?|Aug[ust]*|Sep[tembr]*|Oct[tober]*Nov[embr]*|Dec[embr]*)\b""", "MMM"). - replaceAll("""\b\d\d? M""", "dd M"). - replaceAll("""M \d\d?\b""", "M dd"). - replaceAll(""" \(?[A-Z][A-Z]+T\)?""", " ").trim - } - // format: on - - /** - * Guess date format. - * TODO: currently unable to parse "January 12, 1972" !!!! - */ - def guessFormat(date: String): TimeParser = { - val dateStr = date - val sf = selfFormat(date) - val fmt = simpleFormat(sf) - try { - val dt = fmt.parse(date) - TimeParser(dt) - } catch { - case _: Exception => - if (debug) eprintf("failed self-format: [%s] : [%s]\n", date, fmt) - var mdate: TimeParser = null - - // year is easy - var onthefly = date.replaceFirst("""\d\d\d\d""", "yyyy") - - // possible variants of month first - if ("""^\d\D""".r.findFirstIn(onthefly) != None) { - onthefly = onthefly.replaceFirst("""\d""", "M") - } else if ("""^\d\d""".r.findFirstIn(onthefly) != None) { - onthefly = onthefly.replaceFirst("""\d\d""", "MM") - } else { - // month following yyyy - if ("""\D\d\D""".r.findFirstIn(onthefly) != None) { - onthefly = onthefly.replaceFirst("""\d""", "M") - } else if ("""\D\d\d""".r.findFirstIn(onthefly) != None) { - onthefly = onthefly.replaceFirst("""\d\d""", "MM") - } - } - - // day - onthefly = if ("""^M+\D\d\D""".r.findFirstIn(onthefly) != None) { - onthefly.replaceFirst("""(M\D)\d""", "$1d") - } else { - onthefly.replaceFirst("""(M+\D)\d\d""", "$1dd") - } - // before continuing, make sure we got years, months and days - if ("""^\S*\d\S""".r.findFirstIn(onthefly) != None) { - sys.error(s"problem: onthefly[$onthefly] ($dateStr)") - } - onthefly = if ("""\s\d\d""".r.findFirstIn(onthefly) != None) { - onthefly.replaceFirst("""\d\d""", "hh") - } else { - onthefly.replaceFirst("""\d""", "h") - } - onthefly = onthefly.replaceFirst("""\d\d""", "mm") - onthefly = onthefly.replaceFirst("""\d\d""", "ss") - if ("""\.\d+$""".r.findFirstIn(onthefly) != None) { - onthefly = onthefly.replaceFirst("""\d+$""", "S") - } - if ("""\s[AP]M$""".r.findFirstIn(onthefly) != None) { - onthefly = onthefly.replaceFirst("""[AP]M$""", "a") - } - try { - val testFormat = new SimpleDateFormat(onthefly) - mdate = TimeParser(testFormat.parse(dateStr)) - currentFormat = testFormat // successful - yearFirstFlag = onthefly.startsWith("yyyy") - if (!newFormats.contains(onthefly)) { - newFormats += onthefly - if (verbose) eprintf("onthefly[%s] (%s)\n", onthefly, dateStr) - } - } catch { - case _: Exception => - // ee.printStackTrace() - // fall thru if on-the-fly failed - } - mdate - } - } - - def barePunctuation(fmt: String): String = fmt.replaceAll("[a-zA-Z0-9]+", "").replaceAll("/", "-") - - def relevantFormats(literalDate: String, yearFirst: Boolean): List[SimpleDateFormat] = { - val punct = barePunctuation(literalDate) - val list = if (punctuationMap.contains(punct)) { - punctuationMap(punct).sortBy { sdf => -sdf.toPattern.length } // longest patterns first - } else { - if (TimeParser.debug || TimeParser.verbose) { - eprintf("no date format for punct[%s], literalDate[%s]\n", punct, literalDate) - } - List[SimpleDateFormat]() - } - list.filter { yearFirst == _.toPattern.startsWith("y") } - } - - lazy val punctuationMap: Map[String, List[SimpleDateFormat]] = { - var uniqMap = Map[String, List[SimpleDateFormat]]() - dateFormatStrings.map { str => - val punct = barePunctuation(str) - val sfmt = simpleFormat(str) - - val list = if (uniqMap.contains(punct)) { - sfmt :: uniqMap(punct) - } else { - List(sfmt) - } - uniqMap += (punct -> list) - } - uniqMap - } - - def listVariations(orig: String): List[String] = { - val list1 = if (orig.indexOf(":s") >= 0) { - val hh = orig.replaceAll("HH", "hh") - val hha = hh.replaceFirst("(:s+)", "$1 a") - val hhb = hh.replaceFirst("(:s+)", "$1.S") - val hhc = hh.replaceFirst("(:s+)", "$1.S a") - - val origS = orig.replaceAll(":ss", "") - val hhS = origS.replaceAll("HH", "hh") - val hhaS = hhS + " a" - - List(orig, hh, hha, hhb, hhc, origS, hhS, hhaS) - } else { - val hh = orig.replaceAll("HH", "hh") - val hha = hh + " a" - List(orig, hh, hha) - } - val list2 = list1.map { _.replaceAll("dd", "d") } - val list3 = list1.map { _.replaceAll("MM", "M") } - val list4 = list2.map { _.replaceAll("MM", "M") } - - var bass = list1 ::: list2 ::: list3 ::: list4 - val shortHours = bass.map { _.replaceAll("HH", "H").replaceAll("hh", "h") } - bass = bass ::: shortHours - val list = bass - val notime = list.map { _.replaceAll(""" [Hh]\S*""", " ").trim } - val all = (list ::: notime).filter(_.nonEmpty) - val big = all ::: all.map { _ + " Z" } - val accum = big.map { _.replaceAll(" +", " ").trim }.toSet.toList // remove duplicate - accum.sortWith { (a, b) => (a.length > b.length) } - } - - object sysTimer { - var begin = System.currentTimeMillis - def reset(): Unit = { begin = System.currentTimeMillis } - def elapsed: Long = System.currentTimeMillis - begin - def elapsedMillis = elapsed - - def elapsedSeconds: Double = elapsed.toDouble / 1000.0 - def elapsedMinutes: Double = elapsed.toDouble / (60.0 * 1000.0) - def elapsedHours: Double = elapsed.toDouble / (60.0 * 60.0 * 1000.0) - def elapsedDays: Double = elapsed.toDouble / (24.0 * 60.0 * 60.0 * 1000.0) - } - - /** - * NOTE: only HH variant should appear in this list. - */ - lazy val baseFormats: List[String] = List( - // supported input formats, as of 2012-10-04 - "yyyyMMdd", - "yyyy-MM-dd HH:mm:ss", - "yyyy-MM-dd kk:mm:ss", // 24-hour format - "MM-dd-yyyy HH:mm:ss", - "MM-dd HH:mm:ss yyyy" - ) - - def dateFormatStrings: List[String] = baseFormats.map { listVariations }.flatten - - def simpleFormat(fmt: String): SimpleDateFormat = new SimpleDateFormat(fmt, Locale.US) - - /// ============================================================== former object Main - def parse(line: String, zeroTime: Boolean = false): TimeParser = { - val simplified = line.replaceAll("""[\s\(\),]+""", " ").trim - - simplified match { - case DateRegex_01(dystr, moName, yr, time, tz @ _) => - val (yyyy, mm, dd) = getNumbers(yr, moName, dystr) - val tm = if (zeroTime) "00:00:00" else time - val stdfmt = "%4d/%02d/%02d %s".format(yyyy, mm, dd, tm) - // eprintf("dystr:[%s], moName:[%s], yr:[%s], time:[%s], tz:[%s], stdfmt[%s]".format(dystr, moName, yr, time, tz, stdfmt)) - TimeParser(stdfmt) - - case DateRegex_02(dummy @ _, dystr, moName, yr, time, tz @ _) => - val (yyyy, mm, dd) = getNumbers(yr, moName, dystr) - val tm = if (zeroTime) "00:00:00" else time - val stdfmt = "%4d/%02d/%02d %s".format(yyyy, mm, dd, tm) - // eprintf("dystr:[%s], moName:[%s], yr:[%s], time:[%s], tz:[%s], stdfmt[%s] (%s)".format(dystr, moName, yr, time, tz, stdfmt, dummy)) - TimeParser(stdfmt) - - case other => - sys.error(s"unparseable date:[$other]") - } - } - def getNumbers(yr: String, moName: String, dy: String): (Int, Int, Int) = { - def prep(str: String): Int = str.stripPrefix("0").toInt - (prep(yr), monthNumber(moName), prep(dy)) - } - // ================================== - def monthNumber(monthName: String): Int = { - monthName.trim.toLowerCase.substring(0, 3) match { - case "jan" => 1 - case "feb" => 2 - case "mar" => 3 - case "apr" => 4 - case "may" => 5 - case "jun" => 6 - case "jul" => 7 - case "aug" => 8 - case "sep" => 9 - case "oct" => 10 - case "nov" => 11 - case "dec" => 12 - } - } - lazy val DayNames = """([smtwf][uoehra][neduit])""" - lazy val MonthNames = """([jfmasond][aepuco][nbrynlgptvc])""" - lazy val ZoneHours = """([-+]\d{4})""" - lazy val TimeRegex = """(\d{2}:\d{2}:\d{2})""" - lazy val YearRegex = """([12][01]\d{2})""" - lazy val DayNumber = """(\d{1,2})""" - lazy val Spc = """[\s+]""" - - // format: off - lazy val DateRegex_01: Regex = ("(?i).*"+ DayNumber+Spc+MonthNames+Spc+YearRegex+Spc+TimeRegex+Spc+ZoneHours+".*").r - lazy val DateRegex_02: Regex = ("(?i).*"+ DayNames+Spc+DayNumber+Spc+MonthNames+Spc+YearRegex+Spc+TimeRegex+Spc+ZoneHours+".*").r - // format: on - - // import java.time.LocalDateTime - // type DateTime = java.time.LocalDateTime - def quikDate(yyyyMMdd: String): DateTime = { - assert(yyyyMMdd.matches("2[0-9]{7}"), s"bad date [$yyyyMMdd], expecting yyyyMMdd") - val y = yyyyMMdd.take(4).toInt - val m = yyyyMMdd.drop(4).take(2).toInt - val d = yyyyMMdd.drop(6).take(2).toInt - java.time.LocalDateTime.of(y, m, d, 0, 0, 0) - } - def ymdDate: String => DateTime = quikDate // alias -} - -class TimeParser(msec: Long) extends Ordered[TimeParser] { - import TimeParser.* - - var zone: String = "" - var outfmt: DateFormat = TimeParser.outfmt // inherit the current default - - // return self, to permit this usage: date.dateOnly.toString -// def dateAndTime: TimeParser = { -// outfmt = dateTimeFormat -// this -// } - - // TODO: this sets global mode for default printing of date format -// def dateOnly: TimeParser = { -// outfmt = dateOnlyFormat -// this -// } - - private val cal = java.util.Calendar.getInstance - cal.setTimeInMillis(msec) - private val date = cal.getTime - -// val stringValue: String = dateTimeFormat.format(date).replaceAll("""\s+00:00:00""", "") - -// def toTuple: ((Int, Int, Int), (Int, Int, Int)) = { -// val date = (getYear, getMonth, getDay) -// val time = (getHour, getMinute, getSecond) -// (date, time) -// } - - def getTime: Long = date.getTime - - // alias - def getEpoch: Long = date.getTime - - def toDate: Date = new Date(getEpoch) - - def copyCalendar(): Calendar = { - val tcal = java.util.Calendar.getInstance - tcal.setTimeInMillis(getEpoch) - tcal - } -// def compareTo(that: TimeParser): Int = { -// if (this < that) -1 -// else if (this > that) 1 -// else 0 -// } -// def compare(x: TimeParser, y: TimeParser) = x compareTo y - def compare(that: TimeParser): Int = this compareTo that - -// def isLeapYear: Boolean = gcal.isLeapYear(getYear) - -// def < (other: TimeParser): Boolean = { getTime < other.getTime } -// def <= (other: TimeParser): Boolean = { getTime <= other.getTime } -// def > (other: TimeParser): Boolean = { getTime > other.getTime } -// def >= (other: TimeParser): Boolean = { getTime >= other.getTime } - - override def toString: String = { - outfmt.format(date).replaceAll("""\s+00:00:00""", "") - } - def toString(fmt: String, locale: Locale = Locale.US): String = { - val fixfmt = fmt.replace("T", " ") - val df: DateFormat = new SimpleDateFormat(fixfmt, locale) - df.format(date) - } - - def nextDay: TimeParser = addDays(1) - - def addMilliseconds(milliseconds: Int): TimeParser = TimeParser(getEpoch + milliseconds) - - def addSeconds(seconds: Int): TimeParser = addMilliseconds(seconds * 1000) - def addMinutes(minutes: Int): TimeParser = addSeconds(minutes * 60) -// def addHours(hours: Int): TimeParser = addMinutes(hours * 60) - - def between(a: TimeParser, b: TimeParser): Boolean = { - assert(a <= b) - this >= a && this <= b - } - - def addDays(days: Int): TimeParser = { - val tcal = copyCalendar() - tcal.add(Calendar.DAY_OF_YEAR, days) - TimeParser(tcal.getTime) - } - // time elapsed since previousTime - def elapsedMilliSeconds(previousTime: TimeParser): Long = { - val t0: Long = previousTime.getTime - val t1: Long = this.getTime - if (t0 > t1) { - t0 - t1 - } else { - t1 - t0 - } - } - def elapsedSeconds(previousTime: TimeParser): Long = { - elapsedMilliSeconds(previousTime) / 1000 - } - def elapsedMinutes(previousTime: TimeParser): BigDecimal = { - BigDecimal(elapsedSeconds(previousTime) / 60.0) - } - def elapsedHours(previousTime: TimeParser): BigDecimal = { - elapsedMinutes(previousTime) / 60.0 - } - def elapsedDays(previousTime: TimeParser): BigDecimal = { - elapsedHours(previousTime) / 24.0 - } - - // duration methods that return approximate answers - lazy val averageMonthSize: Double = (31 + 28.25 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31) / 12.0 - def elapsedMonths(previousTime: TimeParser): BigDecimal = { - elapsedDays(previousTime) / averageMonthSize - } - def elapsedYears(previousTime: TimeParser): BigDecimal = { - elapsedDays(previousTime) / 365.25 - } - - def toLongString: String = { - dateTimeFormat.format(this) - } - // this would be an override, if extending Date - def getYear: Int = { - cal.get(Calendar.YEAR) - } - - // @return 1-based month rather than zero based. - // this would be an override, if extending Date - def getMonth: Int = { - cal.get(Calendar.MONTH) + 1 // compatible with java.util.Date - } - def getDayOfMonth: Int = getDay - - // this would be an override, if extending Date - def getDay: Int = { - cal.get(Calendar.DAY_OF_MONTH) // compatible with java.util.Date - } - def dayOfWeek: Int = { - cal.get(Calendar.DAY_OF_WEEK) - } - def dayOfWeekName: String = { - cal.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, Locale.US) - } - - def getHour: Int = { - cal.get(Calendar.HOUR_OF_DAY) - } - def getMinute: Int = { - cal.get(Calendar.MINUTE) - } - def getSecond: Int = { - cal.get(Calendar.SECOND) - } -} - -object LongIso { - // Print date in standard sortable long-iso format. - def main(args: Array[String]): Unit = { - try { - for (arg <- args) { - printf("%s\n", TimeParser(arg)) - } - } catch { - case ee: Exception => - showLimitedStack(ee) - } - } -} diff --git a/src/test/scala/vastblue/file/EzPathTest.scala b/src/test/scala/vastblue/file/EzPathTest.scala index 317634f..5ab7845 100644 --- a/src/test/scala/vastblue/file/EzPathTest.scala +++ b/src/test/scala/vastblue/file/EzPathTest.scala @@ -34,7 +34,7 @@ class EzPathTest extends AnyFunSpec with Matchers with BeforeAndAfter { val unxa = PathUnx(upathstr) // should match java.nio.file.Paths.get printf("unxa.pstr [%s], ", unxa.initstring) - printf("unxa.norm [%s], ", unxa.posx) + printf("unxa.posx [%s], ", unxa.posx) printf("unxa.sl [%s], ", unxa.sl) printf("unxa.abs [%s]\n", unxa.abs) // printf("\n") @@ -44,7 +44,7 @@ class EzPathTest extends AnyFunSpec with Matchers with BeforeAndAfter { val unxb = PathUnx(upathstr) // should match java.nio.file.Paths.get printf("unxb.pstr [%s], ", unxb.initstring) - printf("unxb.norm [%s], ", unxb.posx) + printf("unxb.posx [%s], ", unxb.posx) printf("unxb.sl [%s], ", unxb.sl) printf("unxb.abs [%s]\n", unxb.abs) // printf("\n") @@ -57,7 +57,7 @@ class EzPathTest extends AnyFunSpec with Matchers with BeforeAndAfter { it("PathWin should display with constructed slash type") { val wina = PathWin(upathstr) // should match java.nio.file.Paths.get printf("wina.pstr [%s], ", wina.initstring) - printf("wina.norm [%s], ", wina.posx) + printf("wina.posx [%s], ", wina.posx) printf("wina.sl [%s], ", wina.sl) printf("wina.abs [%s]\n", wina.abs) // printf("\n") @@ -67,7 +67,7 @@ class EzPathTest extends AnyFunSpec with Matchers with BeforeAndAfter { val winb = PathWin(upathstr) // should match java.nio.file.Paths.get printf("winb.pstr [%s], ", winb.initstring) - printf("winb.norm [%s], ", winb.posx) + printf("winb.posx [%s], ", winb.posx) printf("winb.sl [%s], ", winb.sl) printf("winb.abs [%s]\n", winb.abs) // printf("\n") @@ -77,7 +77,7 @@ class EzPathTest extends AnyFunSpec with Matchers with BeforeAndAfter { val winw = PathWin(windowsAbsstr) printf("winw.pstr [%s], ", winw.initstring) - printf("winw.norm [%s], ", winw.posx) + printf("winw.posx [%s], ", winw.posx) printf("winw.sl [%s], ", winw.sl) printf("winw.abs [%s]\n", winw.abs) // printf("\n") @@ -90,7 +90,7 @@ class EzPathTest extends AnyFunSpec with Matchers with BeforeAndAfter { it("EzPath should display with os-appropriate slash type") { val ezpc = EzPath(wpathstr) // should match java.nio.file.Paths.get printf("ezpc.pstr [%s], ", ezpc.initstring) - printf("ezpc.norm [%s], ", ezpc.posx) + printf("ezpc.posx [%s], ", ezpc.posx) printf("ezpc.sl [%s], ", ezpc.sl) printf("ezpc.abs [%s]\n", ezpc.abs) // printf("\n") @@ -105,7 +105,7 @@ class EzPathTest extends AnyFunSpec with Matchers with BeforeAndAfter { val ezpd = EzPath(wpathstr) // should match java.nio.file.Paths.get printf("ezpd.pstr [%s], ", ezpd.initstring) - printf("ezpd.norm [%s], ", ezpd.posx) + printf("ezpd.posx [%s], ", ezpd.posx) printf("ezpd.sl [%s], ", ezpd.sl) printf("ezpd.abs [%s]\n", ezpd.abs) // printf("\n") @@ -120,7 +120,7 @@ class EzPathTest extends AnyFunSpec with Matchers with BeforeAndAfter { val ezxw = EzPath(wpathstr, Slash.Win) // should match specified (same as default) slash printf("ezxw.pstr [%s], ", ezxw.initstring) - printf("ezxw.norm [%s], ", ezxw.posx) + printf("ezxw.posx [%s], ", ezxw.posx) printf("ezxw.sl [%s], ", ezxw.sl) printf("ezxw.abs [%s]\n", ezxw.abs) // printf("\n") @@ -135,7 +135,7 @@ class EzPathTest extends AnyFunSpec with Matchers with BeforeAndAfter { val ezpu = EzPath(wpathstr, Slash.Unx) // should match non-default slash printf("ezpu.pstr [%s], ", ezpu.initstring) - printf("ezpu.norm [%s], ", ezpu.posx) + printf("ezpu.posx [%s], ", ezpu.posx) printf("ezpu.sl [%s], ", ezpu.sl) printf("ezpu.abs [%s]\n", ezpu.abs) // printf("\n") diff --git a/src/test/scala/vastblue/time/ChronoParseTests.scala b/src/test/scala/vastblue/time/ChronoParseTests.scala index f788330..acc7953 100644 --- a/src/test/scala/vastblue/time/ChronoParseTests.scala +++ b/src/test/scala/vastblue/time/ChronoParseTests.scala @@ -1,7 +1,7 @@ package vastblue.time //import vastblue.pallet.* -import vastblue.time.TimeParser +//import vastblue.time.TimeParser import vastblue.time.TimeDate.{parseDateTime => parseDate} import vastblue.time.TimeDate.* @@ -28,6 +28,7 @@ class ChronoParseTests extends AnyFunSpec with Matchers { } } + /* describe("TimeParser") { for ((teststr, expected) <- TestDates.dateStrings) { if (!badDates.contains(teststr)) { @@ -65,6 +66,7 @@ class ChronoParseTests extends AnyFunSpec with Matchers { } } } + */ } // format: off object TestDates {