All Downloads are FREE. Search and download functionalities are using the official Maven repository.

zio.schema.validation.Time.scala Maven / Gradle / Ivy

The newest version!
package zio.schema.validation

import scala.annotation.tailrec
import scala.collection.mutable

trait Time {

  /**
   * Format is almost the same as the one used by the java.time.format.DateTimeFormatter class.
   *
   *  a           AM/PM always 2 letters
   *  h           1-12 hour 1 or 2 digits
   *  hh          01-12 hour always 2 digits
   *  H           0-23 hour 1 or 2 digits
   *  HH          00-23 hour always 2 digits
   *  m           0-59 minute 1 or 2 digits
   *  mm          00-59 minute always 2 digits
   *  s           0-59 second 1 or 2 digits
   *  ss          00-59 second always 2 digits
   *
   *  S           0-9 fraction of seconds 1 digits
   *  ..
   *  SSSSSSSSS   000000000-999999999 maximum number of digits is 9
   *
   * All other letters are reserved.
   *
   * Examples:
   * HH:mm
   * 01:10
   * HH:mm:ss
   * 11:10:30
   * HH:mm:ss.SSSSSSSSS
   * 21:10:30.123456789
   * HH:mm a
   * 01:10 AM
   * h:mm:ss
   * 1:10:30
   *
   */
  def time(format: String): Validation[String] = {
    val regex = parseFormat(format)
    Validation.regex(regex)
  }

  sealed private trait Field
  private case class TimeField(letter: Char, length: Int, maxLength: Int) extends Field
  private case class Literal(value: String)                               extends Field

  private val fields = Map[Char, Field](
    'H' -> TimeField('H', 1, 2),
    'h' -> TimeField('h', 1, 2),
    'm' -> TimeField('m', 1, 2),
    's' -> TimeField('s', 1, 2),
    'a' -> TimeField('a', 1, 1),
    'S' -> TimeField('S', 1, 9)
  )

  private def parseFormat(format: String): Regex = {
    val length               = format.length
    var pos                  = 0
    var field: Option[Field] = None
    val usedFields           = mutable.ListBuffer.empty[Field]
    val result               = mutable.ListBuffer.empty[Field]

    def setField(cur: Char): Unit = {
      field = fields.get(cur)

      field.fold {
        if (cur >= 'a' && cur <= 'z' || cur >= 'A' && cur <= 'Z') {
          throw new IllegalArgumentException(s"Invalid time format: $format. All letters are reserved.")
        } else {
          field = Some(Literal(cur.toString))
        }
      } { f =>
        if (usedFields.contains(f)) {
          throw new IllegalArgumentException(s"Character $cur already used in format $format")
        } else {
          usedFields += f
        }
      }
    }

    while (pos < length) {
      val cur = format.charAt(pos)

      field match {
        case None => setField(cur)
        case Some(f @ TimeField(letter, _, _)) if (letter != cur) =>
          result += f
          setField(cur)
        case Some(TimeField(letter, length, maxLength)) if (length == maxLength) =>
          throw new IllegalArgumentException(s"Invalid time format: $format max length for ${letter} is ${maxLength}")
        case Some(f @ TimeField(_, length, _)) =>
          field = Some(f.copy(length = length + 1))
        case Some(l @ Literal(_)) if fields.contains(cur) =>
          result += l
          setField(cur)
        case Some(l @ Literal(value)) =>
          field = Some(l.copy(value = value + cur))

      }

      pos += 1

    }
    field.foreach(f => result += f)

    @tailrec
    def loop(list: List[Field], regex: Seq[Regex]): Seq[Regex] = list match {
      case Nil          => regex
      case head :: tail => loop(tail, regex :+ fieldToRegex(head))
    }

    if (usedFields.isEmpty) {
      throw new IllegalArgumentException(s"There is no time field (${fields.keySet.mkString(",")}) in format $format")
    }

    val regexes = loop(result.toList, Seq.empty)

    if (regexes.isEmpty) {
      throw new IllegalArgumentException(s"Invalid time format: $format")
    } else {
      regexes.reduce(_ ~ _)
    }
  }

  private val from20to24 = Regex.oneOf('2') ~ Regex.between('0', '4')
  private val from10to19 = Regex.oneOf('1') ~ Regex.digit
  private val from00to09 = Regex.oneOf('0') ~ Regex.digit
  private val from00to19 = Regex.oneOf('0', '1') ~ Regex.digit
  private val from10to12 = Regex.oneOf('1') ~ Regex.oneOf('0', '1', '2')
  private val from10to59 = Regex.between('1', '5') ~ Regex.digit
  private val from00to59 = Regex.between('0', '5') ~ Regex.digit
  private val from0to9   = Regex.digit

  private def fieldToRegex(field: Field): Regex = field match {
    case TimeField('H', 1, _)                        => from20to24 | from10to19 | from00to09 | from0to9
    case TimeField('H', 2, _)                        => from00to19 | from20to24
    case TimeField('h', 1, _)                        => from10to12 | from00to09 | from0to9
    case TimeField('h', 2, _)                        => from00to09 | from10to12
    case TimeField('m', 1, _) | TimeField('s', 1, _) => from10to59 | from00to09 | from0to9
    case TimeField('m', 2, _) | TimeField('s', 2, _) => from00to59
    case TimeField('S', length, _)                   => Regex.digit.between(length, length)
    case TimeField('a', _, _)                        => Regex.oneOf('A', 'P').between(1, 1) ~ Regex.oneOf('M').between(1, 1)
    case TimeField(_, _, _) =>
      throw new IllegalArgumentException(s"Something went terribly wrong. This is a bug. Please report it.")
    case Literal(l) => l.map(c => Regex.oneOf(c)).reduce(_ ~ _)
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy