【问题标题】:Calendar set() broken on Android API 23 and below - java.util.Calendar日历 set() 在 Android API 23 及更低版本上损坏 - java.util.Calendar
【发布时间】:2019-02-13 13:58:40
【问题描述】:

我正在使用 java.util.Calendar 使用其 set() 方法来查找给定一周的开始。

  • 这在 Android Nougat+ 上完美运行,但不适用于 Marshmallow 以下的任何 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 Marshmallow 及以下版本:

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

编辑:我试图创建一个 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);
    }
}

【问题讨论】:

  • 您尝试过使用 GregorianCalendar 吗?
  • 您是否尝试过将日期操作代码与 GUI 代码分开?只需将值转储到控制台以验证值。
  • @BasilBourque,我使用调试器将根本问题确定为日历时间。 (我原本以为这是 Android 中 TextSwitcher 的一个错误。)
  • 如果您编写并发布了一段没有 GUI 的纯控制台代码,其他人将更容易验证您的结果。见:MCVE
  • @TheRealChx101 Joda-Time 项目处于维护模式,建议迁移到 java.time 类。在 ThreeTen-Backport 中向后移植到 Java 6/7,并在 ThreeTenABP 中进一步适应 Android。

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


【解决方案1】:

tl;博士

使用向后移植到早期 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,transitions=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=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=2,HOUR_OF_DAY= 0,MINUTE=0,SECOND=0,MILLISECOND=0,ZONE_OFFSET=-28800000,DST_OFFSET=3600000]

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

gc (week=10): 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,transitions=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,transitions=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 中默认情况并非如此,它使用ISO 8601 标准definition of a week

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

LocalDate

LocalDate 类表示没有时间和时区的仅日期值。

时区对于确定日期至关重要。对于任何给定的时刻,日期在全球范围内因区域而异。例如,Paris France 中午夜过后几分钟是新的一天,而 Montréal Québec 中仍然是“昨天”。

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

continent/region 的格式指定proper time zone name,例如America/MontrealAfrica/CasablancaPacific/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 月的编号为 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.

或者,更好的是,使用预定义的Month 枚举对象,一年中的每个月一个。提示:在整个代码库中使用这些 Month 对象,而不是仅仅使用整数,以使您的代码更具自我记录性、确保有效值并提供 type-safety

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

TemporalAdjuster

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

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

IsoFields

java.time 类在数周内的支持有限。使用 IsoFields 类及其常量 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

weekLater输出:2018-W11-1

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


关于java.time

java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310

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

从哪里获得 java.time 类?

【讨论】:

    猜你喜欢
    • 2020-01-22
    • 1970-01-01
    • 2018-01-22
    • 1970-01-01
    • 2016-09-10
    • 1970-01-01
    • 2016-06-22
    • 2016-12-20
    • 1970-01-01
    相关资源
    最近更新 更多