日历set()在Android API 23及更低版本上损坏-java.util.Calendar

时间:2018-09-08 13:06:30

标签: java android kotlin android-6.0-marshmallow java.util.calendar

我正在使用java.util.Calendar通过其set()方法查找给定星期的开始。

  • 这在Android Nougat +上完美运行,但不适用于棉花糖以下的任何Android版本。

  • 我已经在物理设备和仿真器上进行了测试。

  • 我已使用调试器来验证问题出在日历代码上,而不是显示它时遇到了一些问题。

  • 我已经使用Kotlin和Java创建了不同的最小示例,但问题仍然存在。

以下是Kotlin的最小示例,其中TextView显示日期,而Button用于将该日期增加一周:

class MainActivity : AppCompatActivity() {

    var week = 10

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Set TextView to show the date of the 10th week in 2018.
        setCalendarText(week) 

        // Increase the week on every button click, and show the new date.
        button.setOnClickListener { setCalendarText(++week) }
    }

    /**
     * Set the text of a TextView, defined in XML, to the date of
     * a given week in 2018.
     */
    fun setCalendarText(week: Int) {
        val cal = Calendar.getInstance().apply {
            firstDayOfWeek = Calendar.MONDAY
            set(Calendar.YEAR, 2018)
            set(Calendar.WEEK_OF_YEAR, week)
            set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
            set(Calendar.HOUR_OF_DAY, 0)
            set(Calendar.MINUTE, 0)
            set(Calendar.SECOND, 1)
        }
        textView.text = SimpleDateFormat("dd MMMM yyyy", Locale.UK).format(cal.time)
    }
}

按预期方式工作时,活动启动,TextView设置为显示“ 2018年3月5日”。单击按钮后,此值将更改为每连续一周的第一天。

在Android棉花糖及更低版本上:

  • TextView的初始值设置为当前周的开始(2018年9月3日)。
  • 单击按钮时,日期不会更改。
  • 如果日历设置为Calendar.SUNDAY,则日历可以正确检索当前当前周的最后一天。其他任何星期都无法使用。

编辑:我试图创建Java MVCE,该Java MVCE允许您通过运行CalendarTester.test()来快速检查是否出现基本问题。

import android.util.Log;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;

class CalendarTester {

    /**
     * Check that the Calendar returns the correct date for
     * the start of the 10th week of 2018 instead of returning
     * the start of the current week.
     */
    public static void test() {
        // en_US on my machine, but should probably be en_GB.
        String locale = Locale.getDefault().toString();
        Log.v("CalendarTester", "The locale is " + locale);

        Long startOfTenthWeek = getStartOfGivenWeek(10);
        String startOfTenthWeekFormatted = formatDate(startOfTenthWeek);

        boolean isCorrect = "05 March 2018".equals(startOfTenthWeekFormatted);

        Log.v("CalendarTester", String.format("The calculated date is %s, which is %s",
                startOfTenthWeekFormatted, isCorrect ? "CORRECT" : "WRONG"));
    }

    public static Long getStartOfGivenWeek(int week) {
        Calendar cal = Calendar.getInstance();
        cal.setFirstDayOfWeek(Calendar.MONDAY);
        cal.set(Calendar.YEAR, 2018);
        cal.set(Calendar.WEEK_OF_YEAR, week);
        cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 1);

        return cal.getTimeInMillis();
    }

    public static String formatDate(Long timeInMillis) {
        return new SimpleDateFormat("dd MMMM yyyy", Locale.UK).format(timeInMillis);
    }
}

1 个答案:

答案 0 :(得分:2)

tl; dr

使用回溯到早期Android的 java.time 类。

问题陈述:从当前日期开始,移至上一个或相同的星期一,然后移至该日期的基于周的年份的标准ISO 8601第10周的星期一,添加一周并生成文本以标准ISO 8601格式表示日期。

