Skip to content

Commit 94b5579

Browse files
chrisshafersjrd
authored andcommitted
Implement Instant.parse and LocalDate.parse.
1 parent d58708a commit 94b5579

File tree

7 files changed

+226
-21
lines changed

7 files changed

+226
-21
lines changed

src/main/scala/java/time/Constants.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,8 @@ private[time] object Constants {
4646
final val SECONDS_IN_WEEK = SECONDS_IN_DAY * 7
4747

4848
final val SECONDS_IN_MONTH = 2629746
49+
50+
final val DAYS_IN_LEAP_YEAR = 366
51+
52+
final val DAYS_IN_YEAR = 365
4953
}

src/main/scala/java/time/Instant.scala

Lines changed: 113 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@ package java.time
22

33
import scala.scalajs.js
44

5+
import java.time.Preconditions.requireDateTimeParse
6+
import java.time.chrono.IsoChronology
7+
import java.time.format.DateTimeParseException
58
import java.time.temporal._
69

10+
import scala.util.control.NonFatal
11+
712
/** Created by alonsodomin on 26/12/2015. */
813
final class Instant private (private val seconds: Long, private val nanos: Int)
914
extends TemporalAccessor with Temporal with TemporalAdjuster
@@ -239,16 +244,14 @@ final class Instant private (private val seconds: Long, private val nanos: Int)
239244
override def hashCode(): Int = (seconds + 51 * nanos).hashCode
240245

