Skip to content

Commit 8f8e74f

Browse files
author
Miloslav Metelka
committed
Fixed as.Date() with invalid date string.
1 parent f239dc9 commit 8f8e74f

File tree

4 files changed

+286
-9
lines changed

4 files changed

+286
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Bug fixes:
1717
* `length<-` would remove attributes from the target even if it was a shared value
1818
* `length(x) <- N` should not strip attributes if `length(x) == N`, which is not in line with GNU-R documentation,
1919
but relied upon in the `methods` package #55
20+
* `as.Date` with invalid date string #56
2021

2122
# 1.0 RC 13
2223

com.oracle.truffle.r.nodes.builtin/src/com/oracle/truffle/r/nodes/builtin/base/DatePOSIXFunctions.java

Lines changed: 197 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,37 @@
3030
import static com.oracle.truffle.r.runtime.builtins.RBuiltinKind.INTERNAL;
3131

3232
import java.text.ParsePosition;
33+
import java.time.Clock;
3334
import java.time.DateTimeException;
3435
import java.time.Instant;
3536
import java.time.LocalDate;
3637
import java.time.LocalDateTime;
38+
import java.time.LocalTime;
3739
import java.time.ZoneId;
3840
import java.time.ZoneOffset;
3941
import java.time.ZonedDateTime;
42+
import java.time.chrono.Chronology;
43+
import java.time.chrono.ChronoLocalDate;
44+
import java.time.chrono.ChronoLocalDateTime;
45+
import java.time.chrono.ChronoPeriod;
46+
import java.time.chrono.ChronoZonedDateTime;
47+
import java.time.chrono.Era;
48+
import java.time.chrono.IsoChronology;
4049
import java.time.format.DateTimeFormatter;
4150
import java.time.format.DateTimeFormatterBuilder;
4251
import java.time.format.DateTimeParseException;
4352
import java.time.format.FormatStyle;
53+
import java.time.format.ResolverStyle;
54+
import java.time.format.SignStyle;
4455
import java.time.format.TextStyle;
4556
import java.time.temporal.ChronoField;
4657
import java.time.temporal.TemporalAccessor;
58+
import java.time.temporal.TemporalField;
59+
import java.time.temporal.ValueRange;
4760
import java.util.HashMap;
61+
import java.util.List;
4862
import java.util.Locale;
63+
import java.util.Map;
4964
import java.util.TimeZone;
5065

5166
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
@@ -70,7 +85,7 @@
7085
import com.oracle.truffle.r.runtime.data.model.RAbstractListVector;
7186
import com.oracle.truffle.r.runtime.data.model.RAbstractStringVector;
7287
import com.oracle.truffle.r.runtime.data.model.RAbstractVector;
73-
import java.time.LocalTime;
88+
import java.time.Month;
7489

7590
// from GnuR datatime.c
7691