org.threeten.bp.LocalDate.now(         // Represent a date-only value, without time-of-day and without time zone.
    ZoneId.of( "Europe/London" )       // Determining current date requires a time zone. For any given moment, the date and time vary around the globe by zone.
)                                      // Returns a `LocalDate`. Per immutable objects pattern, any further actions generate another object rather than changing (“mutating”) this object.
.with(                          
    TemporalAdjusters.previousOrSame(  // Move to another date.
        DayOfWeek.MONDAY               // Specify desired day-of-week using `DayOfWeek` enum, with seven objects pre-defined for each day-of-week.
    ) 
)                                      // Renders another `LocalDate` object. 
.with( 
    IsoFields.WEEK_OF_WEEK_BASED_YEAR ,
    10
)
.plusWeeks( 1 )
.toString() 
  

2018-03-12

简化问题

在跟踪神秘或错误的行为时,只需简单地编程即可重现问题。在这种情况下,请删除不相关的GUI代码以专注于日期时间类。

就像在科学实验中一样,控制各种变量。在这种情况下,时区和Locale都会影响Calendar的行为。一方面,Calendar中一周的定义因Locale而异。因此,请通过硬编码明确指定这些方面。

设置特定的日期和时间,因为不同区域中不同日期的不同时间会影响行为。

Calendar是具有各种实现的超类。如果您期望GregorianCalendar,请在调试时明确使用。

因此,请尝试在各种工具方案中运行类似以下内容的程序来解决问题。

TimeZone tz = TimeZone.getTimeZone( "America/Los_Angeles" );
Locale locale = Locale.US;
GregorianCalendar gc = new GregorianCalendar( tz , locale );
gc.set( 2018 , 9- 1 , 3 , 0 , 0 , 0 );  // Subtract 1 from month number to account for nonsensical month numbering used by this terrible class.
gc.set( Calendar.MILLISECOND , 0 ); // Clear fractional second.
System.out.println( "gc (original): " + gc.toString() );
System.out.println( gc.toZonedDateTime() + "\n" );  // Generate a more readable string, using modern java.time classes. Delete this line if running on Android <26. 

int week = 10;
gc.set( Calendar.WEEK_OF_YEAR , week );
System.out.println( "gc (week=10): " + gc.toString() );
System.out.println( gc.toZonedDateTime() + "\n" );

int weekAfter = ( week + 1 );
gc.set( Calendar.WEEK_OF_YEAR , weekAfter );
System.out.println( "gc (weekAfter): " + gc.toString() );
System.out.println( gc.toZonedDateTime() + "\n" );

运行时。

  

gc(原始):java.util.GregorianCalendar [time = ?, areFieldsSet = false,areAllFieldsSet = true,lenient = true,zone = sun.util.calendar.ZoneInfo [id =“ America / Los_Angeles”,offset = -28800000,dstSavings = 3600000,useDaylight = true,转换= 185,lastRule = java.util.SimpleTimeZone [id = America / Los_Angeles,offset = -28800000,dstSavings = 3600000,useDaylight = true,startYear = 0,startMode = 3, startMonth = 2,startDay = 8,startDayOfWeek = 1,startTime = 7200000,startTimeMode = 0,endMode = 3,endMonth = 10,endDay = 1,endDayOfWeek = 1,endTime = 7200000,endTimeMode = 0]],firstDayOfWeek = 1, minimalDaysInFirstWeek = 1,ERA = 1,YEAR = 2018,MONTH = 8,WEEK_OF_YEAR = 36,WEEK_OF_MONTH = 2,DAY_OF_MONTH = 3,DAY_OF_YEAR = 251,DAY_OF_WEEK = 7,DAY_OF_WEEK_IN_MONTH = 2,AM_PM = 1,HOUR_OF = 2,HOUR_OF 0,MINUTE = 0,SECOND = 0,MILLISECOND = 0,ZONE_OFFSET = -28800000,DST_OFFSET = 3600000]

     

