0

When working with java.util.Calendar on Android, I stumbled upon an interesting bug: on API level 23 or lower some fields set by Calendar.set(...) won't be applied until Calendar.get(...) is called (as the get-method internally calls complete() ?)

This problem occurs, when setting the Calendar.DAY_OF_MONTH multiple times, take a look at the following code:

DateHelper.kt

class DateHelper{
    companion object {
        fun getStartOfWeek(yearInformation : Date, weekNumber: Int, applyFix: Boolean = false): Date {
            val cal = Calendar.getInstance()
            cal.time = yearInformation
            cal.set(Calendar.WEEK_OF_YEAR, weekNumber)
            if(applyFix) cal.get(Calendar.WEEK_OF_YEAR)
            cal.set(Calendar.DAY_OF_WEEK, cal.firstDayOfWeek)
            return cal.time
        }
    }
}

DateHelperTest.kt

@RunWith(AndroidJUnit4::class) 
class DateHelperTest{

    /** setting the week of month only once works */
    @Test
    fun shouldReturnCorrectStartOfWeek(){

        val cal = Calendar.getInstance()
        cal.set(Calendar.YEAR, 2018)
        val yearInformation = cal.time

        val weeknumber = 8 //Sunday = 18.02, Monday = 19.02

        val result = DateHelper.getStartOfWeek(yearInformation, weeknumber)
        cal.time = result
        val acualDay = cal.get(Calendar.DAY_OF_MONTH)

        assertThat(acualDay, anyOf(equalTo(18), equalTo(19)))
    }

    /**
     * setting the week of month twice (calling DateHelper.getStartOfWeek multiple times)
     * doesn't work (for API level <= 23)
     */
    @Test
    fun shouldChangeStartOfWeekCorrectly(){
        val cal = Calendar.getInstance()
        cal.set(Calendar.YEAR, 2018)
        val yearInformation = cal.time

        var weeknumber = 8 //Sunday = 18.02, Monday = 19.02
        DateHelper.getStartOfWeek(yearInformation, weeknumber) //initial call

        weeknumber = 9 //incremented, Sunday = 25, Monday = 26 now
        val result = DateHelper.getStartOfWeek(yearInformation, weeknumber) //second, tested call
        cal.time = result
        val actualDay = cal.get(Calendar.DAY_OF_MONTH)
        assertThat(actualDay, anyOf(equalTo(25), equalTo(26)))
    }

    /**
     * same as shouldChangeStartOfWeekCorrectly() but with workaround
     * (internally calling cal.get())
     * */
    @Test
    fun shouldChangeStartOfWeekCorrectly_WithWorkaround(){
        val cal = Calendar.getInstance()
        cal.set(Calendar.YEAR, 2018)
        val yearInformation = cal.time

        var weeknumber = 8 //Sunday = 18.02, Monday = 19.02
        DateHelper.getStartOfWeek(yearInformation, weeknumber) //initial call

        weeknumber = 9 //incremented, Sunday = 25, Monday = 26 now

        /* date helper call changed */
        val result = DateHelper.getStartOfWeek(yearInformation, weeknumber, applyFix = true) //second, tested call
        /* date helper call changed */            

        cal.time = result
        val actualDay = cal.get(Calendar.DAY_OF_MONTH)
        assertThat(actualDay, anyOf(equalTo(25), equalTo(26)))
    }
}

Example Project on GitHub with executable instrumentation tests

The provided workaround (if(applyFix) cal.get(Calendar.WEEK_OF_YEAR)) is rather unpleasant: how to fix this issue in a proper way?

CodeX
  • 48
  • 6
  • 1
    Why not set year instead of cal.time = Date ? – Lance Toth Feb 22 '18 at 15:07
  • @LanceToth because I use `new Date()` within my app to get the current year dynamically – CodeX Feb 22 '18 at 15:20
  • Oh, well Calendar.getInstance() already contains the current year (obviously) – Lance Toth Feb 22 '18 at 15:21
  • Devil’s advocate here. While I see that the behaviour is unexpected and not what you want, I don’t think it is in disagreement with the documentation. – Ole V.V. Feb 22 '18 at 16:34
  • The `Calendar` class is long outdated. The pleasant fix is to use [ThreeTenABP](https://github.com/JakeWharton/ThreeTenABP) and [java.time, the modern Java date and time API,](https://docs.oracle.com/javase/tutorial/datetime/) instead. – Ole V.V. Feb 22 '18 at 16:37

0 Answers0