DateFormat ParseException only during unit testing - android

I'm writing unit tests for Android code using the Robolectric gradle test runner. The code I'm testing happens to be hitting date formatting methods that use the following format:
yyyy-MM-dd'T'HH:mm:ss.SSSZ
We store Unix Time milliseconds as strings in that format, and just before sending it through the formatter to convert it back into a millisecond offset, we replace any instances of "Z" in the string with "+00:00". the call ends up looking like this:
DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
format.setTimeZone(TimeZone.getTimeZone("UTC"));
format.parse(validDateString.replace("Z", "+00:00));
This operation works fine in production code, but attempting to write unit tests has revealed previously unseen ParseExceptions. I first assumed it was because of a formatting issue with the date string I was injecting, but ParseExceptions are being thrown on strings saved from a date successfully parsed in prod code.
What could possibly be causing this radical difference in behavior?
Things I've tried already:
-Checked date formatting
-This DateFormat is actually a global static variable. I'm aware that they're not thread-safe, but inlining all static references with new instances yields the same results.
UPDATE:
Partial stack trace
java.text.ParseException: Unparseable date: "2016-02-20T19:47:33.262+00:00"
at java.text.DateFormat.parse(DateFormat.java:357)
...nothing else useful
Additionally, I should mention that we use a complementary method that stores milliseconds as a String:
DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
Date date = new Date(unixTime);
String validDateString = format.format(date);
return validDateString.replace("+0000", "Z");
Note that we're replacing 4 0's without a colon, whereas the method that fails appends 00:00. That said, the not-quite-complementary operations work fine in production, but fail in unit tests.

Try setting your string for unit test directly:
String validDateString = "2016-02-20T19:47:33.262+0000"; // remove the colon
I had the same issue when running Robolectric. As per this issue on GitHub, replacing +00:00 with +0000 fixed the issue for me.

Related

org.threeten.bp zonedDateTime.parse does not work [duplicate]

I recently moved to Java 8 to, hopefully, deal with local and zoned times more easily.
However, I'm facing an, in my opinion, simple problem when parsing a simple date.
public static ZonedDateTime convertirAFecha(String fecha) throws Exception {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(
ConstantesFechas.FORMATO_DIA).withZone(
obtenerZonaHorariaServidor());
ZonedDateTime resultado = ZonedDateTime.parse(fecha, formatter);
return resultado;
}
In my case:
fecha is '15/06/2014'
ConstantesFechas.FORMATO_DIA is 'dd/MM/yyyy'
obtenerZonaHorariaServidor returns ZoneId.systemDefault()
So, this is a simple example. However, the parse throws this exception:
java.time.format.DateTimeParseException: Text '15/06/2014' could not
be parsed: Unable to obtain ZonedDateTime from TemporalAccessor:
{},ISO resolved to 2014-06-15 of type java.time.format.Parsed
Any tips? I've been trying different combinations of parsing and using TemporalAccesor, but without any luck so far.
This does not work because your input (and your Formatter) do not have time zone information. A simple way is to parse your date as a LocalDate first (without time or time zone information) then create a ZonedDateTime:
public static ZonedDateTime convertirAFecha(String fecha) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date = LocalDate.parse(fecha, formatter);
ZonedDateTime resultado = date.atStartOfDay(ZoneId.systemDefault());
return resultado;
}
This is a bug, see JDK-bug-log. According to that information the problem was solved for Java 9 and Java 8u20. Try to download the latest Java 8 - version. Today on 2014-05-12: There is an early access release 8u20 available.
UPDATE:
Personally I think, since you only have and expect "dd/MM/yyyy" as pattern you should use LocalDate as your primary type as #assylias has already proposed. Regarding your context, it is almost sure a design failure to use ZonedDateTime. What do you want to do with objects of this type? I can only think of specialized timezone calculations as use-case. And you cannot even directly store these ZonedDateTime-objects in a database, so this type is far less useful than many people believe.
What I described as your use-case problem is indeed a new aspect introduced with Java-8 compared with the old GregorianCalendar-class (which is an all-in-one-type). Users have to start thinking about choosing the proper temporal type for their problems and use-cases.
In simple words, the line
ZonedDateTime.parse('2014-04-23', DateTimeFormatter.ISO_OFFSET_DATE_TIME)
throws an exception:
Text '2014-04-23' could not be parsed at index 10
java.time.format.DateTimeParseException: Text '2014-04-23' could not be parsed at index 10
It looks like a bug for me.
I used this workaround:
String dateAsStr = '2014-04-23';
if (dateAsStr.length() == 10) {
dateAsStr += 'T00:00:00';
}
ZonedDateTime.parse(dateAsStr, DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault()));
If coming from Google:
Instead of doing:
ZonedDateTime.from(new Date().toInstant());
Try this:
ZonedDateTime.ofInstant(new Date(), ZoneId.of("UTC"));
Just an example conversions, I believe some folks will get the exception below
(java.time.DateTimeException: Unable to obtain LocalDateTime from TemporalAccessor: 2014-10-24T18:22:09.800Z of type java.time.Instant)
if they try
LocalDateTime localDateTime = LocalDateTime.from(new Date().toInstant());
to resolve the issue, please pass in Zone -
LocalDateTime localDateTime = LocalDateTime.from(new Date()
.toInstant().atZone(ZoneId.of("UTC")));