2018-09-03T00:00-07:00 [美国/洛杉矶]

     

gc(周= 10):java.util.GregorianCalendar [time = ?, areFieldsSet = false,areAllFieldsSet = true,lenient = true,zone = sun.util.calendar.ZoneInfo [id =“ America / Los_Angeles”,偏移量= -28800000,dstSavings = 3600000,useDaylight = true,转换= 185,lastRule = java.util.SimpleTimeZone [id = America / Los_Angeles,offset = -28800000,dstSavings = 3600000,useDaylight = true,startYear = 0,startMode = 3,startMonth = 2,startDay = 8,startDayOfWeek = 1,startTime = 7200000,startTimeMode = 0,endMode = 3,endMonth = 10,endDay = 1,endDayOfWeek = 1,endTime = 7200000,endTimeMode = 0]],firstDayOfWeek = 1,minimalDaysInFirstWeek = 1,ERA = 1,YEAR = 2018,MONTH = 8,WEEK_OF_YEAR = 10,WEEK_OF_MONTH = 2,DAY_OF_MONTH = 3,DAY_OF_YEAR = 246,DAY_OF_WEEK = 2,DAY_OF_WEEK_IN_MONTH = 1,AM_PM = 0,HOUR = 0, HOUR_OF_DAY = 0,MINUTE = 0,SECOND = 0,MILLISECOND = 0,ZONE_OFFSET = -28800000,DST_OFFSET = 3600000]

     

2018-03-05T00:00-08:00 [美国/洛杉矶]

     

gc(weekAfter):java.util.GregorianCalendar [time = ?, areFieldsSet = false,areAllFieldsSet = true,lenient = true,zone = sun.util.calendar.ZoneInfo [id =“ America / Los_Angeles”,offset = -28800000,dstSavings = 3600000,useDaylight = true,转换= 185,lastRule = java.util.SimpleTimeZone [id = America / Los_Angeles,offset = -28800000,dstSavings = 3600000,useDaylight = true,startYear = 0,startMode = 3, startMonth = 2,startDay = 8,startDayOfWeek = 1,startTime = 7200000,startTimeMode = 0,endMode = 3,endMonth = 10,endDay = 1,endDayOfWeek = 1,endTime = 7200000,endTimeMode = 0]],firstDayOfWeek = 1, minimumDaysInFirstWeek = 1,ERA = 1,YEAR = 2018,MONTH = 2,WEEK_OF_YEAR = 11,WEEK_OF_MONTH = 2,DAY_OF_MONTH = 5,DAY_OF_YEAR = 64,DAY_OF_WEEK = 2,DAY_OF_WEEK_IN_MONTH = 1,AM_PM = 0,HOUR = 0,HOUR_OF_DAY 0,MINUTE = 0,SECOND = 0,MILLISECOND = 0,ZONE_OFFSET = -28800000,DST_OFFSET = 0]

     

2018-03-12T00:00-07:00 [美国/洛杉矶]

java.time

确实,您的问题没有解决,因为您根本不应该使用可怕的旧Calendar类。它是几年前麻烦的旧日期时间类的一部分,而现代的 java.time 类取代了该类。对于早期的Android,请参阅下面底部的最后一个项目符号。

Calendar / GregorianCalendar中,一周的定义因Locale而异,默认情况下,在 java.time 中使用{{ 3}}标准ISO 8601

  • 第1周是日历年的第一个星期四。
  • 星期一是一周的第一天。
  • 基于一周的年份为52或53周。
  • 日历的前几天/后几天可能出现在上一周/下一周。

LocalDate

definition of a week类表示没有日期和时区的仅日期值。

时区对于确定日期至关重要。在任何给定时刻,日期都会在全球范围内变化。例如,LocalDate午夜之后的几分钟是新的一天,而Paris France仍然是“昨天”。

