How to parse LDAP Generalized Time in Java 8? - java

Lightweight Directory Access Protocol (LDAP): Syntaxes and Matching Rules defines a Generalized Time which has a seconds component that may be a leap-second.
There are more complications with the definition since the fractional component could refer to either be fractional hours, minutes or seconds depending on what is present.
I have tried various approaches using the DateTimeFormatterBuilder such as a appendInstant, parseUnresolved, and even a custom TemporalField.
The API for DateTimeFormatter takes/returns types written against the date/time abstractions but yet it doesn't seem to actually work with anything other than the standard implementations which is pretty disappointing.
Surely others have hit similar custom formats and I'd hope that these use cases were considered as part of JSR-310.
What are the options?
Is it possible to create a custom formatter which can reuse most of the existing ISO8601 parsing logic?

Discussion of Java-8-approach:
Handling of leap seconds
I don't know if parsing leap seconds is really important for you (because it is rather an exotic feature which hardly happens in standard business applications), but I would not recommend to use the standard Java-8-API for doing it, see also the officially documented limitations:
the handling of leap-seconds is limited to
DateTimeFormatterBuilder.appendInstant()
So following intuitive approach fails (and does so for any other pattern):
DateTimeFormatter dtf =
DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ssXXX").withResolverStyle(ResolverStyle.SMART);
TemporalAccessor raw = dtf.parse("2016-12-31T23:59:60Z");
Instant instant = Instant.from(raw);
System.out.println(
instant
+ " (leap-second-parsed=" + raw.query(DateTimeFormatter.parsedLeapSecond()) + ")");
Rather you have to do this:
DateTimeFormatter dtf =
DateTimeFormatter.ISO_INSTANT;
TemporalAccessor raw = dtf.parse("2016-12-31T23:59:60Z");
Instant instant = Instant.from(raw);
System.out.println(
instant
+ " (leap-second-parsed=" + raw.query(DateTimeFormatter.parsedLeapSecond()) + ")");
// 2016-12-31T23:59:59Z (leap-second-parsed=true)
However, it fails for an input with timezone offset not equal to zero, and the code does not validate due to internal lack of leap second data if the input is really a true leap second, for example it says that "2015-05-01T23:59:60Z" is a leap second (but we know better it is not such one).
Handling of decimal hours and minutes
The suggested solution given by S. Colebourne (the author of java.time-API) is flawed. Using appendFraction() suffers from handling only ONE element but handling of decimal parts requires to handle the specified element AND all other elements with higher precision. See first the printing example (based on the code of the proposal):
DateTimeFormatter f = new DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd HH")
.appendFraction(ChronoField.MINUTE_OF_HOUR, 0, 6, true)
.appendOffsetId()
.toFormatter();
OffsetDateTime dt = OffsetDateTime.of(2017, 3, 21, 5, 28, 59, 0, ZoneOffset.UTC);
System.out.println(dt); // 2017-03-21T05:28:59Z
System.out.println(dt.format(f)); // 2017-03-21 05.466666Z
OffsetDateTime dt2 = OffsetDateTime.of(2017, 3, 21, 5, 28, 0, 0, ZoneOffset.UTC);
System.out.println(dt2); // 2017-03-21T05:28Z
System.out.println(dt2.format(f)); // 2017-03-21 05.466666Z
We see that two different OffsetDateTime-values result in the same decimal hour which is obviously wrong. The difference was just a delta in the field SECOND_OF_MINUTE (not taken into account by appendFraction()).
What about parsing? We can observe the same effect in reverse which makes the whole approach unusable.
Let the input to be parsed "2017-03-01 13.52Z" as given in the proposal as example. The observed parsed value is: 2017-03-01T13:31Z But this result is NOT correct. It should be: 2017-03-01T13:31.2Z or 2017-03-01T13:31:12Z (explanation: 0.52 x 60 = 31.2 => minute component and 0.2 * 60 = 12 => second component).
Conclusion: Don't use the standard API for handling decimal values of time-related elements. There is no support by design. I say even "by design" because all fields finally work with a long-primitive (as value type) which is not suitable to incorporate decimal values based on several fields.
What to do else? I have set up my own library to fill gaps in the java.time-API like described above.
Time4J-solution (v4.25 or later):
I suggest you to use following code in order to model the LDAP-specification. It is rather complex but this is necessary due to the complexity of the specification itself.
ChronoFormatter<PlainDate> df =
ChronoFormatter.setUp(PlainDate.axis(), Locale.ROOT)
.addFixedInteger(PlainDate.YEAR, 4)
.addFixedInteger(PlainDate.MONTH_AS_NUMBER, 2)
.addFixedInteger(PlainDate.DAY_OF_MONTH, 2)
.build();
ChronoFormatter<Moment> mf =
ChronoFormatter.setUp(Moment.axis(), Locale.US) // US for preference of dot in decimal elements
.addCustomized(PlainDate.COMPONENT, df)
.addFixedInteger(PlainTime.DIGITAL_HOUR_OF_DAY, 2)
.startOptionalSection()
.addFixedInteger(PlainTime.MINUTE_OF_HOUR, 2)
.startOptionalSection()
.addFixedInteger(PlainTime.SECOND_OF_MINUTE, 2)
.startOptionalSection()
.addLiteral('.', ',')
.addFraction(PlainTime.NANO_OF_SECOND, 1, 9, false)
.endSection()
.endSection()
.endSection()
.addTimezoneOffset(DisplayMode.SHORT, false, Collections.singletonList("Z"))
.or()
.addCustomized(PlainDate.COMPONENT, df)
.addFixedInteger(PlainTime.DIGITAL_HOUR_OF_DAY, 2)
.addFixedDecimal(PlainTime.DECIMAL_MINUTE)
.addTimezoneOffset(DisplayMode.SHORT, false, Collections.singletonList("Z"))
.or()
.addCustomized(PlainDate.COMPONENT, df)
.addFixedDecimal(PlainTime.DECIMAL_HOUR)
.addTimezoneOffset(DisplayMode.SHORT, false, Collections.singletonList("Z"))
.build();
assertThat(
mf.parse("199412160532-0500").toString(),
is("1994-12-16T10:32:00Z"));
assertThat(
mf.parse("199412160532Z").toString(),
is("1994-12-16T05:32:00Z"));
assertThat(
mf.parse("20161231185960.123456789-0500").toString(),
is("2016-12-31T23:59:60,123456789Z"));
assertThat(
mf.parse("201612311859.25-0500").toString(),
is("2016-12-31T23:59:15Z"));
assertThat(
mf.parse("2016123118.25-0500").toString(),
is("2016-12-31T23:15:00Z"));
As you can see, the code does handle leap seconds (even with non-zero offsets). Time4J also validates leap seconds because it manages its independent leap second data (for example extracted from IANA-TZDB). And a leap second is stored within an object of type Moment. This type is the counterpart to java.time.Instant. A conversion between both types is trivial (or directly via the method moment.toTemporalAccessor()). Just to note: The leap second itself will be lost during such a conversion. If you just want to ignore the leap second i.e. handle it like the last second before then either just go with the conversion to Instant or work with standard POSIX-related methods in Moment (and the conversion to any "local" types like PlainTimestamp/LocalDateTime etc. also looses the leap second).
Decimal values are also supported because the interface ChronoElement (as counterpart to TemporalField) is generified and based on an object value type, not a long-primitive, see for example the element for the decimal minute which uses the value type BigDecimal.
Finally parsing dots or commas is possible (as required by the LDAP-specification). This is a detail which is also not supported by Java-8, for comparison see the JDK-issue 8163932.

