Календарь установлен () не работает на Android API 23 и ниже - java.util.Calendar - PullRequest
0 голосов
/ 08 сентября 2018

Я использую java.util.Calendar, чтобы найти начало данной недели, используя методы set ().

  • Это прекрасно работает на Android Nougat +, но не на любой версии Android ниже Marshmallow.

  • Я тестировал как на физических устройствах, так и на эмуляторах.

  • Я использовал отладчик, чтобы убедиться, что проблема заключается в коде Календаря, а не в его отображении.

  • Я использовал Kotlin и Java для создания различных минимальных примеров, и проблема сохраняется в обоих случаях.

Вот минимальный пример Kotlin, где TextView отображает дату, а кнопка используется для увеличения этой даты на неделю:

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, установленным для отображения «05 марта 2018». Это значение изменяется на первый день каждой следующей недели при нажатии кнопки.

На Android Зефир и ниже:

  • Начальное значение TextView установлено на начало текущей недели (3 сентября 2018 г.).
  • Дата не изменяется при нажатии кнопки.
  • Календарь может корректно извлекать последний день текущей недели , если день установлен на Calendar.SUNDAY. Это не будет работать в течение других недель.

Edit: я попытался создать 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 голосов
/ 09 сентября 2018

ТЛ; др

Используйте классы java.time , перенесенные на раннюю версию Android.

Формулировка проблемы: От текущей даты перейдите к предыдущему или тому же понедельнику, затем перейдите к понедельнику стандартной недели ISO 8601 с номером недели по этой дате, добавьте одну неделю и сгенерируйте текст в стандартный формат 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 = верно, переходы = 185, lastRule = java.util.SimpleTimeZone [ID = Америка / Los_Angeles, смещение = -28800000, dstSavings = 3600000, useDaylight = верно, StartYear = 0, StartMode = 3, StartMonth = 2, startDay = 8, startDayOfWeek = 1, начальный промежуток = 7200000, startTimeMode = 0, endMode = 3, endMonth = 10, endDay = 1, endDayOfWeek = 1, EndTime = 7200000, endTimeMode = 0]], firstDayOfWeek = 1, minimalDaysInFirstWeek = 1, ЭРА = 1, год = 2018, МЕСЯЦ = 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, ЧАС = 2, HOUR_OF_DAY = 0, МИНУТНЫЙ = 0, ВТОРОЙ = 0, миллисекунды = 0, ZONE_OFFSET = -28800000, DST_OFFSET = 3600000]

2018-09-03T00: 00-07: 00 [Америка / Los_Angeles]

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 = верно, переходы = 185, lastRule = java.util.SimpleTimeZone [ID = Америка / Los_Angeles, смещение = -28800000, dstSavings = 3600000, useDaylight = верно, StartYear = 0, StartMode = 3, StartMonth = 2, startDay = 8, startDayOfWeek = 1, начальный промежуток = 7200000, startTimeMode = 0, endMode = 3, endMonth = 10, endDay = 1, endDayOfWeek = 1, EndTime = 7200000, endTimeMode = 0]], firstDayOfWeek = 1, minimalDaysInFirstWeek = 1, ЭРА = 1, год = 2018, МЕСЯЦ = 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, ЧАС = 0, HOUR_OF_DAY = 0, МИНУТНЫЙ = 0, ВТОРОЙ = 0, миллисекунды = 0, ZONE_OFFSET = -28800000, DST_OFFSET = 3600000]

