Skip to content

Commit

Permalink
Merge pull request #36 from cibotech/LocalDateAndInstantParsers
Browse files Browse the repository at this point in the history
Implement Instant.parse and LocalDate.parse.
  • Loading branch information
sjrd committed Mar 28, 2018
2 parents d58708a + 94b5579 commit 24f76a4
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 21 deletions.
4 changes: 4 additions & 0 deletions src/main/scala/java/time/Constants.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@ private[time] object Constants {
final val SECONDS_IN_WEEK = SECONDS_IN_DAY * 7

final val SECONDS_IN_MONTH = 2629746

final val DAYS_IN_LEAP_YEAR = 366

final val DAYS_IN_YEAR = 365
}
129 changes: 113 additions & 16 deletions src/main/scala/java/time/Instant.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ package java.time

import scala.scalajs.js

import java.time.Preconditions.requireDateTimeParse
import java.time.chrono.IsoChronology
import java.time.format.DateTimeParseException
import java.time.temporal._

import scala.util.control.NonFatal

/** Created by alonsodomin on 26/12/2015. */
final class Instant private (private val seconds: Long, private val nanos: Int)
extends TemporalAccessor with Temporal with TemporalAdjuster
Expand Down Expand Up @@ -239,16 +244,14 @@ final class Instant private (private val seconds: Long, private val nanos: Int)
override def hashCode(): Int = (seconds + 51 * nanos).hashCode