This code parses fractional hours. It can be adjusted for fractional days:
DateTimeFormatter f = new DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd HH")
.appendFraction(MINUTE_OF_HOUR, 0, 6, true)
.appendOffsetId()
.toFormatter();
OffsetDateTime dt = OffsetDateTime.now();
System.out.println(dt.format(f));
System.out.println(OffsetDateTime.parse("2017-03-01 13.52Z", f));
This code can be used to find a leap second:
DateTimeFormatter fmt = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
String text = "2017-03-01T23:59:60";
ParsePosition pp = new ParsePosition(0);
TemporalAccessor accessor = fmt.parseUnresolved(text, pp);
if (pp.getErrorIndex() >= 0) {
throw new DateTimeParseException("Parse error", text, pp.getErrorIndex());
}
if (accessor.getLong(SECOND_OF_MINUTE) == 60) {
System.out.println("Leap second");
} else {
System.out.println("Not a leap second");
}
The time library is very extensible. There are implementations at ThreeTen-Extra which show what can be done in terms of alternate date-time classes like YearQuarter and alternative fields like PackedFields.
Update:
It is worth noting for posterity that the complexity here comes from the need to support fractional minutes, hours and days. When parsing a standard Instant, leap seconds are just ignored by default.

Related

Unable to parse optional microseconds in localTime