241246
override def toString: String = {
242-
def tenThousandPartsBasedOnZero: (Long, Long) = {
247+
def tenThousandPartsAndRemainder: (Long, Long) = {
243248
if (seconds < -secondsFromZeroToEpoch) {
244-
val zeroSecs = seconds + secondsFromZeroToEpoch
245-
val quot = zeroSecs / secondsInTenThousandYears
246-
val rem = zeroSecs % secondsInTenThousandYears
249+
val quot = seconds / secondsInTenThousandYears
250+
val rem = seconds % secondsInTenThousandYears
247251
(quot, rem)
248252
} else {
249-
val zeroSecs = seconds - secondsInTenThousandYears + secondsFromZeroToEpoch
250-
val quot = Math.floorDiv(zeroSecs, secondsInTenThousandYears) + 1
251-
val rem = Math.floorMod(zeroSecs, secondsInTenThousandYears)
253+
val quot = Math.floorDiv(seconds, secondsInTenThousandYears)
254+
val rem = Math.floorMod(seconds, secondsInTenThousandYears)
252255
(quot, rem)
253256
}
254257
}
@@ -259,23 +262,29 @@ final class Instant private (private val seconds: Long, private val nanos: Int)
259262
(LocalDate.ofEpochDay(epochDay), LocalTime.ofSecondOfDay(secondsOfDay).withNano(nanos))
260263
}
261264

262-
val (hi, lo) = tenThousandPartsBasedOnZero
263-
val epochSecond = lo - secondsFromZeroToEpoch
265+
val (hi, lo) = tenThousandPartsAndRemainder
266+
val epochSecond = lo
264267
val (date, time) = dateTime(epochSecond)
265268

266-
val hiPart = {
267-
if (hi > 0) s"+$hi"
268-
else if (hi < 0) hi.toString
269-
else ""
269+
val years = hi * 10000 + date.getYear
270+
271+
val yearSegment = {
272+
if (years > 9999) s"+$years"
273+
else if (years < 0 && years > -1000) "-%04d".format(Math.abs(years))
274+
else years.toString
270275
}
271276

277+
val monthSegement = "%02d".format(date.getMonthValue)
278+
val daySegment = "%02d".format(date.getDayOfMonth)
279+
272280
val timePart = {
273281
val timeStr = time.toString
274282
if (time.getSecond == 0 && time.getNano == 0) timeStr + ":00"
275283
else timeStr
276284
}
277285

278-
s"${hiPart}${date}T${timePart}Z"
286+
val dateSegment = s"$yearSegment-$monthSegement-$daySegment"
287+
s"${dateSegment}T${timePart}Z"
279288
}
280289

281290
// Not implemented
@@ -287,12 +296,17 @@ object Instant {
287296
import Constants._
288297
import ChronoField._
289298

299+
private final val iso = IsoChronology.INSTANCE
300+
290301
final val EPOCH = new Instant(0, 0)
291302

292303
private val MinSecond = -31557014167219200L
293304
private val MaxSecond = 31556889864403199L
294305
private val MaxNanosInSecond = 999999999
295306

307+
private val MaxYear = 1000000000
308+
private val MinYear = -1000000000
309+
296310
/*
297311
* 146097 days in 400 years
298312
* 86400 seconds in a day
@@ -335,7 +349,90 @@ object Instant {
335349
ofEpochSecond(temporal.getLong(INSTANT_SECONDS), temporal.getLong(NANO_OF_SECOND))
336350
}
337351

338-
// Not implemented
339-
// def parse(text: CharSequence): Instant
352+
private def parseSegment(segment: String, classifier: String): Int = {
353+
try {
354+
segment.toInt
355+
} catch {
356+
case _: NumberFormatException =>
357+
throw new DateTimeParseException(s"$segment is not a valid $classifier",
358+
segment, 0)
359+
}
360+
}
361+
362+
private def toEpochDay(year: Int, month: Int, day: Int): Long = {
363+
val leapYear = iso.isLeapYear(year)
364+
365+
val extremeLeapYear = 999999996
366+
val epochDaysToAccountForExtreme = (3 * DAYS_IN_YEAR) + DAYS_IN_LEAP_YEAR
367+
368+
requireDateTimeParse(year <= MaxYear || year >= MinYear,
369+
s"$year out of bounds, year > 1000000000 || year < -1000000000",
370+
year.toString, 0)
371+
372+
val monthDay = MonthDay.of(month, day)
373+
if (monthDay.getMonth == Month.FEBRUARY && leapYear) {
374+
requireDateTimeParse(monthDay.getDayOfMonth <= 29,
375+
"Day range out of bounds <= 29 for leap years", day.toString, 0)
376+
}
377+
378+
if (year == MaxYear)
379+
LocalDate.of(extremeLeapYear, month, day).toEpochDay + epochDaysToAccountForExtreme
380+
else if (year == MinYear)
381+
LocalDate.of(-extremeLeapYear, month, day).toEpochDay - epochDaysToAccountForExtreme
382+
else
383+
LocalDate.of(year, month, day).toEpochDay
384+
}
340385

386+
def parse(text: CharSequence): Instant = {
387+
try {
388+
val pattern = """(^[-+]?)(\d*)-(\d*)-(\d*)T(\d*):(\d*):(\d*).?(\d*)Z""".r
389+
val pattern(sign, yearSegment, monthSegment, daySegment,
390+
hourSegment, minutesSegment, secondsSegment, nanosecondsSegment) = text
391+
392+
val year = parseSegment(sign + yearSegment, "year")
393+
val month = parseSegment(monthSegment, "month")
394+
val day = parseSegment(daySegment, "day")
395+
val nanoPower = 9
396+
397+
requireDateTimeParse(!((sign != "+") && (year > 9999)),
398+
s"year > 9999 must be preceded by [+]", text, 0)
399+
400+
val days = toEpochDay(year, month, day)
401+
val dayOffset = days
402+
403+
val hourOffset = parseSegment(hourSegment, "hour")
404+
val minuteOffset = parseSegment(minutesSegment, "minutes")
405+
val secondsOffset = parseSegment(secondsSegment, "seconds")
406+
407+
requireDateTimeParse(hourOffset <= HOURS_IN_DAY,
408+
s"hours are > $HOURS_IN_DAY", text, 0)
409+
410+
requireDateTimeParse(minuteOffset <= MINUTES_IN_HOUR,
411+
s"minutes are > $MINUTES_IN_HOUR", text, 0)
412+
413+
requireDateTimeParse(secondsOffset <= SECONDS_IN_MINUTE,
414+
s"seconds are > $SECONDS_IN_MINUTE", text, 0)
415+
416+
val nanos = if (nanosecondsSegment != "") {
417+
val scale = Math.pow(10, nanoPower - nanosecondsSegment.length).toInt
418+
parseSegment(nanosecondsSegment, "nanoseconds") * scale
419+
} else {
420+
0
421+
}
422+
423+
val epochSecondsOffset = {
424+
dayOffset * SECONDS_IN_DAY +
425+
hourOffset * SECONDS_IN_HOUR +
426+
minuteOffset * SECONDS_IN_MINUTE +
427+
secondsOffset
428+
}
429+
430+
new Instant(epochSecondsOffset, nanos)
431+
} catch {
432+
case err: DateTimeParseException =>
433+
throw err
434+
case NonFatal(err) =>
435+
throw new DateTimeParseException(s"Invalid date $text", text, 0)
436+
}
437+
}
341438
}

src/main/scala/java/time/LocalDate.scala

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package java.time
22

33
import scala.scalajs.js
44

5-
import java.time.chrono.{IsoEra, Era, IsoChronology, ChronoLocalDate}
5+
import java.time.chrono._
6+
import java.time.format.DateTimeParseException
67
import java.time.temporal._
78

9+
import scala.util.control.NonFatal
10+
811
final class LocalDate private (year: Int, month: Month, dayOfMonth: Int)
912
extends ChronoLocalDate with Serializable {
1013
import Preconditions.requireDateTime
@@ -99,7 +102,7 @@ final class LocalDate private (year: Int, month: Month, dayOfMonth: Int)
99102

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

102-
def lengthOfYear(): Int = if (_isLeapYear) 366 else 365
105+
override def lengthOfYear(): Int = if (_isLeapYear) 366 else 365
103106

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

374377
object LocalDate {
375-
import Preconditions.requireDateTime
378+
import Preconditions._
376379

377380
private final val iso = IsoChronology.INSTANCE
378381

@@ -431,8 +434,37 @@ object LocalDate {
431434
ofEpochDay(temporal.getLong(ChronoField.EPOCH_DAY))
432435
}
433436

434-
// TODO
435-
// def parse(text: CharSequence): LocalDate
437+
private def parseSegment(segment: String, classifier: String): Int = {
438+
try {
439+
segment.toInt
440+
} catch {
441+
case _: NumberFormatException =>
442+
throw new DateTimeParseException(s"$segment is not a valid $classifier",
443+
segment, 0)
444+
}
445+
}
446+
447+
def parse(text: CharSequence): LocalDate = {
448+
try {
449+
val pattern = """(^[-+]?)(\d*)-(\d*)-(\d*)""".r
450+
val pattern(sign, yearSegment, monthSegment, daySegment) = text
451+
452+
val year = parseSegment(sign + yearSegment, "year")
453+
val month = parseSegment(monthSegment, "month")
454+
val day = parseSegment(daySegment, "day")
455+
456+
requireDateTimeParse(!((sign != "+") && (year > 9999)),
457+
s"year > 9999 must be preceded by [+]", text, 0)
458+
459+
LocalDate.of(year.toInt, month.toInt, day.toInt)
460+
} catch {
461+
case err: DateTimeParseException =>
462+
throw err
463+
case NonFatal(err) =>
464+
throw new DateTimeParseException(s"Invalid date $text", text, 0)
465+
}
466+
}
467+
436468
// def parse(text: CharSequence,
437469
// formatter: java.time.format.DateTimeFormatter): LocalDate
438470
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
package java.time
22

3+
import java.time.format.DateTimeParseException
4+
35
private[time] object Preconditions {
46
// Like scala.Predef.require, but throws a DateTimeException.
57
def requireDateTime(requirement: Boolean, message: => Any): Unit = {
68
if (!requirement)
79
throw new DateTimeException(message.toString)
810
}
11+
12+
def requireDateTimeParse(requirement: Boolean, message: => Any,
13+
charSequence: CharSequence, index: Int): Unit = {
14+
if (!requirement)
15+
throw new DateTimeParseException(message.toString, charSequence, index)
16+
}
917
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package java.time.format
2+
3+
import java.time.DateTimeException
4+
5+
class DateTimeParseException(message: String, parsedData: CharSequence,
6+
errorIndex: Int, cause: Throwable)
7+
extends DateTimeException(message, cause) {
8+
9+
def this(message: String, parsedData: CharSequence, errorIndex: Int) =
10+
this(message, parsedData, errorIndex, null)
11+
12+
def getErrorIndex(): Int = errorIndex
13+
def getParsedString(): String = parsedData.toString
14+
}

testSuite/shared/src/test/scala/org/scalajs/testsuite/javalib/time/InstantTest.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.scalajs.testsuite.javalib.time
22

33
import java.time._
4+
import java.time.format.DateTimeParseException
45
import java.time.temporal.{UnsupportedTemporalTypeException, ChronoUnit, ChronoField}
56

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

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

380382
assertEquals("+1000000000-12-31T23:59:59.999999999Z", Instant.MAX.toString)
383+
assertEquals("+999999999-12-31T23:59:59.999999999Z", Instant.MAX.minus(366, DAYS).toString)
384+
381385
assertEquals("1999-06-03T06:56:23.942Z", somePositiveInstant.toString)
382386
assertEquals("-0687-08-07T23:38:33.088936253Z", someNegativeInstant.toString)
383387
}
@@ -424,4 +428,31 @@ class InstantTest extends TemporalTest[Instant] {
424428
expectThrows(classOf[DateTimeException], Instant.from(aYear))
425429
}
426430

431+
@Test def parse(): Unit = {
432+
assertEquals(Instant.EPOCH, Instant.parse("1970-01-01T00:00:00Z"))
433+
assertEquals(Instant.MIN, Instant.parse("-1000000000-01-01T00:00:00Z"))
434+
assertEquals(Instant.MIN.plus(366, DAYS), Instant.parse("-999999999-01-01T00:00:00Z"))
435+
436+
// https://github.com/scala-js/scala-js-java-time/issues/23
437+
assertEquals(Instant.EPOCH.plus(10, MINUTES).plusMillis(100), Instant.parse("1970-01-01T00:10:00.100Z"))
438+
439+
assertEquals(Instant.MAX, Instant.parse("+1000000000-12-31T23:59:59.999999999Z"))
440+
assertEquals(Instant.MAX.minus(366, DAYS), Instant.parse("+999999999-12-31T23:59:59.999999999Z"))
441+
442+
assertEquals(somePositiveInstant, Instant.parse("1999-06-03T06:56:23.942Z"))
443+
assertEquals(someNegativeInstant, Instant.parse("-0687-08-07T23:38:33.088936253Z"))
444+
445+
val charSequence: CharSequence = "1999-06-03T06:56:23.942Z".toCharArray
446+
assertEquals(somePositiveInstant, Instant.parse(charSequence))
447+
448+
expectThrows(classOf[DateTimeParseException], Instant.parse("+1000000001-12-31T23:59:59.999999999Z"))
449+
expectThrows(classOf[DateTimeParseException], Instant.parse("-0687-99-07T23:38:33.088936253Z"))
450+
expectThrows(classOf[DateTimeParseException], Instant.parse("-ABCD-08-07T23:38:33.088936253Z"))
451+
expectThrows(classOf[DateTimeParseException], Instant.parse("1999-06-03T13:56:90.942Z"))
452+
expectThrows(classOf[DateTimeParseException], Instant.parse("1999-06-03T13:65:23.942Z"))
453+
expectThrows(classOf[DateTimeParseException], Instant.parse("1999-06-03T25:56:23.942Z"))
454+
expectThrows(classOf[DateTimeParseException], Instant.parse("1999-06-99T13:56:23.942Z"))
455+
expectThrows(classOf[DateTimeParseException], Instant.parse("1999-99-03T13:56:23.942Z"))
456+
}
457+
427458
}

testSuite/shared/src/test/scala/org/scalajs/testsuite/javalib/time/LocalDateTest.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.scalajs.testsuite.javalib.time
22

33
import java.time._
44
import java.time.chrono.{IsoEra, IsoChronology}
5+
import java.time.format.DateTimeParseException
56
import java.time.temporal._
67

78
import org.junit.Test
@@ -812,4 +813,22 @@ class LocalDateTest extends TemporalTest[LocalDate] {
812813
for (t <- Seq(LocalTime.MIN, LocalTime.NOON, LocalTime.MAX))
813814
expectThrows(classOf[DateTimeException], from(t))
814815
}
816+
817+
@Test def test_parse(): Unit = {
818+
assertEquals(parse("-999999999-01-01"), MIN)
819+
assertEquals(parse("-0001-12-31"), of(-1, 12, 31))
820+
assertEquals(parse("0000-01-01"), of(0, 1, 1))
821+
assertEquals(parse("2011-02-28"), someDate)
822+
assertEquals(parse("2012-02-29"), leapDate)
823+
assertEquals(parse("9999-12-31"), of(9999, 12, 31))
824+
assertEquals(parse("+10000-01-01"), of(10000, 1, 1))
825+
assertEquals(parse("+999999999-12-31"), MAX)
826+
827+
expectThrows(classOf[DateTimeParseException], parse("0000-01-99"))
828+
expectThrows(classOf[DateTimeParseException], parse("0000-01-900"))
829+
expectThrows(classOf[DateTimeParseException], parse("aaaa-01-30"))
830+
expectThrows(classOf[DateTimeParseException], parse("2012-13-30"))
831+
expectThrows(classOf[DateTimeParseException], parse("2012-01-34"))
832+
expectThrows(classOf[DateTimeParseException], parse("2005-02-29"))
833+
}
815834
}

0 commit comments

Comments
 (0)