Loading histream-core/src/main/java/de/sekmi/histream/DateTimeAccuracy.java +96 −69 Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ package de.sekmi.histream; import java.text.ParseException; import java.text.ParsePosition; import java.time.DateTimeException; import java.time.Instant; /* * #%L Loading @@ -26,6 +27,7 @@ import java.time.DateTimeException; import java.time.LocalDateTime; import java.time.OffsetTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; Loading @@ -37,6 +39,7 @@ import java.time.temporal.Temporal; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalField; import java.time.temporal.TemporalUnit; import java.time.temporal.UnsupportedTemporalTypeException; import java.util.Date; import java.util.Objects; Loading @@ -47,11 +50,14 @@ import de.sekmi.histream.xml.DateTimeAccuracyAdapter; /** * Local date and time with specified accuracy. Maximum resolution is seconds. * For supported accuracy, see {@link #setAccuracy(ChronoUnit)}. * @author Raphael * @author R.W.Majeed * */ @XmlJavaTypeAdapter(DateTimeAccuracyAdapter.class) public class DateTimeAccuracy implements Temporal, Comparable<DateTimeAccuracy> { static final String PARTIAL_FORMATTER_PATTERN = "u[-M[-d['T'H[:m[:s[.S]]][X]]]]"; static final DateTimeFormatter PARTIAL_FORMATTER = DateTimeFormatter.ofPattern(PARTIAL_FORMATTER_PATTERN); // TODO why not use instant, since we always calculate UTC? or Offset/ZonedDateTime? private LocalDateTime dateTime; private ChronoUnit accuracy; Loading Loading @@ -97,7 +103,18 @@ public class DateTimeAccuracy implements Temporal, Comparable<DateTimeAccuracy> dateTime.truncatedTo(accuracy); } // Temporal interface behaves like undelaying dateTime /** * Convert the partial date time to an instant. * Will return the minimum instant for the given accuracy. * E.g. accuracy of YEAR will return the the first second in the given year. * @return minimum instant within the given accuracy */ public Instant toInstantMin(){ return dateTime.toInstant(ZoneOffset.UTC); } // TODO toInstantMax() (increase field at accuracy and subtract one millisecond) // Temporal interface behaves like underlaying dateTime @Override public long getLong(TemporalField arg0) {return dateTime.getLong(arg0);} @Override Loading Loading @@ -161,14 +178,16 @@ public class DateTimeAccuracy implements Temporal, Comparable<DateTimeAccuracy> * @param digits digits to add */ private static void appendWithZeroPrefix(StringBuilder builder, TemporalAccessor date, TemporalField field, int digits){ int v = date.get(field); padZeros(builder,date.get(field), digits); } private static void padZeros(StringBuilder builder, int value, int digits){ int pow = 1; for( int i=1; i<digits; i++ )pow *= 10; while( v < pow && pow > 1 ){ while( value < pow && pow > 1 ){ builder.append('0'); pow /= 10; } builder.append(v); builder.append(value); } /** * Convert the date to a partial ISO 8601 date time string. Loading @@ -195,8 +214,9 @@ public class DateTimeAccuracy implements Temporal, Comparable<DateTimeAccuracy> TemporalAccessor dt; if( tz != null ){ // use timezone information dt = dateTime.atZone(tz); // use timezone information. // Assume that dateTime is given in UTC. For output convert to destination timezone. dt = dateTime.atOffset(ZoneOffset.UTC).atZoneSameInstant(tz); }else{ // no zone info, output will not have offset dt = dateTime; Loading @@ -214,84 +234,91 @@ public class DateTimeAccuracy implements Temporal, Comparable<DateTimeAccuracy> if( tz != null && i >= 3 ){ // hours present // add zone offset String of = ((ZonedDateTime)dt).getOffset().normalized().toString(); b.append(of); int os = ((ZonedDateTime)dt).getOffset().getTotalSeconds(); if( os == 0 ){ // output Z b.append('Z'); }else{ // append sign and four characters if( os < 0 ){ b.append('-'); }else{ b.append('+'); } // hours int ox = os / 3600; os = os % 3600; padZeros(b,ox,2); // minutes ox = os / 60; padZeros(b,ox,2); // ignore seconds, not part of ISO } } return b.toString(); } /** * Parses a partial ISO 8601 date time string. * [-]CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm] * [-]CCYY-MM-DDThh:mm:ss[Z|(+|-)hhmm] * <p> * At least the year must be specified. All other fields can be left out. * * @param str ISO 8601 string * @return date time with accuracy as derived from parse * @throws ParseException for unparsable string * @throws IllegalArgumentException unparsable string (old unchecked exception) */ public static DateTimeAccuracy parsePartialIso8601(String str)throws ParseException{ if( str.length() < 4 )throw new ParseException("Need at least 4 characters for year: "+str, str.length()); // parse year int year = Integer.parseInt(str.substring(0, 4)); if( str.length() == 4 ){ // specified to accuracy of years return new DateTimeAccuracy(year); }else if( str.length() < 7 || str.charAt(4) != '-' ){ throw new ParseException("Expected YYYY-MM", Integer.min(4, str.length())); } // parse month int month = Integer.parseInt(str.substring(5, 7)); if( str.length() == 7 ){ // specified to accuracy of months return new DateTimeAccuracy(year, month); }else if( str.length() < 10 || str.charAt(7) != '-' ){ throw new ParseException("Expected YYYY-MM-DD", Integer.min(7, str.length())); } // parse day int day = Integer.parseInt(str.substring(8, 10)); if( str.length() == 10 ){ // specified to accuracy of days return new DateTimeAccuracy(year, month, day); }else if( str.length() < 13 || str.charAt(10) != 'T' ){ throw new ParseException("Expected yyyy-mm-ddThh", Integer.min(10, str.length())); } // parse hours int hours = Integer.parseInt(str.substring(11, 13)); if( str.length() == 13 ){ // specified to accuracy of hours return new DateTimeAccuracy(year, month, day, hours); }else if( str.length() < 16 || str.charAt(13) != ':' ){ throw new ParseException("Expected yyyy-mm-ddThh:mm", Integer.min(13, str.length())); } // parse minutes int mins = Integer.parseInt(str.substring(14, 16)); if( str.length() == 16 ){ // specified to accuracy of minutes return new DateTimeAccuracy(year, month, day, hours, mins); }else if( str.length() < 19 || str.charAt(16) != ':' ){ throw new ParseException("Expected yyyy-mm-ddThh:mm:ss", Integer.min(16, str.length())); } // parse seconds int secs = Integer.parseInt(str.substring(17, 19)); if( str.length() == 19 || (str.length() == 20 && str.charAt(19) == 'Z') ){ // specified to accuracy of seconds return new DateTimeAccuracy(year, month, day, hours, mins, secs); }else if( str.length() < 25 || !(str.charAt(19) != '+' || str.charAt(19) != '-') ){ throw new ParseException("Expected yyyy-mm-ddThh:mm:ss[Z|+oo:oo]", 19); }else if( str.length() != 25 || str.charAt(22) != ':' ){ // handles longer input and missing : in offset throw new ParseException("Expected yyyy-mm-ddThh:mm:ss[Z|+oo:oo]", 22); ParsePosition pos = new ParsePosition(0); TemporalAccessor a = PARTIAL_FORMATTER.parseUnresolved(str, pos); // first check that everything was parsed if( pos.getErrorIndex() != -1 ){ throw new ParseException("Parse error at position "+pos.getErrorIndex(), pos.getErrorIndex()); }else if( pos.getIndex() != str.length() ){ throw new ParseException("Unparsed text found at index "+pos.getIndex()+": "+str.substring(pos.getIndex()), pos.getIndex()); } // everything parsed without error // now check for accuracy ChronoUnit accuracy; LocalDateTime dateTime; if( a.isSupported(ChronoField.NANO_OF_SECOND) ){ // maximum accuracy of nanoseconds // not supported yet, truncate to seconds accuracy = ChronoUnit.NANOS; dateTime = LocalDateTime.from(a); }else if( a.isSupported(ChronoField.SECOND_OF_MINUTE) ){ accuracy = ChronoUnit.SECONDS; dateTime = LocalDateTime.of(a.get(ChronoField.YEAR), a.get(ChronoField.MONTH_OF_YEAR), a.get(ChronoField.DAY_OF_MONTH), a.get(ChronoField.HOUR_OF_DAY), a.get(ChronoField.MINUTE_OF_HOUR), a.get(ChronoField.SECOND_OF_MINUTE)); }else if( a.isSupported(ChronoField.MINUTE_OF_HOUR) ){ accuracy = ChronoUnit.MINUTES; dateTime = LocalDateTime.of(a.get(ChronoField.YEAR), a.get(ChronoField.MONTH_OF_YEAR), a.get(ChronoField.DAY_OF_MONTH), a.get(ChronoField.HOUR_OF_DAY), a.get(ChronoField.MINUTE_OF_HOUR)); }else if( a.isSupported(ChronoField.HOUR_OF_DAY) ){ accuracy = ChronoUnit.HOURS; dateTime = LocalDateTime.of(a.get(ChronoField.YEAR), a.get(ChronoField.MONTH_OF_YEAR), a.get(ChronoField.DAY_OF_MONTH), a.get(ChronoField.HOUR_OF_DAY), 0); }else if( a.isSupported(ChronoField.DAY_OF_MONTH) ){ accuracy = ChronoUnit.DAYS; dateTime = LocalDateTime.of(a.get(ChronoField.YEAR), a.get(ChronoField.MONTH_OF_YEAR), a.get(ChronoField.DAY_OF_MONTH), 0, 0); }else if( a.isSupported(ChronoField.MONTH_OF_YEAR) ){ dateTime = LocalDateTime.of(a.get(ChronoField.YEAR), a.get(ChronoField.MONTH_OF_YEAR), 1, 0, 0); accuracy = ChronoUnit.MONTHS; }else{ DateTimeAccuracy me = new DateTimeAccuracy(year, month, day, hours, mins, secs); // parse time zone ZoneOffset of = ZoneOffset.ofHoursMinutes( Integer.parseInt(str.substring(20, 22)), Integer.parseInt(str.substring(24, 25)) ); // format requires at least year accuracy = ChronoUnit.YEARS; dateTime = LocalDateTime.of(a.get(ChronoField.YEAR), 1, 1, 0, 0); } // check for zone offset ZoneOffset off = null; if( a.isSupported(ChronoField.OFFSET_SECONDS) ){ off = ZoneOffset.ofTotalSeconds(a.get(ChronoField.OFFSET_SECONDS)); // adjust to UTC // TODO unit test for this behavior me.dateTime = me.dateTime.atOffset(of).withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(); return me; dateTime = dateTime.atOffset(off).withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(); } // unparsed data (longer input) will be handled above //throw new ParseException("Unparsed data at index 26", 26); DateTimeAccuracy me = new DateTimeAccuracy(dateTime); me.accuracy = accuracy; return me; } /** Loading histream-core/src/test/java/de/sekmi/histream/TestDateTimeAccuracy.java +58 −3 Original line number Diff line number Diff line Loading @@ -7,8 +7,11 @@ import java.time.format.DateTimeParseException; import java.time.format.ResolverStyle; import java.time.temporal.ChronoField; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; import java.time.temporal.UnsupportedTemporalTypeException; import org.junit.Assert; import static org.junit.Assert.*; import org.junit.Test; public class TestDateTimeAccuracy { Loading @@ -34,10 +37,62 @@ public class TestDateTimeAccuracy { a = DateTimeAccuracy.parse(formatter, "01.02.2003 13"); Assert.assertEquals(ChronoUnit.HOURS, a.getAccuracy()); Assert.assertEquals("2003-02-01T13", a.toPartialIso8601(null)); Assert.assertEquals("2003-02-01T13+08:00", a.toPartialIso8601(tz)); Assert.assertEquals("2003-02-01T21+0800", a.toPartialIso8601(tz)); } @Test public void verifyDateTimeFormatter(){ TemporalAccessor a; DateTimeFormatter f = DateTimeFormatter.ISO_DATE_TIME; // zone offset missing, field should be not available a = f.parse("2001-02-03T04:05:06"); Assert.assertFalse(a.isSupported(ChronoField.OFFSET_SECONDS)); try{ a.get(ChronoField.OFFSET_SECONDS); Assert.fail("Expected exception not thrown"); }catch( UnsupportedTemporalTypeException e ){ // expected outcome } // zero zone offset, field should be available a = f.parse("2001-02-03T04:05:06Z"); Assert.assertEquals(0, a.get(ChronoField.OFFSET_SECONDS)); a = f.parse("2001-02-03T04:05:06+00:00"); Assert.assertEquals(0, a.get(ChronoField.OFFSET_SECONDS)); a = f.parse("2001-02-03T04:05"); // seconds can be omitted // test the partial timestamp formatter f = DateTimeFormatter.ofPattern("u[-M[-d['T'H[:m[:s[.S]]][X]]]]"); } @Test public void verifyParsingIncompleteIsoTimestamp() throws ParseException{ DateTimeAccuracy a; a = DateTimeAccuracy.parsePartialIso8601("2001"); assertEquals(ChronoUnit.YEARS, a.getAccuracy()); a = DateTimeAccuracy.parsePartialIso8601("2001-02"); assertEquals(ChronoUnit.MONTHS, a.getAccuracy()); a = DateTimeAccuracy.parsePartialIso8601("2001-02-03"); assertEquals(ChronoUnit.DAYS, a.getAccuracy()); a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04"); assertEquals(ChronoUnit.HOURS, a.getAccuracy()); a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:05"); assertEquals(ChronoUnit.MINUTES, a.getAccuracy()); a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:05:06"); assertEquals(ChronoUnit.SECONDS, a.getAccuracy()); // verify zone offset // for second accuracy a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:05:06+0800"); assertEquals(ChronoUnit.SECONDS, a.getAccuracy()); // zone offset calculation assertEquals(DateTimeAccuracy.parsePartialIso8601("2001-02-02T20:05:06Z"), a); a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04+0800"); assertEquals(ChronoUnit.HOURS, a.getAccuracy()); a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04Z"); assertEquals(ChronoUnit.HOURS, a.getAccuracy()); a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:05+0800"); assertEquals(ChronoUnit.MINUTES, a.getAccuracy()); } @Test public void testFormatExceedsText(){ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d.M.u[ H[:m[:s]]]"); Loading @@ -63,7 +118,7 @@ public class TestDateTimeAccuracy { } // TODO test more aspects of zone offset parsing DateTimeAccuracy.parsePartialIso8601("2003-02-01T04:05:06Z"); DateTimeAccuracy a = DateTimeAccuracy.parsePartialIso8601("2003-02-01T04:05:06+01:00"); DateTimeAccuracy a = DateTimeAccuracy.parsePartialIso8601("2003-02-01T04:05:06+0100"); // make sure the date is adjusted to UTC Assert.assertEquals(3, a.get(ChronoField.HOUR_OF_DAY)); } Loading Loading
histream-core/src/main/java/de/sekmi/histream/DateTimeAccuracy.java +96 −69 Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ package de.sekmi.histream; import java.text.ParseException; import java.text.ParsePosition; import java.time.DateTimeException; import java.time.Instant; /* * #%L Loading @@ -26,6 +27,7 @@ import java.time.DateTimeException; import java.time.LocalDateTime; import java.time.OffsetTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; Loading @@ -37,6 +39,7 @@ import java.time.temporal.Temporal; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalField; import java.time.temporal.TemporalUnit; import java.time.temporal.UnsupportedTemporalTypeException; import java.util.Date; import java.util.Objects; Loading @@ -47,11 +50,14 @@ import de.sekmi.histream.xml.DateTimeAccuracyAdapter; /** * Local date and time with specified accuracy. Maximum resolution is seconds. * For supported accuracy, see {@link #setAccuracy(ChronoUnit)}. * @author Raphael * @author R.W.Majeed * */ @XmlJavaTypeAdapter(DateTimeAccuracyAdapter.class) public class DateTimeAccuracy implements Temporal, Comparable<DateTimeAccuracy> { static final String PARTIAL_FORMATTER_PATTERN = "u[-M[-d['T'H[:m[:s[.S]]][X]]]]"; static final DateTimeFormatter PARTIAL_FORMATTER = DateTimeFormatter.ofPattern(PARTIAL_FORMATTER_PATTERN); // TODO why not use instant, since we always calculate UTC? or Offset/ZonedDateTime? private LocalDateTime dateTime; private ChronoUnit accuracy; Loading Loading @@ -97,7 +103,18 @@ public class DateTimeAccuracy implements Temporal, Comparable<DateTimeAccuracy> dateTime.truncatedTo(accuracy); } // Temporal interface behaves like undelaying dateTime /** * Convert the partial date time to an instant. * Will return the minimum instant for the given accuracy. * E.g. accuracy of YEAR will return the the first second in the given year. * @return minimum instant within the given accuracy */ public Instant toInstantMin(){ return dateTime.toInstant(ZoneOffset.UTC); } // TODO toInstantMax() (increase field at accuracy and subtract one millisecond) // Temporal interface behaves like underlaying dateTime @Override public long getLong(TemporalField arg0) {return dateTime.getLong(arg0);} @Override Loading Loading @@ -161,14 +178,16 @@ public class DateTimeAccuracy implements Temporal, Comparable<DateTimeAccuracy> * @param digits digits to add */ private static void appendWithZeroPrefix(StringBuilder builder, TemporalAccessor date, TemporalField field, int digits){ int v = date.get(field); padZeros(builder,date.get(field), digits); } private static void padZeros(StringBuilder builder, int value, int digits){ int pow = 1; for( int i=1; i<digits; i++ )pow *= 10; while( v < pow && pow > 1 ){ while( value < pow && pow > 1 ){ builder.append('0'); pow /= 10; } builder.append(v); builder.append(value); } /** * Convert the date to a partial ISO 8601 date time string. Loading @@ -195,8 +214,9 @@ public class DateTimeAccuracy implements Temporal, Comparable<DateTimeAccuracy> TemporalAccessor dt; if( tz != null ){ // use timezone information dt = dateTime.atZone(tz); // use timezone information. // Assume that dateTime is given in UTC. For output convert to destination timezone. dt = dateTime.atOffset(ZoneOffset.UTC).atZoneSameInstant(tz); }else{ // no zone info, output will not have offset dt = dateTime; Loading @@ -214,84 +234,91 @@ public class DateTimeAccuracy implements Temporal, Comparable<DateTimeAccuracy> if( tz != null && i >= 3 ){ // hours present // add zone offset String of = ((ZonedDateTime)dt).getOffset().normalized().toString(); b.append(of); int os = ((ZonedDateTime)dt).getOffset().getTotalSeconds(); if( os == 0 ){ // output Z b.append('Z'); }else{ // append sign and four characters if( os < 0 ){ b.append('-'); }else{ b.append('+'); } // hours int ox = os / 3600; os = os % 3600; padZeros(b,ox,2); // minutes ox = os / 60; padZeros(b,ox,2); // ignore seconds, not part of ISO } } return b.toString(); } /** * Parses a partial ISO 8601 date time string. * [-]CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm] * [-]CCYY-MM-DDThh:mm:ss[Z|(+|-)hhmm] * <p> * At least the year must be specified. All other fields can be left out. * * @param str ISO 8601 string * @return date time with accuracy as derived from parse * @throws ParseException for unparsable string * @throws IllegalArgumentException unparsable string (old unchecked exception) */ public static DateTimeAccuracy parsePartialIso8601(String str)throws ParseException{ if( str.length() < 4 )throw new ParseException("Need at least 4 characters for year: "+str, str.length()); // parse year int year = Integer.parseInt(str.substring(0, 4)); if( str.length() == 4 ){ // specified to accuracy of years return new DateTimeAccuracy(year); }else if( str.length() < 7 || str.charAt(4) != '-' ){ throw new ParseException("Expected YYYY-MM", Integer.min(4, str.length())); } // parse month int month = Integer.parseInt(str.substring(5, 7)); if( str.length() == 7 ){ // specified to accuracy of months return new DateTimeAccuracy(year, month); }else if( str.length() < 10 || str.charAt(7) != '-' ){ throw new ParseException("Expected YYYY-MM-DD", Integer.min(7, str.length())); } // parse day int day = Integer.parseInt(str.substring(8, 10)); if( str.length() == 10 ){ // specified to accuracy of days return new DateTimeAccuracy(year, month, day); }else if( str.length() < 13 || str.charAt(10) != 'T' ){ throw new ParseException("Expected yyyy-mm-ddThh", Integer.min(10, str.length())); } // parse hours int hours = Integer.parseInt(str.substring(11, 13)); if( str.length() == 13 ){ // specified to accuracy of hours return new DateTimeAccuracy(year, month, day, hours); }else if( str.length() < 16 || str.charAt(13) != ':' ){ throw new ParseException("Expected yyyy-mm-ddThh:mm", Integer.min(13, str.length())); } // parse minutes int mins = Integer.parseInt(str.substring(14, 16)); if( str.length() == 16 ){ // specified to accuracy of minutes return new DateTimeAccuracy(year, month, day, hours, mins); }else if( str.length() < 19 || str.charAt(16) != ':' ){ throw new ParseException("Expected yyyy-mm-ddThh:mm:ss", Integer.min(16, str.length())); } // parse seconds int secs = Integer.parseInt(str.substring(17, 19)); if( str.length() == 19 || (str.length() == 20 && str.charAt(19) == 'Z') ){ // specified to accuracy of seconds return new DateTimeAccuracy(year, month, day, hours, mins, secs); }else if( str.length() < 25 || !(str.charAt(19) != '+' || str.charAt(19) != '-') ){ throw new ParseException("Expected yyyy-mm-ddThh:mm:ss[Z|+oo:oo]", 19); }else if( str.length() != 25 || str.charAt(22) != ':' ){ // handles longer input and missing : in offset throw new ParseException("Expected yyyy-mm-ddThh:mm:ss[Z|+oo:oo]", 22); ParsePosition pos = new ParsePosition(0); TemporalAccessor a = PARTIAL_FORMATTER.parseUnresolved(str, pos); // first check that everything was parsed if( pos.getErrorIndex() != -1 ){ throw new ParseException("Parse error at position "+pos.getErrorIndex(), pos.getErrorIndex()); }else if( pos.getIndex() != str.length() ){ throw new ParseException("Unparsed text found at index "+pos.getIndex()+": "+str.substring(pos.getIndex()), pos.getIndex()); } // everything parsed without error // now check for accuracy ChronoUnit accuracy; LocalDateTime dateTime; if( a.isSupported(ChronoField.NANO_OF_SECOND) ){ // maximum accuracy of nanoseconds // not supported yet, truncate to seconds accuracy = ChronoUnit.NANOS; dateTime = LocalDateTime.from(a); }else if( a.isSupported(ChronoField.SECOND_OF_MINUTE) ){ accuracy = ChronoUnit.SECONDS; dateTime = LocalDateTime.of(a.get(ChronoField.YEAR), a.get(ChronoField.MONTH_OF_YEAR), a.get(ChronoField.DAY_OF_MONTH), a.get(ChronoField.HOUR_OF_DAY), a.get(ChronoField.MINUTE_OF_HOUR), a.get(ChronoField.SECOND_OF_MINUTE)); }else if( a.isSupported(ChronoField.MINUTE_OF_HOUR) ){ accuracy = ChronoUnit.MINUTES; dateTime = LocalDateTime.of(a.get(ChronoField.YEAR), a.get(ChronoField.MONTH_OF_YEAR), a.get(ChronoField.DAY_OF_MONTH), a.get(ChronoField.HOUR_OF_DAY), a.get(ChronoField.MINUTE_OF_HOUR)); }else if( a.isSupported(ChronoField.HOUR_OF_DAY) ){ accuracy = ChronoUnit.HOURS; dateTime = LocalDateTime.of(a.get(ChronoField.YEAR), a.get(ChronoField.MONTH_OF_YEAR), a.get(ChronoField.DAY_OF_MONTH), a.get(ChronoField.HOUR_OF_DAY), 0); }else if( a.isSupported(ChronoField.DAY_OF_MONTH) ){ accuracy = ChronoUnit.DAYS; dateTime = LocalDateTime.of(a.get(ChronoField.YEAR), a.get(ChronoField.MONTH_OF_YEAR), a.get(ChronoField.DAY_OF_MONTH), 0, 0); }else if( a.isSupported(ChronoField.MONTH_OF_YEAR) ){ dateTime = LocalDateTime.of(a.get(ChronoField.YEAR), a.get(ChronoField.MONTH_OF_YEAR), 1, 0, 0); accuracy = ChronoUnit.MONTHS; }else{ DateTimeAccuracy me = new DateTimeAccuracy(year, month, day, hours, mins, secs); // parse time zone ZoneOffset of = ZoneOffset.ofHoursMinutes( Integer.parseInt(str.substring(20, 22)), Integer.parseInt(str.substring(24, 25)) ); // format requires at least year accuracy = ChronoUnit.YEARS; dateTime = LocalDateTime.of(a.get(ChronoField.YEAR), 1, 1, 0, 0); } // check for zone offset ZoneOffset off = null; if( a.isSupported(ChronoField.OFFSET_SECONDS) ){ off = ZoneOffset.ofTotalSeconds(a.get(ChronoField.OFFSET_SECONDS)); // adjust to UTC // TODO unit test for this behavior me.dateTime = me.dateTime.atOffset(of).withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(); return me; dateTime = dateTime.atOffset(off).withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(); } // unparsed data (longer input) will be handled above //throw new ParseException("Unparsed data at index 26", 26); DateTimeAccuracy me = new DateTimeAccuracy(dateTime); me.accuracy = accuracy; return me; } /** Loading
histream-core/src/test/java/de/sekmi/histream/TestDateTimeAccuracy.java +58 −3 Original line number Diff line number Diff line Loading @@ -7,8 +7,11 @@ import java.time.format.DateTimeParseException; import java.time.format.ResolverStyle; import java.time.temporal.ChronoField; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; import java.time.temporal.UnsupportedTemporalTypeException; import org.junit.Assert; import static org.junit.Assert.*; import org.junit.Test; public class TestDateTimeAccuracy { Loading @@ -34,10 +37,62 @@ public class TestDateTimeAccuracy { a = DateTimeAccuracy.parse(formatter, "01.02.2003 13"); Assert.assertEquals(ChronoUnit.HOURS, a.getAccuracy()); Assert.assertEquals("2003-02-01T13", a.toPartialIso8601(null)); Assert.assertEquals("2003-02-01T13+08:00", a.toPartialIso8601(tz)); Assert.assertEquals("2003-02-01T21+0800", a.toPartialIso8601(tz)); } @Test public void verifyDateTimeFormatter(){ TemporalAccessor a; DateTimeFormatter f = DateTimeFormatter.ISO_DATE_TIME; // zone offset missing, field should be not available a = f.parse("2001-02-03T04:05:06"); Assert.assertFalse(a.isSupported(ChronoField.OFFSET_SECONDS)); try{ a.get(ChronoField.OFFSET_SECONDS); Assert.fail("Expected exception not thrown"); }catch( UnsupportedTemporalTypeException e ){ // expected outcome } // zero zone offset, field should be available a = f.parse("2001-02-03T04:05:06Z"); Assert.assertEquals(0, a.get(ChronoField.OFFSET_SECONDS)); a = f.parse("2001-02-03T04:05:06+00:00"); Assert.assertEquals(0, a.get(ChronoField.OFFSET_SECONDS)); a = f.parse("2001-02-03T04:05"); // seconds can be omitted // test the partial timestamp formatter f = DateTimeFormatter.ofPattern("u[-M[-d['T'H[:m[:s[.S]]][X]]]]"); } @Test public void verifyParsingIncompleteIsoTimestamp() throws ParseException{ DateTimeAccuracy a; a = DateTimeAccuracy.parsePartialIso8601("2001"); assertEquals(ChronoUnit.YEARS, a.getAccuracy()); a = DateTimeAccuracy.parsePartialIso8601("2001-02"); assertEquals(ChronoUnit.MONTHS, a.getAccuracy()); a = DateTimeAccuracy.parsePartialIso8601("2001-02-03"); assertEquals(ChronoUnit.DAYS, a.getAccuracy()); a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04"); assertEquals(ChronoUnit.HOURS, a.getAccuracy()); a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:05"); assertEquals(ChronoUnit.MINUTES, a.getAccuracy()); a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:05:06"); assertEquals(ChronoUnit.SECONDS, a.getAccuracy()); // verify zone offset // for second accuracy a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:05:06+0800"); assertEquals(ChronoUnit.SECONDS, a.getAccuracy()); // zone offset calculation assertEquals(DateTimeAccuracy.parsePartialIso8601("2001-02-02T20:05:06Z"), a); a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04+0800"); assertEquals(ChronoUnit.HOURS, a.getAccuracy()); a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04Z"); assertEquals(ChronoUnit.HOURS, a.getAccuracy()); a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:05+0800"); assertEquals(ChronoUnit.MINUTES, a.getAccuracy()); } @Test public void testFormatExceedsText(){ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d.M.u[ H[:m[:s]]]"); Loading @@ -63,7 +118,7 @@ public class TestDateTimeAccuracy { } // TODO test more aspects of zone offset parsing DateTimeAccuracy.parsePartialIso8601("2003-02-01T04:05:06Z"); DateTimeAccuracy a = DateTimeAccuracy.parsePartialIso8601("2003-02-01T04:05:06+01:00"); DateTimeAccuracy a = DateTimeAccuracy.parsePartialIso8601("2003-02-01T04:05:06+0100"); // make sure the date is adjusted to UTC Assert.assertEquals(3, a.get(ChronoField.HOUR_OF_DAY)); } Loading