I am receiving timestamp in format : HHmmss followed by milleseconds and microseconds.Microseconds after the '.' are optional
For example: "timestamp ":"152656375.489991" is 15:26:56:375.489991.
Below code is throwing exceptions:
final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
.appendPattern("HHmmssSSS")
.appendFraction(ChronoField.MICRO_OF_SECOND, 0, 6, true)
.toFormatter();
LocalTime.parse(dateTime,FORMATTER);
Can someone please help me with DateTimeformatter to get LocalTime in java.
Here is the stacktrace from the exception from the code above:
java.time.format.DateTimeParseException: Text '152656375.489991' could not be parsed: Conflict found: NanoOfSecond 375000000 differs from NanoOfSecond 489991000 while resolving MicroOfSecond
at java.base/java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:1959)
at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1894)
at java.base/java.time.LocalTime.parse(LocalTime.java:463)
at com.ajax.so.Test.main(Test.java:31)
Caused by: java.time.DateTimeException: Conflict found: NanoOfSecond 375000000 differs from NanoOfSecond 489991000 while resolving MicroOfSecond
at java.base/java.time.format.Parsed.updateCheckConflict(Parsed.java:329)
at java.base/java.time.format.Parsed.resolveTimeFields(Parsed.java:462)
at java.base/java.time.format.Parsed.resolveFields(Parsed.java:267)
at java.base/java.time.format.Parsed.resolve(Parsed.java:253)
at java.base/java.time.format.DateTimeParseContext.toResolved(DateTimeParseContext.java:331)
at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1994)
at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1890)
... 3 more
There are many options, depending on the possible variations in the strings you need to parse.
1. Modify the string so you need no formatter
String timestampString = "152656375.489991";
timestampString = timestampString.replaceFirst(
"^(\\d{2})(\\d{2})(\\d{2})(\\d{3})(?:\\.(\\d*))?$", "$1:$2:$3.$4$5");
System.out.println(timestampString);
LocalTime time = LocalTime.parse(timestampString);
System.out.println(time);
The output from this snippet is:
15:26:56.375489991
The replaceFirst() call modifies your string into 15:26:56.375489991, the default format for LocalTime (ISO 8601) so it can be parsed without any explicit formatter. For this I am using a regular expression that may not be too readable. (…) enclose groups that I use as $1, $2, etc., in the replacement string. (?:…) denotes a non-capturing group, that is, cannot be used in the replacement string. I put a ? after it to specify that this group is optional in the original string.
This solution accepts from 1 through 6 decimals after the point and also no fractional part at all.
2. Use a simpler string modification and a formatter
I want to modify the string so I can use this formatter:
private static DateTimeFormatter fullParser
= DateTimeFormatter.ofPattern("HHmmss.[SSSSSSSSS][SSS]");
This requires the point to be after the seconds rather than after the milliseoncds. So move it three places to the left:
timestampString = timestampString.replaceFirst("(\\d{3})(?:\\.|$)", ".$1");
LocalTime time = LocalTime.parse(timestampString, fullParser);
15:26:56.375489991
Again I am using a non-capturing group, this time to say that after the (captured) group of three digits must come either a dot or the end of the string.
3. The same with a more flexible parser
The formatter above specifies that there must be either 9 or 3 digits after the decimal point, which may be too rigid. If you want to accept something in between too, a builder can build a more flexible formatter:
private static DateTimeFormatter fullParser = new DateTimeFormatterBuilder()
.appendPattern("HHmmss")
.appendFraction(ChronoField.NANO_OF_SECOND, 3, 9, true)
.toFormatter();
I think that this would be my favourite approach, again depending on the exact requirements.
4. Parse only a part of the string
There is no problem so big and awful that it cannot simply be run away
from (Linus in Peanuts, from memory)
If you can live without the microseconds, ignore them:
private static DateTimeFormatter partialParser
= DateTimeFormatter.ofPattern("HHmmssSSS");
To parse only a the part of the string up to the point using this formatter:
TemporalAccessor parsed
= partialParser.parse(timestampString, new ParsePosition(0));
LocalTime time = LocalTime.from(parsed);
15:26:56.375
As you can see it has ignored the part from the decimal point, which I wouldn’t find too satisfactory.
What went wrong in your code?
Your 6 digits after the decimal point denote nanoseconds. Microseconds would have been only 3 decimals after the milliseconds. To use appendFraction() to parse these you would have needed a TemporalUnit of nano of millisecond. The ChronoUnit enum offers nano of day and nano of second, but not nano of milli. TemporalUnit is an interface, so in theory we could develop our own nano of milli class for the purpose. I tried to develop a class implementing TemporalUnit once, but gave up, I couldn’t get it to work.
Links
Wikipedia article: ISO 8601
Regular expressions in Java - Tutorial