@@ -435,7 +450,7 @@ protected RList strptime(RAbstractStringVector x, RAbstractStringVector format,
435450
DateTimeFormatterBuilder[] builders = createFormatters(format, true);
436451
DateTimeFormatter[] formatters = new DateTimeFormatter[builders.length];
437452
for (int i = 0; i < builders.length; i++) {
438-
formatters[i] = builders[i].toFormatter();
453+
formatters[i] = builders[i].toFormatter().withChronology(LeapYearChronology.INSTANCE);
439454
}
440455

441456
for (int i = 0; i < length; i++) {
@@ -485,7 +500,7 @@ private static DateTimeFormatterBuilder[] createFormatters(RAbstractStringVector
485500
private static DateTimeFormatterBuilder createFormatter(String format, boolean forInput) {
486501
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
487502
if (forInput) {
488-
// Opposite of STRICT mode; allows to parse single-digit hour and minute like '0:3:22'.
503+
// Lenient parsing required to parse datetimes like "2002-6-24-0-0-10"
489504
builder.parseLenient();
490505
}
491506
boolean escaped = false;
@@ -564,7 +579,11 @@ private static DateTimeFormatterBuilder createFormatter(String format, boolean f
564579
* 24:00:00 are accepted for input, since ISO 8601 allows these. For output
565580
* 00-23 is required thus using HOUR_OF_DAY.
566581
*/
567-
builder.appendValue(forInput ? ChronoField.CLOCK_HOUR_OF_DAY : ChronoField.HOUR_OF_DAY, 2);
582+
if (forInput) {
583+
builder.appendValue(ChronoField.CLOCK_HOUR_OF_DAY, 1, 2, SignStyle.NOT_NEGATIVE);
584+
} else {
585+
builder.appendValue(ChronoField.HOUR_OF_DAY, 2);
586+
}
568587
break;
569588
case 'I':
570589
// Hours as decimal number (01–12).
@@ -580,7 +599,11 @@ private static DateTimeFormatterBuilder createFormatter(String format, boolean f
580599
break;
581600
case 'M':
582601
// Minute as decimal number (00–59).
583-
builder.appendValue(ChronoField.MINUTE_OF_HOUR, 2);
602+
if (forInput) {
603+
builder.appendValue(ChronoField.MINUTE_OF_HOUR, 1, 2, SignStyle.NOT_NEGATIVE);
604+
} else {
605+
builder.appendValue(ChronoField.MINUTE_OF_HOUR, 2);
606+
}
584607
break;
585608
case 'n':
586609
// Newline on output, arbitrary whitespace on input.
@@ -625,16 +648,21 @@ private static DateTimeFormatterBuilder createFormatter(String format, boolean f
625648
* Second as decimal number (00–61), allowing for up to two leap-seconds
626649
* (but POSIX-compliant implementations will ignore leap seconds).
627650
*/
628-
builder.appendValue(ChronoField.SECOND_OF_MINUTE, 2);
651+
if (forInput) {
652+
builder.appendValue(ChronoField.SECOND_OF_MINUTE, 1, 2, SignStyle.NOT_NEGATIVE);
653+
} else {
654+
builder.appendValue(ChronoField.SECOND_OF_MINUTE, 2);
655+
}
629656
break;
630657
case 't':
631658
// Tab on output, arbitrary whitespace on input.
632659
builder.appendLiteral('\t');
633660
break;
634661
case 'T':
635662
// Equivalent to %H:%M:%S.
636-
builder.appendValue(ChronoField.CLOCK_HOUR_OF_DAY, 2).appendLiteral(':').appendValue(ChronoField.MINUTE_OF_HOUR, 2).appendLiteral(':').appendValue(
637-
ChronoField.SECOND_OF_MINUTE, 2);
663+
builder.appendValue(ChronoField.CLOCK_HOUR_OF_DAY, forInput ? 1 : 2).appendLiteral(':').appendValue(ChronoField.MINUTE_OF_HOUR, forInput ? 1 : 2).appendLiteral(
664+
':').appendValue(
665+
ChronoField.SECOND_OF_MINUTE, forInput ? 1 : 2);
638666
break;
639667
case 'u':
640668
// Weekday as a decimal number (1–7, Monday is 1).
@@ -786,4 +814,165 @@ private static String getTimeZomeFromAttribute(RAbstractListVector x) {
786814
}
787815
return zone;
788816
}
817+
818+
private static final class LeapYearChronology implements Chronology {
819+
820+
static LeapYearChronology INSTANCE = new LeapYearChronology(IsoChronology.INSTANCE);
821+
822+
private static final int[] maxDayOfMonths = new int[]{31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
823+
824+
private final Chronology delegate;
825+
826+
LeapYearChronology(Chronology delegate) {
827+
this.delegate = delegate;
828+
}
829+
830+
@Override
831+
public String getId() {
832+
return delegate.getId();
833+
}
834+
835+
@Override
836+
public String getCalendarType() {
837+
return delegate.getCalendarType();
838+
}
839+
840+
@Override
841+
public ChronoLocalDate date(Era era, int yearOfEra, int month, int dayOfMonth) {
842+
return delegate.date(era, yearOfEra, month, dayOfMonth);
843+
}
844+
845+
@Override
846+
public ChronoLocalDate date(int prolepticYear, int month, int dayOfMonth) {
847+
return delegate.date(prolepticYear, month, dayOfMonth);
848+
}
849+
850+
@Override
851+
public ChronoLocalDate dateYearDay(Era era, int yearOfEra, int dayOfYear) {
852+
return delegate.dateYearDay(era, yearOfEra, dayOfYear);
853+
}
854+
855+
@Override
856+
public ChronoLocalDate dateYearDay(int prolepticYear, int dayOfYear) {
857+
return delegate.dateYearDay(prolepticYear, dayOfYear);
858+
}
859+
860+
@Override
861+
public ChronoLocalDate dateEpochDay(long epochDay) {
862+
return delegate.dateEpochDay(epochDay);
863+
}
864+
865+
@Override
866+
public ChronoLocalDate dateNow() {
867+
return delegate.dateNow();
868+
}
869+
870+
@Override
871+
public ChronoLocalDate dateNow(ZoneId zone) {
872+
return delegate.dateNow(zone);
873+
}
874+
875+
@Override
876+
public ChronoLocalDate dateNow(Clock clock) {
877+
return delegate.dateNow(clock);
878+
}
879+
880+
@Override
881+
public ChronoLocalDate date(TemporalAccessor temporal) {
882+
return delegate.date(temporal);
883+
}
884+
885+
@Override
886+
public ChronoLocalDateTime<? extends ChronoLocalDate> localDateTime(TemporalAccessor temporal) {
887+
return delegate.localDateTime(temporal);
888+
}
889+
890+
@Override
891+
public ChronoZonedDateTime<? extends ChronoLocalDate> zonedDateTime(TemporalAccessor temporal) {
892+
return delegate.zonedDateTime(temporal);
893+
}
894+
895+
@Override
896+
public ChronoZonedDateTime<? extends ChronoLocalDate> zonedDateTime(Instant instant, ZoneId zone) {
897+
return delegate.zonedDateTime(instant, zone);
898+
}
899+
900+
@Override
901+
public boolean isLeapYear(long prolepticYear) {
902+
return delegate.isLeapYear(prolepticYear);
903+
}
904+
905+
@Override
906+
public int prolepticYear(Era era, int yearOfEra) {
907+
return delegate.prolepticYear(era, yearOfEra);
908+
}
909+
910+
@Override
911+
public Era eraOf(int eraValue) {
912+
return delegate.eraOf(eraValue);
913+
}
914+
915+
@Override
916+
public List<Era> eras() {
917+
return delegate.eras();
918+
}
919+
920+
@Override
921+
public ValueRange range(ChronoField field) {
922+
return delegate.range(field);
923+
}
924+
925+
@Override
926+
public String getDisplayName(TextStyle style, Locale locale) {
927+
return delegate.getDisplayName(style, locale);
928+
}
929+
930+
@Override
931+
public ChronoLocalDate resolveDate(Map<TemporalField, Long> fieldValues, ResolverStyle resolverStyle) {
932+
// date(int prolepticYear, int month, int dayOfMonth) not called -> handle here
933+
Long day = fieldValues.get(ChronoField.DAY_OF_MONTH);
934+
if (day != null && day >= 29) {
935+
Long month = fieldValues.get(ChronoField.MONTH_OF_YEAR);
936+
if (month != null && month <= 12) {
937+
if (month == 2 && day == 29) {
938+
Long year = fieldValues.get(ChronoField.YEAR);
939+
if (year != null && !isLeapYear(year)) {
940+
throw new DateTimeException("Invalid date 'February 29' as '" + year + "' is not a leap year");
941+
}
942+
} else {
943+
int monthInt = (int) (long) month;
944+
if (day > maxDayOfMonths[(monthInt) - 1]) {
945+
throw new DateTimeException("Invalid date '" + Month.of(monthInt).name() + " " + day + "'");
946+
}
947+
}
948+
}
949+
}
950+
return delegate.resolveDate(fieldValues, resolverStyle);
951+
}
952+
953+
@Override
954+
public ChronoPeriod period(int years, int months, int days) {
955+
return delegate.period(years, months, days);
956+
}
957+
958+
@Override
959+
public int compareTo(Chronology other) {
960+
return delegate.compareTo(other);
961+
}
962+
963+
@Override
964+
public boolean equals(Object obj) {
965+
return delegate.equals(obj);
966+
}
967+
968+
@Override
969+
public int hashCode() {
970+
return delegate.hashCode();
971+
}
972+
973+
@Override
974+
public String toString() {
975+
return delegate.toString();
976+
}
977+
}
789978
}

com.oracle.truffle.r.test/src/com/oracle/truffle/r/test/ExpectedTestOutput.test

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5778,6 +5778,75 @@ actual 34 35 36 36 38 39
57785778
#argv <- structure(list(x = c('2007-11-06', NA)), .Names = 'x');do.call('as.Date.character', argv)
57795779
[1] "2007-11-06" NA
57805780

5781+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5782+
#{ as.Date('2016-02-29') }
5783+
[1] "2016-02-29"
5784+
5785+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5786+
#{ as.Date('2017-00-10') }
5787+
Error in charToDate(x) :
5788+
character string is not in a standard unambiguous format
5789+
5790+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5791+
#{ as.Date('2017-01-31') }
5792+
[1] "2017-01-31"
5793+
5794+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5795+
#{ as.Date('2017-01-32') }
5796+
Error in charToDate(x) :
5797+
character string is not in a standard unambiguous format
5798+
5799+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5800+
#{ as.Date('2017-02-28') }
5801+
[1] "2017-02-28"
5802+
5803+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5804+
#{ as.Date('2017-02-29') }
5805+
Error in charToDate(x) :
5806+
character string is not in a standard unambiguous format
5807+
5808+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5809+
#{ as.Date('2017-02-30') }
5810+
Error in charToDate(x) :
5811+
character string is not in a standard unambiguous format
5812+
5813+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5814+
#{ as.Date('2017-03-31') }
5815+
[1] "2017-03-31"
5816+
5817+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5818+
#{ as.Date('2017-04-31') }
5819+
Error in charToDate(x) :
5820+
character string is not in a standard unambiguous format
5821+
5822+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5823+
#{ as.Date('2017-04-32') }
5824+
Error in charToDate(x) :
5825+
character string is not in a standard unambiguous format
5826+
5827+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5828+
#{ as.Date('2017-05-31') }
5829+
[1] "2017-05-31"
5830+
5831+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5832+
#{ as.Date('2017-10-00') }
5833+
Error in charToDate(x) :
5834+
character string is not in a standard unambiguous format
5835+
5836+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5837+
#{ as.Date('2017-12-31') }
5838+
[1] "2017-12-31"
5839+
5840+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5841+
#{ as.Date('2017-12-32') }
5842+
Error in charToDate(x) :
5843+
character string is not in a standard unambiguous format
5844+
5845+
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatecharacter.testasDatecharacter1#
5846+
#{ as.Date('2017-13-01') }
5847+
Error in charToDate(x) :
5848+
character string is not in a standard unambiguous format
5849+
57815850
##com.oracle.truffle.r.test.builtins.TestBuiltin_asDatedefault.testasDatedefault1#
57825851
#argv <- structure(list(x = logical(0)), .Names = 'x');do.call('as.Date.default', argv)
57835852
Date of length 0

com.oracle.truffle.r.test/src/com/oracle/truffle/r/test/builtins/TestBuiltin_asDatecharacter.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1515
*
1616
* Copyright (c) 2014, Purdue University
17-
* Copyright (c) 2014, 2018, Oracle and/or its affiliates
17+
* Copyright (c) 2014, 2019, Oracle and/or its affiliates
1818
*
1919
* All rights reserved.
2020
*/
@@ -31,5 +31,23 @@ public class TestBuiltin_asDatecharacter extends TestBase {
3131
@Test
3232
public void testasDatecharacter1() {
3333
assertEval("argv <- structure(list(x = c('2007-11-06', NA)), .Names = 'x');do.call('as.Date.character', argv)");
34+
35+
assertEval("{ as.Date('2017-02-29') }");
36+
assertEval("{ as.Date('2017-01-31') }");
37+
assertEval("{ as.Date('2017-01-32') }");
38+
assertEval("{ as.Date('2017-02-28') }");
39+
assertEval("{ as.Date('2017-02-29') }");
40+
assertEval("{ as.Date('2016-02-29') }");
41+
assertEval("{ as.Date('2017-02-30') }");
42+
assertEval("{ as.Date('2017-03-31') }");
43+
assertEval("{ as.Date('2017-04-31') }");
44+
assertEval("{ as.Date('2017-05-31') }");
45+
assertEval("{ as.Date('2017-04-32') }");
46+
assertEval("{ as.Date('2017-00-10') }");
47+
assertEval("{ as.Date('2017-10-00') }");
48+
assertEval("{ as.Date('2017-12-31') }");
49+
assertEval("{ as.Date('2017-12-32') }");
50+
assertEval("{ as.Date('2017-13-01') }");
51+
3452
}
3553
}

0 commit comments

Comments
 (0)