Android and iOS libraries behave as if there never was a daylight saving time before 1996

Jargon:
CET : Central European Time.
Daylight saving time : UTC+1 in winter, UTC+2 in summer.
In the CET region, Android and iOS libraries behave as if there never was a daylight saving time before 1996 while .Net behaves as it has always existed.
To illustrate this behaviour, here's some code written in .NET/Java executed on CET machines.
In .Net :
static void PrintDate(String input)
{
String format = "yyyy-MM-ddTHH:mm:ss.fffffffzzz";
var date = DateTime.ParseExact(input, format, CultureInfo.InvariantCulture);
var output = date.ToString(format, CultureInfo.InvariantCulture);
System.Diagnostics.Debug.WriteLine(input + " => " + output);
}
In Android and iOS (just the java example but both behave in the same manner).
static void printDate(String input)
{
String format = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSZZZZZ";
Date date = new SimpleDateFormat(format).parse(input);
String output = new SimpleDateFormat(format).format(date);
Log.i("MyTag", input + " => " + output);
}
A simple call to the method :
public static void Main()
{
PrintDate("1993-10-06T00:00:00.0000000+02:00");
PrintDate("1993-12-06T00:00:00.0000000+02:00");
PrintDate("1996-10-06T00:00:00.0000000+02:00");
PrintDate("1996-12-06T00:00:00.0000000+02:00");
}
Here's the output in .Net :
1993-10-06T00:00:00.0000000+02:00 => 1993-10-06T00:00:00.0000000+02:00
1993-12-06T00:00:00.0000000+02:00 => 1993-12-05T23:00:00.0000000+01:00
1996-10-06T00:00:00.0000000+02:00 => 1996-10-06T00:00:00.0000000+02:00
1996-12-06T00:00:00.0000000+02:00 => 1996-12-05T23:00:00.0000000+01:00
And here's the output in Android/iOs
1993-10-06T00:00:00.0000000+02:00 => 1993-10-05T23:00:00.0000000+01:00
1993-12-06T00:00:00.0000000+02:00 => 1993-12-05T23:00:00.0000000+01:00
1996-10-06T00:00:00.0000000+02:00 => 1996-10-06T00:00:00.0000000+02:00
1996-12-06T00:00:00.0000000+02:00 => 1996-12-05T23:00:00.0000000+01:00
How could I homogenize the behaviour between these three polatforms?
In Java, SimpleDateFormat uses the JVM default timezone if you don't set one in it. (check what's yours with TimeZone.getDefault()).
So 1993-10-06T00:00+02:00 is converted to 1993-10-05T23:00+01:00 probably because the default timezone is one with +01:00 offset in October 1993, while in October 1996 it was in DST (+02:00). My guess is Europe/Paris, but it can be other, as lots of timezones uses CET as a short name.
Anyway, just check the history of DST in Paris and note that in October 1993 the offset was +01:00 while in October 6th 1996 it was +02:00. So it's a good guess, but any timezone with the same rules will have the same behaviour.
Also, +02:00 is an offset, not a timezone. Just being +02:00 doesn't necessarily mean that it's CET during DST, because there's more than one timezone that uses this offset. And short names like CET are ambiguous and not standard, so you should consider using IANA timezones names (always in the format Region/City, like America/Sao_Paulo or Europe/Paris).
Anyway, if you don't want to have variable offsets, you shouldn't rely on the JVM default timezone, because it can have DST effects and the offset will vary according to the date (and the default timezone can be changed without notice, even at runtime). One way to avoid it, is to set a fixed offset in the formatter:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSZZZZZ");
// set the offset +02:00, so all dates will be formatted using this
// (instead of the current offset for the JVM default timezone)
sdf.setTimeZone(TimeZone.getTimeZone("GMT+02:00"));
Date date = sdf.parse("1993-10-06T00:00:00.0000000+02:00");
System.out.println(sdf.format(date));
Just some quick notes:
SimpleDateFormat doesn't work well with more than 3 digits after the decimal point. In the case above, it works fine because it's all zeroes, but if you have any value different from zero and more than 3 digits, you can have strange, wrong, unexpected results. In this case, you should remove the extra digits, because this class simply can't handle more than 3 (and it also doesn't work well for formatting).
I'm testing with JDK 7, so the pattern ZZZZZ doesn't work for parsing. Instead, I've used yyyy-MM-dd'T'HH:mm:ss.SSSSSSSXXX, which parses the inputs above, and formats the date to 1993-10-06T00:00:00.0000000+02:00 (but note that the X is not available in JDK 6)
If you want the output in another offset, just change it accordingly in the getTimeZone method. If you want UTC, use getTimeZone("UTC")
Java new Date/Time API
The old classes (Date, Calendar and SimpleDateFormat) have lots of problems and design issues, and they're being replaced by the new APIs.
In Android, you can use the ThreeTen Backport, a great backport for Java 8's new date/time classes. To make it work, you'll also need the ThreeTenABP (more on how to use it here).
One improvement in this new API is the support to nanoseconds (up to 9 digits after the decimal point), so it can handle your inputs without the problems of SimpleDateFormat.
This new API also has lots of new date/time types suited for different situations. In this case, you have a date and time in a specific offset, and wish to maintain it. So, the best class is a org.threeten.bp.OffsetDateTime:
OffsetDateTime odt = OffsetDateTime.parse("1993-10-06T00:00:00.0000000+02:00");
System.out.println(odt.toString()); // 1993-10-06T00:00+02:00
Note that the toString() method omits the seconds and nanoseconds if they are zero. If you want the output exactly like the input (with 7 digits after the decimal point), just create a org.threeten.bp.format.DateTimeFormatter:
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSXXX");
System.out.println(fmt.format(odt)); // 1993-10-06T00:00:00.0000000+02:00
To change this to another offset (or to UTC), use a org.threeten.bp.ZoneOffset:
// convert to UTC
odt = odt.withOffsetSameInstant(ZoneOffset.UTC);
System.out.println(fmt.format(odt)); // 1993-10-05T22:00:00.0000000Z
// convert to another offset (+01:00)
odt = odt.withOffsetSameInstant(ZoneOffset.ofHours(1));
System.out.println(fmt.format(odt)); // 1993-10-05T23:00:00.0000000+01:00