Java Instant.parse on Date java 8

I have some legacy KML documents which includes a time stamp entry.
Why is the below date not valid when using Instant to parse? Both methods are suppose to parse ISO 8601 formatted dates.
String dateString = "2017-12-04T08:06:60Z"
Using
java.time.Instant.parse(dateString)
throws an error
"DateTimeParseException Text 2017-12-04T08:06:60Z could not be parsed at index 0."
However, when using
Date myDate = javax.xml.bind.DatatypeConverter.parseDateTime( dateString )
myDate is parsed correctly....
60 seconds isn't a valid time. Meaning that this is invalid 2017-12-04T08:06:60Z, if it was 60 seconds then the minute should have incremented and your time would be 2017-12-04T08:07:00Z
Using a valid date and then parsing the String would work just fine:
String date = "2017-12-04T08:07:00Z";
System.out.println(Instant.parse(date));
Also java.time ignores leap seconds. From the docs:
Implementations of the Java time-scale using the JSR-310 API are not
required to provide any clock that is sub-second accurate, or that
progresses monotonically or smoothly. Implementations are therefore
not required to actually perform the UTC-SLS slew or to otherwise be
aware of leap seconds. JSR-310 does, however, require that
implementations must document the approach they use when defining a
clock representing the current instant. See Clock for details on the
available clocks.
The accepted answer is fine. I just have two things to add:
You can parse the string with the invalid second value of 60 by using ResolverStyle.LENIENT.
Since Jon Skeet in a comment mentioned a possible leap second: It’s not a valid leap second. java.time does support the parsing of a (valid) leap second.
Parsing your string
DateTimeFormatter lenientFormatter
= DateTimeFormatter.ISO_OFFSET_DATE_TIME
.withResolverStyle(ResolverStyle.LENIENT);
String dateString = "2018-12-04T08:06:60Z";
Instant myInstant = lenientFormatter.parse(dateString, Instant::from);
System.out.println(myInstant);
Output:
2018-12-04T08:07:00Z
So the overflowing second value of 60 has been rolled into a full minute.
By the way, javax.xml.bind.DatatypeConverter.parseDateTime parses into a Calendar (not a Date), which is how the returned object can in fact hold a second value of 60. It seems that it generally accepts a second value of 60, but throws an exception on 61.
Parsing a valid leap second
This does in no way answer your question, but I thought that it might be useful for future readers. A leap second is always the last second of the day, so 23:59:60. An Instant cannot hold this value, but you can query whether one was parsed. It’s supported via DateTimeFormatterBuilder.appendInstant(), and DateTimeFormatter.parsedLeapSecond().
DateTimeFormatter leapSecondFormatter = new DateTimeFormatterBuilder()
.appendInstant()
.toFormatter();
Instant myInstant
= leapSecondFormatter.parse("2018-12-04T23:59:60Z", Instant::from);
System.out.println(myInstant);
TemporalAccessor parsed = leapSecondFormatter.parse("2018-12-04T23:59:60Z");
System.out.println("Instant: " + parsed.query(Instant::from));
System.out.println("Was a leap second parsed? "
+ parsed.query(DateTimeFormatter.parsedLeapSecond()));
Output:
2018-12-04T23:59:59Z
Instant: 2018-12-04T23:59:59Z
Was a leap second parsed? true
I don’t know why it had to be this complicated, but it works.
Link: Documentation of DateTimeFormatter.parsedLeapSecond

Compare LDAP date to epoch