如果未指定时区,则JVM隐式应用其当前的默认时区。该默认值可能在运行时(!)期间Montréal Québec,因此您的结果可能会有所不同。最好将change at any moment明确指定为参数。

continent/region的格式指定desired/expected time zone,例如proper time zone nameAmerica/MontrealPacific/Auckland。切勿使用3-4个字母的缩写,例如ESTIST,因为它们不是真正的时区,不是标准化的,甚至不是唯一的(!)。

ZoneId z = ZoneId.of( "America/Montreal" ) ;  
LocalDate today = LocalDate.now( z ) ;

如果要使用JVM的当前默认时区,请提出要求并作为参数传递。如果省略,则会隐式应用JVM的当前默认值。最好明确一点,因为默认值可能会在运行时的任何时候被JVM中任何应用程序的任何线程中的任何代码更改。

ZoneId z = ZoneId.systemDefault() ;  // Get JVM’s current default time zone.

或指定日期。您可以用数字设置月份,一月至十二月的理智编号为1-12。

LocalDate ld = LocalDate.of( 1986 , 2 , 23 ) ;  // Years use sane direct numbering (1986 means year 1986). Months use sane numbering, 1-12 for January-December.

或者更好的是,使用预定义的Africa/Casablanca枚举对象,每年的每个月使用一个。提示:在整个代码库中使用这些Month对象,而不是仅使用整数,可以使您的代码更具自文档性,确保有效值并提供Month

LocalDate ld = LocalDate.of( 2018 , Month.SEPTEMBER , 3 ) ;

TemporalAdjuster

要移至上一个星期一,或者如果已经是星期一,则保留该日期,请使用TemporalAdjuster类中提供的TemporalAdjusters实现。用DayOfWeek枚举指定所需的星期几。

LocalDate monday = ld.with( TemporalAdjusters.previousOrSame( DayOfWeek.MONDAY ) ) ;

IsoFields

java.time 类在数周内的支持有限。将type-safety类及其常数WEEK_OF_WEEK_BASED_YEARWEEK_BASED_YEAR一起使用。

LocalDate mondayOfWeekTen = monday.with( IsoFields.WEEK_OF_WEEK_BASED_YEAR , 10 ) ;

ISO 8601

ISO 8601标准定义了许多有用的实用格式,用于将日期时间值表示为文本。这包括几周。让我们生成这样的文本作为输出。

String weekLaterOutput = 
    weekLater
    .get( IsoFields.WEEK_BASED_YEAR ) 
    + "-W" 
    + String.format( "%02d" , weekLater.get( IsoFields.WEEK_OF_WEEK_BASED_YEAR ) ) 
    + "-" 
    + weekLater.getDayOfWeek().getValue()
; // Generate standard ISO 8601 output. Ex: 2018-W11-1

转储到控制台。

System.out.println("ld.toString(): " + ld);
System.out.println("monday.toString(): " +monday);
System.out.println("weekLater.toString(): " + weekLater);
System.out.println( "weekLaterOutput: " + weekLaterOutput ) ;

运行时。

  

ld.toString():2018-09-03

     

monday.toString():2018-09-03

     

weekLater.toString():2018-03-12

     

weekLaterOutput:2018-W11-1

针对Java的提示(非Android):如果需要花费数周时间进行大量工作,请考虑添加IsoFields库以访问其YearWeek类。


关于 java.time

ThreeTen-Extra框架已内置在Java 8及更高版本中。这些类取代了麻烦的旧java.time日期时间类,例如legacyjava.util.DateCalendar

目前位于SimpleDateFormatJoda-Time项目建议迁移到maintenance mode类。

要了解更多信息,请参见java.time。并在Stack Overflow中搜索许多示例和说明。规格为Oracle Tutorial

您可以直接与数据库交换 java.time 对象。使用符合JSR 310或更高版本的JDBC driver。不需要字符串,不需要java.sql.*类。

在哪里获取java.time类?