override def toString: String = {
def tenThousandPartsBasedOnZero: (Long, Long) = {
def tenThousandPartsAndRemainder: (Long, Long) = {
if (seconds < -secondsFromZeroToEpoch) {
val zeroSecs = seconds + secondsFromZeroToEpoch
val quot = zeroSecs / secondsInTenThousandYears
val rem = zeroSecs % secondsInTenThousandYears
val quot = seconds / secondsInTenThousandYears
val rem = seconds % secondsInTenThousandYears
(quot, rem)
} else {
val zeroSecs = seconds - secondsInTenThousandYears + secondsFromZeroToEpoch
val quot = Math.floorDiv(zeroSecs, secondsInTenThousandYears) + 1
val rem = Math.floorMod(zeroSecs, secondsInTenThousandYears)
val quot = Math.floorDiv(seconds, secondsInTenThousandYears)
val rem = Math.floorMod(seconds, secondsInTenThousandYears)
(quot, rem)
}
}
Expand All @@ -259,23 +262,29 @@ final class Instant private (private val seconds: Long, private val nanos: Int)
(LocalDate.ofEpochDay(epochDay), LocalTime.ofSecondOfDay(secondsOfDay).withNano(nanos))
}

val (hi, lo) = tenThousandPartsBasedOnZero
val epochSecond = lo - secondsFromZeroToEpoch
val (hi, lo) = tenThousandPartsAndRemainder
val epochSecond = lo
val (date, time) = dateTime(epochSecond)

val hiPart = {
if (hi > 0) s"+$hi"
else if (hi < 0) hi.toString
else ""
val years = hi * 10000 + date.getYear

val yearSegment = {
if (years > 9999) s"+$years"
else if (years < 0 && years > -1000) "-%04d".format(Math.abs(years))
else years.toString
}

val monthSegement = "%02d".format(date.getMonthValue)
val daySegment = "%02d".format(date.getDayOfMonth)

val timePart = {
val timeStr = time.toString
if (time.getSecond == 0 && time.getNano == 0) timeStr + ":00"
else timeStr
}

s"${hiPart}${date}T${timePart}Z"
val dateSegment = s"$yearSegment-$monthSegement-$daySegment"
s"${dateSegment}T${timePart}Z"
}

// Not implemented
Expand All @@ -287,12 +296,17 @@ object Instant {
import Constants._
import ChronoField._

private final val iso = IsoChronology.INSTANCE

final val EPOCH = new Instant(0, 0)

private val MinSecond = -31557014167219200L
private val MaxSecond = 31556889864403199L
private val MaxNanosInSecond = 999999999

private val MaxYear = 1000000000
private val MinYear = -1000000000

/*
* 146097 days in 400 years
* 86400 seconds in a day
Expand Down Expand Up @@ -335,7 +349,90 @@ object Instant {
ofEpochSecond(temporal.getLong(INSTANT_SECONDS), temporal.getLong(NANO_OF_SECOND))
}

// Not implemented
// def parse(text: CharSequence): Instant
private def parseSegment(segment: String, classifier: String): Int = {
try {
segment.toInt
} catch {
case _: NumberFormatException =>
throw new DateTimeParseException(s"$segment is not a valid $classifier",
segment, 0)
}
}

private def toEpochDay(year: Int, month: Int, day: Int): Long = {
val leapYear = iso.isLeapYear(year)

val extremeLeapYear = 999999996
val epochDaysToAccountForExtreme = (3 * DAYS_IN_YEAR) + DAYS_IN_LEAP_YEAR

requireDateTimeParse(year <= MaxYear || year >= MinYear,
s"$year out of bounds, year > 1000000000 || year < -1000000000",
year.toString, 0)

val monthDay = MonthDay.of(month, day)
if (monthDay.getMonth == Month.FEBRUARY && leapYear) {
requireDateTimeParse(monthDay.getDayOfMonth <= 29,
"Day range out of bounds <= 29 for leap years", day.toString, 0)
}

if (year == MaxYear)
LocalDate.of(extremeLeapYear, month, day).toEpochDay + epochDaysToAccountForExtreme
else if (year == MinYear)
LocalDate.of(-extremeLeapYear, month, day).toEpochDay - epochDaysToAccountForExtreme
else
LocalDate.of(year, month, day).toEpochDay
}

def parse(text: CharSequence): Instant = {
try {
val pattern = """(^[-+]?)(\d*)-(\d*)-(\d*)T(\d*):(\d*):(\d*).?(\d*)Z""".r
val pattern(sign, yearSegment, monthSegment, daySegment,
hourSegment, minutesSegment, secondsSegment, nanosecondsSegment) = text

val year = parseSegment(sign + yearSegment, "year")
val month = parseSegment(monthSegment, "month")
val day = parseSegment(daySegment, "day")
val nanoPower = 9

requireDateTimeParse(!((sign != "+") && (year > 9999)),
s"year > 9999 must be preceded by [+]", text, 0)

val days = toEpochDay(year, month, day)
val dayOffset = days

val hourOffset = parseSegment(hourSegment, "hour")
val minuteOffset = parseSegment(minutesSegment, "minutes")
val secondsOffset = parseSegment(secondsSegment, "seconds")

requireDateTimeParse(hourOffset <= HOURS_IN_DAY,
s"hours are > $HOURS_IN_DAY", text, 0)

requireDateTimeParse(minuteOffset <= MINUTES_IN_HOUR,
s"minutes are > $MINUTES_IN_HOUR", text, 0)

requireDateTimeParse(secondsOffset <= SECONDS_IN_MINUTE,
s"seconds are > $SECONDS_IN_MINUTE", text, 0)

val nanos = if (nanosecondsSegment != "") {
val scale = Math.pow(10, nanoPower - nanosecondsSegment.length).toInt
parseSegment(nanosecondsSegment, "nanoseconds") * scale
} else {
0
}

val epochSecondsOffset = {
dayOffset * SECONDS_IN_DAY +
hourOffset * SECONDS_IN_HOUR +
minuteOffset * SECONDS_IN_MINUTE +
secondsOffset
}

new Instant(epochSecondsOffset, nanos)
} catch {
case err: DateTimeParseException =>
throw err
case NonFatal(err) =>
throw new DateTimeParseException(s"Invalid date $text", text, 0)
}
}
}
42 changes: 37 additions & 5 deletions src/main/scala/java/time/LocalDate.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package java.time

import scala.scalajs.js

import java.time.chrono.{IsoEra, Era, IsoChronology, ChronoLocalDate}
import java.time.chrono._
import java.time.format.DateTimeParseException
import java.time.temporal._

import scala.util.control.NonFatal

final class LocalDate private (year: Int, month: Month, dayOfMonth: Int)
extends ChronoLocalDate with Serializable {
import Preconditions.requireDateTime
Expand Down Expand Up @@ -99,7 +102,7 @@ final class LocalDate private (year: Int, month: Month, dayOfMonth: Int)

def lengthOfMonth(): Int = month.length(_isLeapYear)

def lengthOfYear(): Int = if (_isLeapYear) 366 else 365
override def lengthOfYear(): Int = if (_isLeapYear) 366 else 365

override def `with`(adjuster: TemporalAdjuster): LocalDate =
adjuster.adjustInto(this).asInstanceOf[LocalDate]
Expand Down Expand Up @@ -372,7 +375,7 @@ final class LocalDate private (year: Int, month: Month, dayOfMonth: Int)
}

object LocalDate {
import Preconditions.requireDateTime
import Preconditions._

private final val iso = IsoChronology.INSTANCE

Expand Down Expand Up @@ -431,8 +434,37 @@ object LocalDate {
ofEpochDay(temporal.getLong(ChronoField.EPOCH_DAY))
}

// TODO
// def parse(text: CharSequence): LocalDate
private def parseSegment(segment: String, classifier: String): Int = {
try {
segment.toInt
} catch {
case _: NumberFormatException =>
throw new DateTimeParseException(s"$segment is not a valid $classifier",
segment, 0)
}
}

def parse(text: CharSequence): LocalDate = {
try {
val pattern = """(^[-+]?)(\d*)-(\d*)-(\d*)""".r
val pattern(sign, yearSegment, monthSegment, daySegment) = text

val year = parseSegment(sign + yearSegment, "year")
val month = parseSegment(monthSegment, "month")
val day = parseSegment(daySegment, "day")

requireDateTimeParse(!((sign != "+") && (year > 9999)),
s"year > 9999 must be preceded by [+]", text, 0)

LocalDate.of(year.toInt, month.toInt, day.toInt)
} catch {
case err: DateTimeParseException =>
throw err
case NonFatal(err) =>
throw new DateTimeParseException(s"Invalid date $text", text, 0)
}
}

// def parse(text: CharSequence,
// formatter: java.time.format.DateTimeFormatter): LocalDate
}
8 changes: 8 additions & 0 deletions src/main/scala/java/time/Preconditions.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
package java.time

import java.time.format.DateTimeParseException

private[time] object Preconditions {
// Like scala.Predef.require, but throws a DateTimeException.
def requireDateTime(requirement: Boolean, message: => Any): Unit = {
if (!requirement)
throw new DateTimeException(message.toString)
}

def requireDateTimeParse(requirement: Boolean, message: => Any,
charSequence: CharSequence, index: Int): Unit = {
if (!requirement)
throw new DateTimeParseException(message.toString, charSequence, index)
}
}
14 changes: 14 additions & 0 deletions src/main/scala/java/time/format/DateTimeParseException.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package java.time.format

import java.time.DateTimeException

class DateTimeParseException(message: String, parsedData: CharSequence,
errorIndex: Int, cause: Throwable)
extends DateTimeException(message, cause) {

def this(message: String, parsedData: CharSequence, errorIndex: Int) =
this(message, parsedData, errorIndex, null)

def getErrorIndex(): Int = errorIndex
def getParsedString(): String = parsedData.toString
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.scalajs.testsuite.javalib.time

import java.time._
import java.time.format.DateTimeParseException
import java.time.temporal.{UnsupportedTemporalTypeException, ChronoUnit, ChronoField}

import org.junit.Test
Expand Down Expand Up @@ -373,11 +374,14 @@ class InstantTest extends TemporalTest[Instant] {
@Test def toStringOutput(): Unit = {
assertEquals("1970-01-01T00:00:00Z", Instant.EPOCH.toString)
assertEquals("-1000000000-01-01T00:00:00Z", Instant.MIN.toString)
assertEquals("-999999999-01-01T00:00:00Z", Instant.MIN.plus(366, DAYS).toString)

// https://github.com/scala-js/scala-js-java-time/issues/23
assertEquals("1970-01-01T00:10:00.100Z", Instant.EPOCH.plus(10, MINUTES).plusMillis(100).toString)

assertEquals("+1000000000-12-31T23:59:59.999999999Z", Instant.MAX.toString)
assertEquals("+999999999-12-31T23:59:59.999999999Z", Instant.MAX.minus(366, DAYS).toString)

assertEquals("1999-06-03T06:56:23.942Z", somePositiveInstant.toString)
assertEquals("-0687-08-07T23:38:33.088936253Z", someNegativeInstant.toString)
}
Expand Down Expand Up @@ -424,4 +428,31 @@ class InstantTest extends TemporalTest[Instant] {
expectThrows(classOf[DateTimeException], Instant.from(aYear))
}

@Test def parse(): Unit = {
assertEquals(Instant.EPOCH, Instant.parse("1970-01-01T00:00:00Z"))
assertEquals(Instant.MIN, Instant.parse("-1000000000-01-01T00:00:00Z"))
assertEquals(Instant.MIN.plus(366, DAYS), Instant.parse("-999999999-01-01T00:00:00Z"))

// https://github.com/scala-js/scala-js-java-time/issues/23
assertEquals(Instant.EPOCH.plus(10, MINUTES).plusMillis(100), Instant.parse("1970-01-01T00:10:00.100Z"))

assertEquals(Instant.MAX, Instant.parse("+1000000000-12-31T23:59:59.999999999Z"))
assertEquals(Instant.MAX.minus(366, DAYS), Instant.parse("+999999999-12-31T23:59:59.999999999Z"))

assertEquals(somePositiveInstant, Instant.parse("1999-06-03T06:56:23.942Z"))
assertEquals(someNegativeInstant, Instant.parse("-0687-08-07T23:38:33.088936253Z"))

val charSequence: CharSequence = "1999-06-03T06:56:23.942Z".toCharArray
assertEquals(somePositiveInstant, Instant.parse(charSequence))

expectThrows(classOf[DateTimeParseException], Instant.parse("+1000000001-12-31T23:59:59.999999999Z"))
expectThrows(classOf[DateTimeParseException], Instant.parse("-0687-99-07T23:38:33.088936253Z"))
expectThrows(classOf[DateTimeParseException], Instant.parse("-ABCD-08-07T23:38:33.088936253Z"))
expectThrows(classOf[DateTimeParseException], Instant.parse("1999-06-03T13:56:90.942Z"))
expectThrows(classOf[DateTimeParseException], Instant.parse("1999-06-03T13:65:23.942Z"))
expectThrows(classOf[DateTimeParseException], Instant.parse("1999-06-03T25:56:23.942Z"))
expectThrows(classOf[DateTimeParseException], Instant.parse("1999-06-99T13:56:23.942Z"))
expectThrows(classOf[DateTimeParseException], Instant.parse("1999-99-03T13:56:23.942Z"))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.scalajs.testsuite.javalib.time

import java.time._
import java.time.chrono.{IsoEra, IsoChronology}
import java.time.format.DateTimeParseException
import java.time.temporal._

import org.junit.Test
Expand Down Expand Up @@ -812,4 +813,22 @@ class LocalDateTest extends TemporalTest[LocalDate] {
for (t <- Seq(LocalTime.MIN, LocalTime.NOON, LocalTime.MAX))
expectThrows(classOf[DateTimeException], from(t))
}

@Test def test_parse(): Unit = {
assertEquals(parse("-999999999-01-01"), MIN)
assertEquals(parse("-0001-12-31"), of(-1, 12, 31))
assertEquals(parse("0000-01-01"), of(0, 1, 1))
assertEquals(parse("2011-02-28"), someDate)
assertEquals(parse("2012-02-29"), leapDate)
assertEquals(parse("9999-12-31"), of(9999, 12, 31))
assertEquals(parse("+10000-01-01"), of(10000, 1, 1))
assertEquals(parse("+999999999-12-31"), MAX)

expectThrows(classOf[DateTimeParseException], parse("0000-01-99"))
expectThrows(classOf[DateTimeParseException], parse("0000-01-900"))
expectThrows(classOf[DateTimeParseException], parse("aaaa-01-30"))
expectThrows(classOf[DateTimeParseException], parse("2012-13-30"))
expectThrows(classOf[DateTimeParseException], parse("2012-01-34"))
expectThrows(classOf[DateTimeParseException], parse("2005-02-29"))
}
}

0 comments on commit 24f76a4

Please sign in to comment.