I'm trying to calculate LDAP accountExpires.
The given value is LDAP date - nanoseconds since 01/01/1601 00:00.
What is the best way to test if it is indeed after new Date()?
The best way probably depends on your precision requirements. I suggest
private static final Instant ldapEpoch = LocalDateTime.of(1601, Month.JANUARY, 1, 0, 0)
.atOffset(ZoneOffset.UTC)
.toInstant();
and then
long ldapTime = 131_428_662_140_000_000L;
Instant convertedTime = ldapEpoch.plusMillis( ldapTime / 10_000L );
System.out.println(convertedTime.isAfter(Instant.now()));
With my example LDAP time value this produces an Instant of 2017-06-25T12:10:14Z and prints false because the time is not after the current time.
Since you mentioned new Date() in the question, I assumed that the precision of Date would suffice for you, that is, milliseconds. I would really have loved to do ldapEpoch.plusNanos(ldapTime * 100) to keep the full precision, but this overflows the Java long data type and therefore gives an incorrect result. If you need the full precision, … Edit: as suggested by Basil Bourque in a comment, slice off the fractional second, work in whole seconds, then add back your fractional second:
Instant convertedTime = ldapEpoch.plusSeconds( ldapTime / 10_000_000L )
.plusNanos( ldapTime % 10_000_000L * 100L );
(The way I had first presented works too, gives the same result; but the edited version may be more natural to readers who know the Java date & time API (and may also perform a slight bit better, but that’s hardly critical).)
Why I wanted to multiply by 100? The LDAP, Active Directory & Filetime Timestamp Converter I found says “The timestamp is the number of 100-nanoseconds intervals (1 nanosecond = one billionth of a second) since Jan 1, 1601 UTC.”
Beware that in 1601 not everyone agreed about calendars, so January 1 that year is ambiguous. Most computer software assumes the Gregorian calendar, so I guess the definition of LDAP time does too, it’s not something I know.

Problems when moving from SimpleDateFormat to DateTimeFormatter

