From 6b7c2b08d29230713c3290c3398ee3f2ccbca89f Mon Sep 17 00:00:00 2001 From: MatloaItumeleng Date: Tue, 5 May 2026 10:13:39 +0200 Subject: [PATCH] workaround , subtracting single-quote characters --- .../time/DateTimePattern.scala | 35 ++++++++++++++----- .../time/DateTimePatternSuite.scala | 11 ++++++ .../types/parsers/DateTimeParserSuite.scala | 13 +++++++ 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/main/scala/za/co/absa/standardization/time/DateTimePattern.scala b/src/main/scala/za/co/absa/standardization/time/DateTimePattern.scala index 068f071..a0d4132 100644 --- a/src/main/scala/za/co/absa/standardization/time/DateTimePattern.scala +++ b/src/main/scala/za/co/absa/standardization/time/DateTimePattern.scala @@ -129,24 +129,41 @@ object DateTimePattern { override val defaultTimeZone: Option[String] = assignedDefaultTimeZone.filterNot(_ => timeZoneInPattern) override val isTimeZoned: Boolean = timeZoneInPattern || defaultTimeZone.nonEmpty - val (millisecondsPosition, microsecondsPosition, nanosecondsPosition) = analyzeSecondFractionsPositions(pattern) + private val (patternMilliPos, patternMicroPos, patternNanoPos) = scanSecondFractionsInPattern(pattern) + override val patternWithoutSecondFractions: String = { + val patternSections = Section.mergeTouchingSectionsAndSort(Seq(patternMilliPos, patternMicroPos, patternNanoPos).flatten) + Section.removeMultipleFrom(pattern, patternSections) + } + + val (millisecondsPosition, microsecondsPosition, nanosecondsPosition) = adjustPositionsForValue(pattern, patternMilliPos, patternMicroPos, patternNanoPos) override val secondFractionsSections: Seq[Section] = Section.mergeTouchingSectionsAndSort(Seq(millisecondsPosition, microsecondsPosition, nanosecondsPosition).flatten) - override val patternWithoutSecondFractions: String = Section.removeMultipleFrom(pattern, secondFractionsSections) private def scanForPlaceholder(withinString: String, placeHolder: Char): Option[Section] = { val start = withinString.findFirstUnquoted(Set(placeHolder), Set('\'')) start.map(index => Section.ofSameChars(withinString, index)) } - private def analyzeSecondFractionsPositions(withinString: String): (Option[Section], Option[Section], Option[Section]) = { - val clearedPattern = withinString - - // TODO as part of #7 fix (originally Enceladus#677) - val milliSP = scanForPlaceholder(clearedPattern, patternMilliSecondChar) - val microSP = scanForPlaceholder(clearedPattern, patternMicroSecondChar) - val nanoSP = scanForPlaceholder(clearedPattern, patternNanoSecondChat) + private def scanSecondFractionsInPattern(withinString: String): (Option[Section], Option[Section], Option[Section]) = { + val milliSP = scanForPlaceholder(withinString, patternMilliSecondChar) + val microSP = scanForPlaceholder(withinString, patternMicroSecondChar) + val nanoSP = scanForPlaceholder(withinString, patternNanoSecondChat) (milliSP, microSP, nanoSP) } + + private def adjustPositionsForValue( + pat: String, + milliPos: Option[Section], + microPos: Option[Section], + nanoPos: Option[Section] + ): (Option[Section], Option[Section], Option[Section]) = { + def adjust(sectionOpt: Option[Section]): Option[Section] = { + sectionOpt.map { s => + val quotesBefore = pat.substring(0, s.start).count(_ == '\'') + s.copy(start = s.start - quotesBefore) + } + } + (adjust(milliPos), adjust(microPos), adjust(nanoPos)) + } } private final case class StandardDTPattern(override val pattern: String, diff --git a/src/test/scala/za/co/absa/standardization/time/DateTimePatternSuite.scala b/src/test/scala/za/co/absa/standardization/time/DateTimePatternSuite.scala index 6945d8b..f589b9e 100644 --- a/src/test/scala/za/co/absa/standardization/time/DateTimePatternSuite.scala +++ b/src/test/scala/za/co/absa/standardization/time/DateTimePatternSuite.scala @@ -268,4 +268,15 @@ class DateTimePatternSuite extends AnyFunSuite { assert(dtp.patternWithoutSecondFractions == "yyyy-MM-dd HH:mm:ss") assert(!dtp.containsSecondFractions) } + + test("Second fractions detection in regular pattern with quoted literal - milliseconds") { + val pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" + val dtp = DateTimePattern(pattern) + assert(dtp.millisecondsPosition.contains(Section(20, 3))) + assert(dtp.microsecondsPosition.isEmpty) + assert(dtp.nanosecondsPosition.isEmpty) + assert(dtp.secondFractionsSections == Seq(Section(20, 3))) + assert(dtp.patternWithoutSecondFractions == "yyyy-MM-dd'T'HH:mm:ss.XXX") + assert(dtp.containsSecondFractions) + } } diff --git a/src/test/scala/za/co/absa/standardization/types/parsers/DateTimeParserSuite.scala b/src/test/scala/za/co/absa/standardization/types/parsers/DateTimeParserSuite.scala index 9511eae..8afa34d 100644 --- a/src/test/scala/za/co/absa/standardization/types/parsers/DateTimeParserSuite.scala +++ b/src/test/scala/za/co/absa/standardization/types/parsers/DateTimeParserSuite.scala @@ -186,6 +186,19 @@ class DateTimeParserSuite extends AnyFunSuite{ assert(parser7.format(t) == "(789) 1970-01-02 (123) 01:00:00 (456)") } + test("DateParser class actual pattern with quoted literal and timezone and milliseconds") { + val parser = DateTimeParser("yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + val str = "2025-07-18T16:07:51.569+02:00" + + val resultDate: Date = parser.parseDate(str) + val expectedDate: Date = Date.valueOf("2025-07-18") + assert(resultDate == expectedDate) + + val resultTimestamp: Timestamp = parser.parseTimestamp(str) + val expectedTimestamp: Timestamp = Timestamp.valueOf("2025-07-18 14:07:51.569") + assert(resultTimestamp == expectedTimestamp) + } + test("Lenient interpretation is not accepted") { //first lenient interpretation val pattern = "dd-MM-yyyy"