Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Instant.parse and LocalDate.parse. #36

Merged
merged 1 commit into from
Mar 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"))
}
}