I have been successfully using SimpleDateFormat for the last couple of years. I built a bunch of time utility classes using it.
As I ran into problems with SimpleDateFormat (SDF) not being thread safe, I spent the last couple of days refactoring these utility classes to internally use DateTimeFormatter (DTF) now. Since both classes' time patterns are almost identical, this transition seemed a good idea at the time.
I now have problems obtaining EpochMillis (milliseconds since 1970-01-01T00:00:00Z): While SDF would e.g. interpret 10:30 parsed using HH:mm as 1970-01-01T10:30:00Z, DTF does not do the same. DTF can use 10:30 to parse a LocalTime, but not a ZonedDateTime which is needed to obtain EpochMillis.
I understand that the objects of java.time follow a different philosophy; Date, Time, and Zoned objects are kept separately. However, in order for my utility class to interpret all strings as it did before, I need to be able to define the default parsing for all missing objects dynamically. I tried to use
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
builder.parseDefaulting(ChronoField.YEAR, 1970);
builder.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1);
builder.parseDefaulting(ChronoField.DAY_OF_MONTH, 1);
builder.parseDefaulting(ChronoField.HOUR_OF_DAY, 0);
builder.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0);
builder.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0);
builder.append(DateTimeFormatter.ofPattern(pattern));
but this does not work for all patterns. It seems to only allow defaults for parameters that are not defined in pattern. Is there a way to test which ChronoFields are defined in pattern to then selectively add defaults?
Alternatively, I tried
TemporalAccessor temporal = formatter.parseBest(time,
ZonedDateTime::from,
LocalDateTime::from,
LocalDate::from,
LocalTime::from,
YearMonth::from,
Year::from,
Month::from);
if ( temporal instanceof ZonedDateTime )
return (ZonedDateTime)temporal;
if ( temporal instanceof LocalDateTime )
return ((LocalDateTime)temporal).atZone(formatter.getZone());
if ( temporal instanceof LocalDate )
return ((LocalDate)temporal).atStartOfDay().atZone(formatter.getZone());
if ( temporal instanceof LocalTime )
return ((LocalTime)temporal).atDate(LocalDate.of(1970, 1, 1)).atZone(formatter.getZone());
if ( temporal instanceof YearMonth )
return ((YearMonth)temporal).atDay(1).atStartOfDay().atZone(formatter.getZone());
if ( temporal instanceof Year )
return ((Year)temporal).atMonth(1).atDay(1).atStartOfDay().atZone(formatter.getZone());
if ( temporal instanceof Month )
return Year.of(1970).atMonth((Month)temporal).atDay(1).atStartOfDay().atZone(formatter.getZone());
which does not cover all cases either.
What is the best strategy to enable dynamic date / time / date-time / zone-date-time parsing?
Java-8-solution:
Change the order of your parsing instructions inside the builder such that the defaulting instructions all happen AFTER the pattern instruction.
For example using this static code (well, your approach will use an instance-based combination of different patterns, not performant at all):
private static final DateTimeFormatter FLEXIBLE_FORMATTER;
static {
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
builder.appendPattern("MM/dd");
builder.parseDefaulting(ChronoField.YEAR_OF_ERA, 1970);
builder.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1);
builder.parseDefaulting(ChronoField.DAY_OF_MONTH, 1);
builder.parseDefaulting(ChronoField.HOUR_OF_DAY, 0);
builder.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0);
builder.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0);
FLEXIBLE_FORMATTER = builder.toFormatter();
}
Reason:
The method parseDefaulting(...) works in a funny way, namely like an embedded parser. That means, this method will inject a default value for defined field if that field has not been parsed yet. And the later pattern instruction tries to parse the same field (here: MONTH_OF_YEAR for pattern "MM/dd" and input "07/13") but with a possibly different value. If so then the composite parser will abort because it has found ambivalent values for same field and is unable to resolve the conflict (parsed value 7, but default value 1).
The official API contains following notice:
During parsing, the current state of the parse is inspected. If the
specified field has no associated value, because it has not been
parsed successfully at that point, then the specified value is
injected into the parse result. Injection is immediate, thus the
field-value pair will be visible to any subsequent elements in the
formatter. As such, this method is normally called at the end of the
builder.
We should read it as:
Dont't call parseDefaulting(...) before any parsing instruction for the same field.
Side note 1:
Your alternative approach based on parseBest(...) is even worse because
it does not cover all combinations with missing minute or only missing year (MonthDay?) etc. The default value solution is more flexible.
it is performancewise not worth to be discussed.
Side note 2:
I would rather have made the whole implementation order-insensitive because this detail is like a trap for many users. And it is possible to avoid this trap by choosing a map-based implementation for default values as done in my own time library Time4J where the order of default-value-instructions does not matter at all because injecting default values only happens after all fields have been parsed. Time4J also offers a dedicated answer to "What is the best strategy to enable dynamic date / time / date-time / zone-date-time parsing?" by offering a MultiFormatParser.
UPDATE:
In Java-8: Use ChronoField.YEAR_OF_ERA instead of ChronoField.YEAR because the pattern contains the letter "y" (=year-of-era, not the same as proleptic gregorian year). Otherwise the parse engine will inject the proleptic default year in addition to parsed year-of-era and will find a conflict. A real pitfall. Just yesterday I had fixed a similar pitfall in my time library for the month field which exists in two slightly different variations.
I have used new java.time package and it takes time getting used to it. But after a learning curve I have to say it is definitely very comprehensive and robust solution probably superseding Joda time library and other previous solutions. I wrote my own utilities for working with parsing Strings to Date. I wrote a summarizing article that explains how I implemented a feature that parsed String of unknown format to Date. It might be helpful. Here is the link to an article: Java 8 java.time package: parsing any string to date

Java : Custom Timestamp Format : verify format to microsec precision

