Round instant with more than 6 digits (nanoseconds) after comma - java

I have postgresql with TIMESTAMP field, which have Instant with date and time. For example: 2021-10-13T15:04:24.944921Z.
As you can see, there are 6 digits after comma - 944921. But what if I have more digits, for example: 2021-10-13T07:14:47.791616921Z. How can I correctly round such Instant to 2021-10-13T07:14:47.791617Z?

The code is short but requires a bit of explanation. You are right, the best we have is the truncatedTo method, and it always rounds down. Instead we want the half-up rounding that we learned in school: If the 7th decimal is 4 or less, round down. If it is 5 or higher, round up.
To obtain this I first add 500 nanoseconds. Then truncate. If the 7th decimal was 4 or less, it is still 9 or less, so the digits before it have not been changed, and truncation will do the rounding-down that we want. If the 7th decimal was 5 or more, adding 500 nanoseconds will overflow it, the 6th decimal will be incremented by 1, and the 7th decimal will end up in the 0 through 4 range. Then truncation will effectively do the rounding up of the original value that we wanted.
Instant sourceValue = Instant.parse("2021-10-13T07:14:47.791616921Z");
Instant rounded = sourceValue.plusNanos(500).truncatedTo(ChronoUnit.MICROS);
System.out.println(rounded);
Output is the desired:
2021-10-13T07:14:47.791617Z
Let’s try the edge cases too.
Instant sourceValue = Instant.parse("2021-10-13T07:14:00.000000499Z");
2021-10-13T07:14:00Z
Instant sourceValue = Instant.parse("2021-10-13T07:14:59.999999500Z");
2021-10-13T07:15:00Z

Related

Java8 LocalDateTime and NANO_OF_SECOND strange formatting

I try this code:
import java.time.*;
...
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(
"dd-MMM-yyyy HH:mm:ss.n");
System.out.format("Now = %s %n", now.format(formatter));
in order to get an output with subsecond information
Now = 12-Apr-2018 14:47:38.039578300
Unfortunately, in the first 100 ms of every second, the leading zero of the subsecond information is omitted and I get a very misleading output Now = 12-Apr-2018 14:47:38.39578300 , which can be easily misinterpreted as about 38.4 sec, or 396 ms after the full second, instead of the real 38.04 sec.
The only workarond I found, is a format of ss.nnnnnnnnn with exactly 9 n, to get my desired output.
Edit:
There is something nicer, which I missed in this area when posting this question.
I'm not really interested in Nanoseconds, but a fractional part of seconds (about ms resolution), is what I'm really looking for.
Then, this one is much more suitable
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
The capital S indicates the number of subsecond digits, including leading zeros of course.
To get better control of fractions you can use the builder, not just pattern letters. Specifically appendFraction.
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendPattern("dd-MMM-yyyy HH:mm:ss")
.appendFraction(ChronoField.NANO_OF_SECOND, 1, 9, true)
.toFormatter();
Pattern letter "n" is rarely what you want.
If you want just ms resolution, you can use S instead of n:
DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("dd-MMM-yyyy HH:mm:ss.SSS", Locale.US);
This will print just the first 3 fractional digits (which is ms resolution):
12-Apr-2018 14:47:38.039
Note that I used a java.util.Locale to define the language to be used for the month name. That's becase the JVM might not always be set to English, and the results can't be what you expect. Ex: my JVM is set to Portuguese and the month name is "abr". Setting a specific locale eliminates this problem.
To print all the 9 digits, using either nnnnnnnnn or SSSSSSSSS will work.
We can see why it behaves like this when we check the javadoc. S and n have different presentations:
Symbol Meaning Presentation Examples
------ ------- ------------ -------
S fraction-of-second fraction 978
n nano-of-second number 987654321
S is a fraction, while n is a number. The docs tell you the difference:
Number: If the count of letters is one, then the value is output using the minimum number of digits and without padding.
Fraction: Outputs the nano-of-second field as a fraction-of-second. The nano-of-second value has nine digits, thus the count of pattern letters is from 1 to 9. If it is less than 9, then the nano-of-second value is truncated, with only the most significant digits being output.
So, just 1 n will print the value without padding (without the 0 in the beginning), leading to the wrong output you've got, while SSS will give you the correct output.
I don't believe there is anything better than nnnnnnnnn. As per DateTimeFormatter docs for n pattern the leading zeros will be truncated if less than 9 pattern letters are used:
Fraction: Outputs the nano-of-second field as a fraction-of-second.
The nano-of-second value has nine digits, thus the count of pattern
letters is from 1 to 9. If it is less than 9, then the nano-of-second
value is truncated, with only the most significant digits being
output.
n and N are the only nano fields supported by DateTimeFormatter.