Issue with formatting time zone for Joda DateTime from ISO String

So time formatting and adjusting has always been my biggest nemesis in programing and I'm having some issues in Android/Java that I can't figure out. I get a timestamp from a server that is formatted in UTC (here's an example 2016-06-17T18:30:00-07:00. Now this time needs to get formatted to the users local time (so for a user in PST it should show as 11:30AM) but so far whatever I try I either get 1AM or 6:30PM (so I know I'm doing something wrong I just don't know what). Here's what I've been trying to do
public static DateTime convertISOStringToDate(String inputString) {
//setup the ISO Date Formatter with GMT/UTC format
DateTimeFormatter formatter = ISODateTimeFormat.dateTimeParser()
.withLocale(Locale.US)
.withZone(DateTimeZone.forOffsetHours(0));
DateTime dateTime = formatter.parseDateTime(inputString);
//now convert the datetime object to a local date time object
DateTimeFormatter localFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss")
.withZone(DateTimeZone.getDefault());
String localString = localFormatter.print(dateTime);
DateTime localDateTime = localFormatter.parseDateTime(localString);
return localDateTime;
So at this point I'm getting 1:30AM, so I know I'm messing it up somewhere in the conversion process but I can't figure it out. I've been trying to google around but so far haven't found much that use the ISODateTimeFormat parser so they don't work either when I try them.
You seem to have a basic mis-understanding of how dates are represented.
A date (in almost every known programming language / library) is represented internally as an offset from a specific 'origin time', known as the 'Epoch'.
In java.util.Date as well as joda dates, the internal representation is the number of milliseconds since midnight, Jan 1, 1980, UTC.
As such, a date does not have a timezone. You only introduce a timezone when you format a date (turn it into a String representation of the date).
You have made the common mistake of parsing a String into a date object, serializing (printing) it back out with a different timezone than the the original string indicated, and then parsing back into a date again, expecting something to have changed. If you do that correctly, you will get back exactly the same date that you started with.
In your case, the "localString" that you get shows the correct time in the local timezone. I'm in EDT, which is UTC-4:00, and I correctly get 2016-06-17 21:30:00 as the result.
As I said, parsing that back into a DateTime, and then looking at it is useless, because:
You'll get the same DateTime back that you started with
Your IDE (or whatever you're using to inspect the DateTime) probably isn't showing what you expect.
You should re-evaluate what you're doing here, and whether you really need to "convert" the DateTime, or to just parse it, and really understand how date formatting works with respect to timezones.

How to get hours from JodaTime with timezone

I encounter strange behaviour with Jodatime and Android. I want to parse string:
2014-05-19T18:13:00.000+02:00
to DateTime, and get year, month, hours to int. I started with some test on IntelliJ Studio, and I done something like that:
String date = "2014-05-19T18:13:00.000+02:00";
DateTime dateTime = new DateTime(date);
System.out.println(dateTime.toString());
System.out.println(dateTime.getYear());
System.out.println(dateTime.getMonthOfYear());
System.out.println(dateTime.getDayOfMonth());
System.out.println(dateTime.getHourOfDay());
System.out.println(dateTime.getMinuteOfHour());
System.out.println(dateTime.getMillis());
Which gave me correct answers:
2014-05-19T18:13:00.000+02:00
2014
5
19
18
13
1400515980000
Now, when I changed IDE to Android Studio, and do the same:
String dateT = "2014-05-19T18:13:00.000+02:00";
DateTime dateTime = new DateTime(dateT);
Lo.g(dateTime.getHourOfDay() + "");
Lo.g(dateTime.toString());
my results are:
16
2014-05-19T16:13:00.000Z
For some reason DateTime on Android Studio / Android not take into account the timezone which is +2:00.
I can not find solution for this. Also there is no simple method "addTimeZone" in Joda.
How to display correct time with DataTime? I tried LocalDateTime, construct DateTime with DateTimeZone.getDefault() (which gaves me UTF...)
Since you said that you use the same Joda-Time version on both platforms and regarding the fact that Joda-Time has its own timezone repository independent from system timezone data, there is probably only one explanation left why you observe different behaviour: Different input either explicit or implicit. Let's go into details:
Well, you say, obviously there is the same input given the same input string:
"2014-05-19T18:13:00.000+02:00"
So we have the same (explicit) input. But wait, there is another thing: implicit default settings which can also be considered as kind of input in an abstract way. You use the constructor DateTime(Object). This constructor first delegates to super constructor of class BaseDateTime as you can see in the source code.
public DateTime(Object instant) {
super(instant, (Chronology) null);
}
The javadoc of this super-constructor says:
"Constructs an instance from an Object that represents a datetime,
using the specified chronology. If the chronology is null, ISO in the
default time zone is used.
The recognised object types are defined in ConverterManager and
include ReadableInstant, String, Calendar and Date."
So finally we see that Joda-Time uses the default timezone. This is really the only possibility for different behaviour I can see by studying the source code and the documentation. All other things are equal: Same library version and same explicit string input and same test scenario.
Conclusion: You have different default timezones on your platforms. Note that you get the same instant on both platforms however, just represented with different local timestamps and offsets due to different internal timezone/offset setting inside the DateTime-object.
Update: I have tested the zone overriding behaviour with this code:
String date = "2014-05-19T19:13:00.000+03:00"; // same instant as yours (just with offset +03)
DateTime dateTime = new DateTime(date);
System.out.println(dateTime.toString());
// output: 2014-05-19T18:13:00.000+02:00 (in my default timezone Europe/Berlin)
So the default timezone has precedence over any offset in string input. If you instead want to use the parsed offset then look at using the DateTimeFormatter and its method withOffsetParsed().

Time zone offset format +HH:mm versus +HHmm

I need to communicate with one application that is not implementing the full ISO 8601 and only accept the format +HH:mm for time zone offset.
Android seems to only generate the format +HHmm (no ':' character in between hours and minutes) with the 'z' in SampleDateFormat.
Code example:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
String str = String.format(Locale.US, "%s", sdf.format(new Date(0)));
generates: 1970-01-01T00:00+0000
and I would like to generate: 1970-01-01T00:00+00:00
Is there any simple way of producing the desirable output without writing the code to manipulate the string?
Thanks,
Luis
I think the easiest way is to manipulate a string.
a) SimpleDateFormat doesn't support what you need.
b) The method appendNumericTimeZone in SimpleDateFormat class is private. So, you can't override it.
2) You can create your own formater (implement java.text.DataFormat). However, it will be way more hassle than string manipulation.
BTW. Interesting thing which I found while looking into SimpleDateFormat source code. There is some code which generates almost what you need (it adds to the end "GMT+XX:XX"). However, this code will be only called, if you specified "Z" in the format and system can't find a timezone name for current timezone.

Categories

Resources