My Objective is
to create a java class that can handle the below two requirements
(A) 1. Verify if the format of a timestamp matches with expected format.
CCYY-MM-DD'T'hh:mm:ss'.0000000000+'uh:um"
Ex: the expected format is not static.
It may be either of these
"2013-09-10T18:30:20.123456+10:00" or
"2013-09-10T18:30:20.123+10:00".
I am not bothered about the
precision and value. Only the format matters.
(B) 2. Verify if the timestamp is in a certain range.
Ex: Verify if the timestamp is in
between "2013-09-10 18:27" and "2013-09-10 18:33". (verification is only upto minute level precision) (may be a delta of + or - 2min)
As suggested by one of the member, I have edited the post to target at
One specific question.
The QUESTION :
How to validate the custom timestamp upto microsec precision using JAVA class ?
The two arguments for this class will be
1) Expected FORMAT as a String
2) timestamp value as a String
Based on analysis from various search results, below is my understanding :
Java (by default) does not parse/format Timestamp at microsecond level( I used SimpleDateFormat)
If 6 digits are given in milliseconds place, it will re-calculate the value into seconds and the dateformat will be updated and the new dateformat will have 3 digits in milliseconds precision.
I have also seen a thread which suggests to use java.sql.Timestamp.
Tried this approach but not working.
I was not able to convert my strTimestamp 2013-09-10T18:30:20.123456+10:00 into Timestamp object.
Timestamp ts = Timestamp.valueOf(strTimestamp);
java.lang.IllegalArgumentException:
Timestamp format must be yyyy-mm-dd hh:mm:ss[.fffffffff]
I was not able convert my input format into Timestamp object.
I have a workaround to validate using regular expression :
2013-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[1-5][0-9]):(0[0-9]|[1-5][0-9]).[0-9][0-9][0-9][0-9][0-9][0-9]\+10:00
The problem with this reg ex is that, my expected timestamp format is not static. So i have to use a regex for every pattern.
So I am trying to figure out if there is any robust solution in java, which can be self sufficient even if the expected format changes.
java.time in Java 8
JSR 310 defined a new java.time package in Java 8. Its date-time class resolves to nanoseconds. That gives you 9 digits after the decimal point.
The java.time package is inspired by Joda-Time but entirely re-architected. Concepts are similar.
Like Joda-Time, the java.time package uses ISO 8601 formats as its defaults for parsing and formatting. So you can input or output strings such as 2013-09-10T18:30:20.123456789+10:00.
An early release of Java 8 is available now. Official release should be this month.
A project to backport this package to earlier versions of Java was underway. I do not know of its current status or success. The backport project is independent of Oracle and the OpenJDK project.
Milliseconds
The old bundled classes, java.util.Date & .Calendar, use a precision of milliseconds.
Ditto for the excellent Joda-Time library, milliseconds precision.
So not enough digits in the fractional seconds to meet your needs.
A java.sql.Timestamp is not going to help you, because that is a java.util.Date.
The code is fairly simple, if you use the right format String with SimpleDateFormat, which you let do the heavy lifting. Here's an entire working solution:
public static boolean isNear(String timestamp, int microPlaces, Date near, int minutes) {
if (!timestamp.matches(".*\\.\\d{" + microPlaces + "}\\D.*") {
return false;
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss.SSSSSSZ");
try {
Date date = sdf.parse(timestamp.replaceAll(":(?=\\d\\d$)", ""));
return Math.abs(date.getTime() - near.getTime()) <= minutes * 60000;
} catch (ParseException ignore) {
return false; // string was not of correct format
}
}
This may not be exactly what you had in mind - if not, you should be able to use it as a basis for what you want. The key points are:
The S format string means "microseconds", and it doesn't require all the digits - so your timestamp can have any number
Java 6 needs the colon removed from the timezone. Java 7 doesn't need this - use the X format string instead of Z
A failure to parse a date from the input throws a ParseException - do what you want with this event
I chose to make the API give central date for the range and a +/- minute value. You may need to pass two dates - up to you. Use Date.before() and Date.after() to compare if you do that.
Here's some test code testing your examples and a couple of edge cases:
public static void main(String[] args) throws Exception {
Date near = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm").parse("2013-09-10T18:32");
System.out.println(isNear("2013-09-10T18:30:20.123456+10:00", near, 2));
System.out.println(isNear("2013-09-10T18:30:20.123+10:00", near, 2));
System.out.println(isNear("2013-09-10T18:10:20.123+10:00", near, 1));
System.out.println(isNear("XXXX-09-10T18:10:20.123+10:00", near, 1));
}
Output:
true
true
false
false
Really I`m also trying to find answer to this problem. As I have no ability to add comment to the Bohemian answer. I want to mention that 'S' pattern in SimpleDateFormat is used not for microseconds but for milliseconds. It means that for pattern "yyyy-MM-dd'T'hh:mm:ss.SSSSSSZ" provided microsecond digits in string would be parsed as milliseconds.
So the first three digits would be passed as XXX seconds and their value value would be added to date. So we can receive mistake about 16 minutes.

Categories