DanteC
DanteC

Reputation: 378

Android - GregorianCalendar displays wrong month

I tried searching the internet and found a lot of questions on StackOverlflow somewhat regarding the same topic, but couldn't find anything that I was able to understand...

So, I have this data class that contains a dateOfOrigin of type GregorianCalendar. Using gson I convert all json and return an Observable array with all locations. Inside the json file, I added the dateOfOrigin as an object like so:

{
    "id": 6,
    "name": "Fuse",
    "image": "fuse.jpg",
    "street": "Blaesstraat 208",
    "city": "Brussels Hoofdstedelijk Gewest",
    "zip": 1000,
    "date_of_origin": {"year":1994,"month":4,"dayOfMonth":16},
    "parking": true
}

And this is what my data class looks like:

data class Location (
    val id : Int,
    val name : String,
    val image : String,
    val street : String,
    val city : String,
    val zip : Int,
    @SerializedName("date_of_origin")
    val originDate : GregorianCalendar?,
    val parking : Boolean = true,
    var imageBitmap : Bitmap? = null
)

Whenever I try to set the dateText like this:

originDate?.let {
    dateText = "${it.get(Calendar.DAY_OF_MONTH)} ${it.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.getDefault())} ${it.get(Calendar.YEAR)}"

    dateText = resources.getString(R.string.origin_date, dateText)
}

It outputs 16 May 1994 instead of 16 Apr 1994

I'm not able to figure out how to fix this...

EDIT Subtracting 1 from the month seems to fix the problem for most cases. Still, I have one result that is supposed to output 30 Jan 2016 but displays 1 Feb 2016.

"date_of_origin": {"year":2016,"month":1,"dayOfMonth":30}

Upvotes: 2

Views: 663

Answers (2)

Basil Bourque
Basil Bourque

Reputation: 339332

The Answer by Hawklike is correct. You were tricked by the crazy month-numbering scheme employed by the GregorianCalendar class. One of many reasons to avoid this class.


tl;dr

myGregCal  
.toZonedDateTime()                           // Convert from obsolete `GregorianCalendar` class to modern `java.time.ZonedDateTime` class.
.toLocalDate()                               // Extract the date portion, without time-of-day and without time zone.
.format(                                     // Generate text representing the value of this date-time object.
    DateTimeFormatter
    .ofLocalizedDate( FormatStyle.MEDIUM )   // Automatically localize.
    .withLocale(                             // Specify a locale for the human language and cultural norms used in localization. 
        Locale.UK
    )
)

23 Jan 2021

Details

Never use GregorianCalendar. This class is part of the date-time classes bundled with the earliest versions of Java. These classes were years ago supplanted by the modern java.time classes defined in JSR 310.

If you must interoperate with code not yet updated to java.time, convert. Call new conversion methods added to the old classes.

GregorianCalendar was replaced by ZonedDateTime.

ZonedDateTime zdt = myGregCal.toZonedDateTime() ;  // From legacy class to modern class.

Going the other direction.

ZonedDateTime zdt = ZonedDateTime.of( 2021 , Month.JANUARY , 23 , 12 , 0 , 0 , 0 , ZoneId.of( "America/Montreal" ) ) ;
GregorianCalendar myGregCal = GregorianCalendar.from( zdt ) ;

Or break that int multiple parts.

LocalDate ld = LocalDate.of( 2021 , Month.JANUARY , 23 ) ;
LocalTime lt = LocalTime.of( 12 , 0 ) ;
ZoneId z = ZoneId.of( "America/Montreal" ) ;
ZonedDateTime zdt = ZonedDateTime.of( ld , lt , z ) ;
GregorianCalendar myGregCal = GregorianCalendar.from( zdt ) ;

Or use month number rather than Month enum for the month. Notice that java.time uses sane numbering, 1-12 for January-December, unlike GregorianCalendar.

LocalDate.of( 2021 , 1 , 23 )  // Same effect as Month.JANUARY. 

To generate text, use DateTimeFormatter class. Your desired format happens to match that of localized format used in the UK. So let java.time automatically localize for you by calling DateTimeFormatter.ofLocalizedDate.

Locale locale = Locale.UK ; 
DateTimeFormatter f = DateTimeFormatter.ofLocalizedDate( FormatStyle.MEDIUM ).withLocale( locale ) ;
String output = zdt2.toLocalDate().format( f ) ;

See this code run live at IdeOne.com.

23 Jan 2021


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes. Hibernate 5 & JPA 2.2 support java.time.

Where to obtain the java.time classes?

Table of which java.time library to use with which version of Java or Android

Upvotes: 1

Hawklike
Hawklike

Reputation: 1181

GregorianCalendar represents months with numbers in the range from 0 to 11. This means that the number 0 is represented as January and 11 is represented as of December.

Therefore you need to subtract 1 if your API is not using the same logic as the Java implementation.

Updated: GregorianCalendar(2016, 1, 30) is understood as 30th of February. This is internally converted to 1st of March, therefore when you subtract one month from the date, you get 1st of February. You need to create an instance of GregorianCalendar class already with the subtracted month number, ie. January as 0, February as 1 and so on.

Upvotes: 3

Related Questions