How to format a double value as time

I am working on a program that reads gps data.
A NMEA string returns time like this: 34658.00.
My parser treats that as a double
InputLine = "\\$GPGGA,34658.00,5106.9792434234,N,11402.3003,W,2,09,1.0,1048.47,M,-16.27,M,08,AAAA*60";
//NMEA string
//inputLine=input.readLine();
if(inputLine.contains("$GPGGA")) {
String gpsgga = inputLine.replace("\\", "");
String[] gga = gpsgga.split(",");
String utc_time = gga[1];
if (!gga[1].isEmpty()) {
Double satTime = Double.parseDouble(utc_time);
gpsData.setgpsTime(satTime);
}
How would I go about formatting 34658.00 as 03:46:58?
DateTimeFormatter nmeaTimeFormatter = DateTimeFormatter.ofPattern("Hmmss.SS");
String utcTime = "34658.00";
System.out.println(LocalTime.parse(utcTime, nmeaTimeFormatter));
This prints
03:46:58
Parsing into a double first is the detour, it just gives you more complicated code than necessary, so avoid that. Just parse the time as you would parse a time in any other format. Does the above also work for times after 10 AM where the hours are two digits? It does. Input:
String utcTime = "123519.00";
Output:
12:35:19
It does require exactly two decimals, though (and will render them back if they are non-zero). If the number of decimals may vary, there are at least two options
Use a DateTimeFormatterBuilder to specify a fractional part (even an optional fractional part) with anything between 0 and 9 decimals.
The hack: use String.replaceFirst with a regular expression to remove the fractional part, and then also remove it from the format pattern string.
It also requires at least 5 digits before the decimal point, so times in the first hour of day need to have leading zeroes, for example 00145.00 for 00:01:45.
Since the time is always in UTC, you may want to use atOffset to convert the LocalTime into an OffsetTime with ZoneOffset.UTC. If you know the date too, an OffsetDateTime or an Instant would be appropriate, but I haven’t delved enough into the documentation to find out whether you know the date.
Links
Oracle tutorial: Date Time explaining how to use java.time.
NMEA data
int hours = (int) Math.floor(satTime / 10000);
int minutes = (int) Math.floor((satTime - hours * 10000) / 100);
double seconds = satTime % 100;
Then zero-pad and add in ':'s in between. You could truncate or round to whole seconds too, if you wish, but then be careful about seconds rounding up to 60, in which case you have to zero the seconds, add 1 to the minutes, and then possible do it over if you get 60 minutes and round up the hours.

JodaTime round Duration/Period to nearest minutes/hours/days/weeks/months/years

If I have two instants in JodaTime, for example
DateTime then=new DateTime(0);
DateTime now=DateTime.now();
and I want to get the number of years between these two dates, I can do
new Period(then,now).getYears();
or
Years.yearsBetween(then,now);
But the problem is that if the interval is not an exact number of years, both of these methods will return the floor of the number of years. So for example, of you have two instants which are two years and 11 months apart, it will still return "0" years.
Is there any way JodaTime provides to get the rounded number of years,months,etc?
The purpose of this is that I wish to display durations in a human friendly format, for example
1 year, 11 months, 30 days, 1 hour -> 2 years.
6 days 4 hours 1 minute -> 1 week
4 hours 18 minutes 5 seconds -> 4 hours
And so I'd like to display properly rounded amounts.
I think I could probably check whether the end of the interval is closer to N Units in the future or N+1 Units, but is there a better way to do this?

java.time ISO date format with fixed millis digits (in Java 8 and later)

By default, the toString method of Instant uses the DateTimeFormatter.ISO_INSTANT formatter. That formatter won’t print the digits for fraction-of-second if they happen to be 0.
java-time examples:
2015-10-08T17:13:07.589Z
2015-10-08T17:13:07Z
Joda-Time examples (and what I'd expect from java.time):
2015-10-08T17:13:07.589Z
2015-10-08T17:13:07.000Z
This is really frustrating to parse in some systems. Elasticsearch was the first problem I encountered, there's no pre-defined format that supports optional millis, but I can probably work around that with a custom format. The default just seems wrong.
It appears that you can’t really build your own format string for Instants anyway. Is the only option implementing my own java.time.format.DateTimeFormatterBuilder.InstantPrinterParser?
Just create a DateTimeFormatter that keeps three fractional digits.
DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendInstant(3).toFormatter();
Then use it. For example:
System.out.println(formatter.format(Instant.now()));
System.out.println(formatter.format(Instant.now().truncatedTo(ChronoUnit.SECONDS)));
…prints (at the time I run it):
2015-10-08T21:26:16.571Z
2015-10-08T21:26:16.000Z
Excerpt of the class doc:
… The fractionalDigits parameter allows the output of the fractional second to be controlled. Specifying zero will cause no fractional digits to be output. From 1 to 9 will output an increasing number of digits, using zero right-padding if necessary. The special value -1 is used to output as many digits as necessary to avoid any trailing zeroes. …

Round down a DateTime based on a given Period using Joda-Time

Given a period such as 3 days, or 5 weeks (a period with only one field type), I want to round a given DateTime to the nearest unit of that period (i.e, ignore the 5 in '5 days'). Examples:
Example 1:
Period: 3 days.
DateTime: Wednesday 4:26 AM UTC (2013-05-15T04:26:00Z)
Rounded DateTime: Wednesday Midnight UTC (2013-05-15T00:00:00Z)
Example 2:
Period: 5 weeks.
DateTime: Wednesday 4:26 AM UTC (2013-05-15T04:26:00Z)
Rounded DateTime: Monday Midnight UTC (2013-05-13T00:00:00Z)
My initial idea was to use Period's DurationFieldType getFieldTypes() method, and for every matching field in a DateTime (below the largest field), set them to zero. However, I don't know how to get the DateTimeFieldTypes from a DateTime and how to compare them to a DurationFieldType.
I would prefer not to do a huge if else approach.
Example bellow is a solution in case you can express period in days (can be modified to weeks, months etc.). Using DateTime Joda Java Library.
Unfortunately with rounding you require I see possible issue. You need to have a starting point in time since when you calculate the periods. In example bellow we calculate periods since 1970/01/01 00:00:00 UTC. Or are you actually asking to get period of 3 days from first day of a month (year) etc? It would make more sense.
Questions you need to ask your self: What will happen on leap days?
Java Method
DateTime roundDays(DateTime dt, int windowDays) {
Duration p = Duration.standardDays(windowDays);
long t = dt.getMillis() / p.getMillis() * p.getMillis();
// Keep TimeZone and round floor to a day
return new DateTime(t, dt.getZone()).dayOfMonth().roundFloorCopy();
}
Example use:
DateTime dt = new DateTime(1385578964580L, DateTimeZone.UTC);
System.out.println(roundDays(dt, 3));
System.out.println(roundDays(dt.plusDays(2), 3));
System.out.println(roundDays(dt.plusDays(4), 3));
System.out.println(roundDays(dt.plusDays(6), 3));
// Prints data rounded to every 3 days
// 2013-11-26T00:00:00.000Z
// 2013-11-29T00:00:00.000Z
// 2013-11-29T00:00:00.000Z
// 2013-12-02T00:00:00.000Z
Too long for comment:
It's not clear what that "rounding" means. To start with, you should deal with LocalDateTimes, not with DateTimes (they are very different things, see my answer here ).
It seems to me you want to set to zero all fields with resolution lower than that of your "period" unit, and then set the next field to a multiple of the given value... is that so? Then, I don't understand your second example (where are the 5 weeks?), and anyway, that would be badly specified: what to do with a period of "40 months" ?

Categories