2018-03-05T00: 00-08: 00 [Америка / Los_Angeles]

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 = верно, переходы = 185, lastRule = java.util.SimpleTimeZone [ID = Америка / Los_Angeles, смещение = -28800000, dstSavings = 3600000, useDaylight = верно, StartYear = 0, StartMode = 3, StartMonth = 2, startDay = 8, startDayOfWeek = 1, начальный промежуток = 7200000, startTimeMode = 0, endMode = 3, endMonth = 10, endDay = 1, endDayOfWeek = 1, EndTime = 7200000, endTimeMode = 0]], firstDayOfWeek = 1, minimalDaysInFirstWeek = 1, ЭРА = 1, год = 2018, МЕСЯЦ = 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, ЧАС = 0, HOUR_OF_DAY = 0, МИНУТНЫЙ = 0, ВТОРОЙ = 0, миллисекунды = 0, ZONE_OFFSET = -28800000, DST_OFFSET = 0]

2018-03-12T00: 00-07: 00 [Америка / Los_Angeles]

java.time

Действительно, ваша проблема спорная, потому что вам вообще не следует использовать ужасный старый класс Calendar. Это часть проблемных старых классов даты и времени, которые много лет назад были вытеснены современными java.time классами. Для ранних версий Android см. Последние маркеры внизу.

В Calendar / GregorianCalendar определение недели изменяется на Locale, Не так в java.time по умолчанию, который использует стандарт ISO 8601 определение недели .

  • Неделя № 1 имеет первый четверг календарного года.
  • Понедельник - первый день недели.
  • Недельный год имеет 52 или 53 недели.
  • Первые / последние несколько дней календаря могут появиться в предыдущей / следующей неделе на основе года.

LocalDate

Класс LocalDate представляет значение только для даты без времени суток и без часового пояса.

Часовой пояс имеет решающее значение при определении даты. В любой момент времени дата меняется по всему земному шару в зависимости от зоны. Например, через несколько минут после полуночи в Париж Франция - это новый день, хотя все еще "вчера" в Монреаль Квебек .

Если часовой пояс не указан, JVM неявно применяет свой текущий часовой пояс по умолчанию. Это значение по умолчанию может измениться в любой момент во время выполнения (!), Поэтому ваши результаты могут отличаться. Лучше указать в качестве аргумента ваш желаемый / ожидаемый часовой пояс .

Укажите собственное имя часового пояса в формате continent/region, например America/Montreal, Africa/Casablanca или Pacific/Auckland. Никогда не используйте 3-4-буквенное сокращение, такое как EST или IST, поскольку они не истинные часовые пояса, не стандартизированы и даже не уникальны (!).

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.

Или, лучше, использовать предварительно определенные объекты перечисления Month, по одному на каждый месяц года. Совет: используйте эти Month объекты по всей вашей кодовой базе, а не просто целое число, чтобы сделать ваш код более самодокументируемым, обеспечить допустимые значения и обеспечить безопасность типов .

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

TemporalAdjuster

Чтобы перейти к предыдущему понедельнику или остаться на дату, если уже понедельник, используйте реализацию TemporalAdjuster, предоставленную в классе TemporalAdjusters. Укажите желаемый день недели с помощью DayOfWeek enum.

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

IsoFields

Классы java.time имеют ограниченную поддержку в течение нескольких недель. Используйте класс IsoFields с его константами WEEK_OF_WEEK_BASED_YEAR & WEEK_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): Если вы выполняете много работы неделями, рассмотрите возможность добавления библиотеки ThreeTen-Extra для доступа к классу YearWeek.


О java.time

Фреймворк java.time встроен в Java 8 и более поздние версии. Эти классы вытесняют проблемные старые устаревшие классы даты и времени, такие как java.util.Date, Calendar, & SimpleDateFormat.

Проект Joda-Time , теперь в режиме обслуживания , рекомендует выполнить переход на классы java.time .

К лзарабатывайте больше, см. Oracle Tutorial . И поиск переполнения стека для многих примеров и объяснений. Спецификация JSR 310 .

Вы можете обмениваться java.time объектами напрямую с вашей базой данных. Используйте драйвер JDBC , совместимый с JDBC 4.2 или более поздней версии. Нет необходимости в строках, нет необходимости в java.sql.* классах.

Где получить классы java.time?

...