【问题标题】:Printing calendar starting from the first day of the week based on locale using java.time/Java-8使用 java.time/Java-8 根据语言环境从一周的第一天开始打印日历
【发布时间】:2021-11-03 03:21:38
【问题描述】:

我见过的所有日历视图库和基于日历的日期选择器的示例都使用旧的日历 API。但是,我还没有发现任何使用 Java 8 日期 API 来构建日历视图的工具。这是我正在努力实现并且基本上已经完成的事情,但我遇到的问题是使用语言环境设置一周的第一天。

我已经设法获取并显示给定月份一周中每一天的所有日期。但是,我遇到的问题是生成的日历不是根据我的语言环境从一周的第一天开始的。 Java.Time 使用的DayOfWeek 枚举从星期一到星期日(1 - 7)开始计算天数。但是,我希望日历按地区显示日期,在我的例子中是周日到周六。

根据文档,WeekFields 类提供了对基于区域设置的星期几的访问,并具有“正确”的值,但我不确定如何正确使用它。

这是我到目前为止所做的:

private enum class DaysOfWeek {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
}

fun TimetableCalendar() {
    val dates = getDaysOfMonth(LocalDate.now().year, LocalDate.now().month)
    
    // WORK AROUND: Hardcoding the first day of the week by manually inserting them into a new map
    val tempDates = hashMapOf<DaysOfWeek, MutableList<LocalDate>>()
    tempDates[DaysOfWeek.SUNDAY] = dates[DayOfWeek.SUNDAY] !!
    tempDates[DaysOfWeek.MONDAY] = dates[DayOfWeek.MONDAY] !!
    tempDates[DaysOfWeek.TUESDAY] = dates[DayOfWeek.TUESDAY] !!
    tempDates[DaysOfWeek.WEDNESDAY] = dates[DayOfWeek.WEDNESDAY] !!
    tempDates[DaysOfWeek.THURSDAY] = dates[DayOfWeek.THURSDAY] !!
    tempDates[DaysOfWeek.FRIDAY] = dates[DayOfWeek.FRIDAY] !!
    tempDates[DaysOfWeek.SATURDAY] = dates[DayOfWeek.SATURDAY] !!

    // Sort the days by ordinal, 0 - 6
    val sortedDates = tempDates.toSortedMap(compareBy {
        d -> d.ordinal
    })

    LazyVerticalGrid(
        cells = GridCells.Fixed(7),
        contentPadding = PaddingValues(16. dp)
    ) {
        // Display short day of week name
        items(sortedDates.keys.toList()) { dayOfWeek ->
                Text(text = dayOfWeek.name.substring(0, 3), textAlign = TextAlign.Center)
        }
        itemsIndexed(sortedDates.values.toList()) { _, date ->
            Column(
                modifier = Modifier.padding(4. dp)
            ) {
                date.forEach { day ->
                        DateView(date = day.dayOfMonth.toString())
                }
            }
        }
    }
}

/**
 * Returns the dates for each day of the week in the given [year] and [month]
 * Including dates of the previous and next month if the [month] does not
 * begin on the first or last day of the week
 */
fun getDaysOfMonth(year: Int, month: Month): HashMap<DayOfWeek, MutableList<LocalDate>> {
    val weekFields = WeekFields.of(Locale.getDefault())

    val daysOfWeek = mutableSetOf<DayOfWeek>()
    daysOfWeek.add(DayOfWeek.SUNDAY)
    daysOfWeek.add(DayOfWeek.MONDAY)
    daysOfWeek.add(DayOfWeek.TUESDAY)
    daysOfWeek.add(DayOfWeek.WEDNESDAY)
    daysOfWeek.add(DayOfWeek.THURSDAY)
    daysOfWeek.add(DayOfWeek.FRIDAY)
    daysOfWeek.add(DayOfWeek.SATURDAY)

    val ym = YearMonth.of(year, month)
    val firstOfMonth = ym.atDay(1) // first day of the month
    val lastDayOfMonth = ym.atDay(ym.lengthOfMonth())

    val dayDates = hashMapOf<DayOfWeek, MutableList<LocalDate>>()

    // Get all the dates for each day of the week
    daysOfWeek.forEach { day ->
            val dates = mutableListOf<LocalDate>()
        var ld = firstOfMonth.with(TemporalAdjusters.dayOfWeekInMonth(1, day))
        do {
            dates.add(ld)
            ld = ld.plusWeeks(1)
        } while (YearMonth.from(ld).equals(ym))
        dayDates[day] = dates
    }

    // If current month does not start on a Sunday, get the last few days of the previous month
    if (firstOfMonth.dayOfWeek != weekFields.firstDayOfWeek) {
        val previousMonth = YearMonth.of(year, month.minus(1))
        var lastDateOfPrevMonth = LocalDate.of(year, previousMonth.month, previousMonth.atEndOfMonth().dayOfMonth)

        do {
            dayDates[lastDateOfPrevMonth.dayOfWeek]?.add(0, lastDateOfPrevMonth)
            lastDateOfPrevMonth = lastDateOfPrevMonth.minusDays(1)
        } while (lastDateOfPrevMonth.dayOfWeek != DayOfWeek.SATURDAY)
    }

    // If current month does not end on a saturday, get the first few days of the next month
    if (lastDayOfMonth.dayOfWeek != weekFields.firstDayOfWeek.minus(1)) {
        val nextMonth = YearMonth.of(year, month.plus(1))
        var firstDateOfNextMonth = LocalDate.of(year, nextMonth.month, 1)

        do {
            dayDates[firstDateOfNextMonth.dayOfWeek]?.add(firstDateOfNextMonth)
            firstDateOfNextMonth = firstDateOfNextMonth.plusDays(1)
        } while (firstDateOfNextMonth.dayOfWeek != DayOfWeek.SUNDAY)
    }

    return dayDates
}

上面的代码按预期工作,并显示一个从星期日开始的星期的日历。正如我所做的那样,使用从星期日开始的新枚举手动将星期几插入新的哈希图中是否有意义?还是让日期 api 处理它(如果可能)以及如何处理它更有意义?

还是我只是采取了错误的方法,而日历 API 会是更好的选择?

【问题讨论】:

  • 永远不要使用旧的日期时间类。它们完全被 JSR 310 中定义的现代 java.time 类所取代。遗留类很糟糕,在设计上存在严重缺陷。

标签: java android kotlin android-jetpack-compose java-time


【解决方案1】:

WeekFields.dayOfWeek()

您是完全正确的:要正确地为某个语言环境对一周中的日子进行排序,也就是说,根据该语言环境,一周中的第一天是一周的第一天,您需要使用适当的 @987654321 @ 目的。下面的 Java 方法只是一个简短的演示:

public static void sortAndPrintDaysOfWeek(Locale loc) {
    List<DayOfWeek> daysOfTheWeek = Arrays.asList(DayOfWeek.values());
    WeekFields wf = WeekFields.of(loc);

    daysOfTheWeek.sort(Comparator.comparingInt(dow -> dow.get(wf.dayOfWeek())));

    System.out.println(daysOfTheWeek);
}

我相信您可以将类似的比较器应用于您的 Kotlin 排序映射。试试上面的方法:

    sortAndPrintDaysOfWeek(Locale.FRANCE);

输出:

[周一、周二、周三、周四、周五、周六、周日]

    sortAndPrintDaysOfWeek(Locale.US);

[周日、周一、周二、周三、周四、周五、周六]

    sortAndPrintDaysOfWeek(Locale.forLanguageTag("ar-EG"));

[周六、周日、周一、周二、周三、周四、周五]

【讨论】:

    【解决方案2】:

    您无需定义private enum class DaysOfWeek {…}。见DayOfWeek

    How to get the first day of week for a certain Locale / Country

    DayOfWeek firstDayOfWeek = WeekFields.of( Locale.CANADA_FRENCH ).getFirstDayOfWeek();
    

    使用YearMonth 来表示年份和月份。

    ZoneId z = ZoneId.of( "America/Montreal" ) ;
    YearMonth yearMonth = YearMonth.now( z ) ;
    

    获取该月的第一天。

    LocalDate firstOfMonth = yearMonth.atDay( 1 ) ;
    

    如果需要,可以及时回溯,找到一周的第一天。使用TemporalAdjuster 实现。

    TemporalAdjuster ta = TemporalAdjusters.previousOrSame( firstDayOfWeek ) ;
    LocalDate startOfCalendar = firstOfMonth.with( ta );
    

    建立我们的日历。使用可导航的地图将每周编号收集到LocalDate 对象的列表中。

    NavigableMap< Integer , List< LocalDate > > calendar = new TreeMap<>() ;
    

    或者只使用列表列表。

    List< List < LocalDate > > calendar = new ArrayList<>() ;
    

    一周。

    LocalDate ld = startOfCalendar ; 
    List< LocalDate > week = ld.datesUntil( ld.plusWeeks(1) ).toList() ;
    

    如果使用旧版本的 Java,请将 .datesUntil 行替换为您自己的循环。

    int daysInWeek = 7;
    List < LocalDate > week = new ArrayList <>( daysInWeek );
    for ( int indexIntoWeek = 0 ; indexIntoWeek < daysInWeek ; indexIntoWeek++ )
    {
        week.add( ld.plusDays( indexIntoWeek ) );
    }
    

    将该周添加到您的地图中,然后继续,直到进入新的YearMonth

    将所有代码放在一起。

    // Populate data.
    DayOfWeek firstDayOfWeek = WeekFields.of( Locale.CANADA_FRENCH ).getFirstDayOfWeek();
    
    ZoneId z = ZoneId.of( "America/Montreal" );
    YearMonth yearMonth = YearMonth.now( z );
    LocalDate firstOfMonth = yearMonth.atDay( 1 );
    
    TemporalAdjuster ta = TemporalAdjusters.previousOrSame( firstDayOfWeek );
    LocalDate startOfCalendar = firstOfMonth.with( ta );
    
    NavigableMap < Integer, List < LocalDate > > calendar = new TreeMap <>();
    int nthWeek = 1;
    LocalDate ld = startOfCalendar;
    while ( ! YearMonth.from( ld ).isAfter( yearMonth ) )
    {
        List < LocalDate > week = ld.datesUntil( ld.plusWeeks( 1 ) ).toList();
        calendar.put( nthWeek++ , week );
        ld = ld.plusWeeks( 1 );  // Set up the next loop. Increment `ld` to the next week.
    }
    
    // Report results.
    for ( Integer weekNumber : calendar.keySet() )
    {
        DateTimeFormatter f = DateTimeFormatter.ofLocalizedDate( FormatStyle.SHORT ).withLocale( Locale.US );
        System.out.println( "weekNumber = " + weekNumber );
        for ( LocalDate localDate : calendar.get( weekNumber ) )
        {
            String output = localDate.getDayOfWeek().getDisplayName( TextStyle.FULL , Locale.US ) + " = " + localDate.format( f );
            System.out.println( output );
        }
    }
    

    运行时。

    weekNumber = 1
    Sunday = 8/29/21
    Monday = 8/30/21
    Tuesday = 8/31/21
    Wednesday = 9/1/21
    Thursday = 9/2/21
    Friday = 9/3/21
    Saturday = 9/4/21
    weekNumber = 2
    Sunday = 9/5/21
    Monday = 9/6/21
    Tuesday = 9/7/21
    Wednesday = 9/8/21
    Thursday = 9/9/21
    Friday = 9/10/21
    Saturday = 9/11/21
    weekNumber = 3
    Sunday = 9/12/21
    Monday = 9/13/21
    Tuesday = 9/14/21
    Wednesday = 9/15/21
    Thursday = 9/16/21
    Friday = 9/17/21
    Saturday = 9/18/21
    weekNumber = 4
    Sunday = 9/19/21
    Monday = 9/20/21
    Tuesday = 9/21/21
    Wednesday = 9/22/21
    Thursday = 9/23/21
    Friday = 9/24/21
    Saturday = 9/25/21
    weekNumber = 5
    Sunday = 9/26/21
    Monday = 9/27/21
    Tuesday = 9/28/21
    Wednesday = 9/29/21
    Thursday = 9/30/21
    Friday = 10/1/21
    Saturday = 10/2/21
    

    【讨论】:

    • 我没有看到datesUntil 方法。它似乎是Java 9+中的一种方法
    • @user15187712 没错,它是在 Java 9 中引入的。
    • @user15187712 您可以用自己的循环替换流的使用。查看我的编辑。
    【解决方案3】:

    使用现代日期时间 API java.time 非常容易。第一步是根据您的Locale 找到一周的第一天,您可以执行以下操作:

    DayOfWeek firstDayOfWeek = WeekFields.of(locale).getFirstDayOfWeek();
    

    如果您只需要工作日列表:

    import java.time.DayOfWeek;
    import java.time.LocalDate;
    import java.time.temporal.TemporalAdjusters;
    import java.time.temporal.WeekFields;
    import java.util.List;
    import java.util.Locale;
    import java.util.stream.Collectors;
    import java.util.stream.IntStream;
    
    public class Main {
        public static void main(String[] args) {
            // Let's test it
            System.out.println(getWeekDays(Locale.UK));
            System.out.println(getWeekDays(Locale.US));
        }
    
        static List<DayOfWeek> getWeekDays(Locale locale) {
            LocalDate localDate = LocalDate.now();
    
            // First day of week
            DayOfWeek firstDayOfWeek = WeekFields.of(locale).getFirstDayOfWeek();
    
            LocalDate date = localDate.with(TemporalAdjusters.dayOfWeekInMonth(0, firstDayOfWeek));
            return IntStream.rangeClosed(0, 6)
                        .mapToObj(i -> date.plusDays(i).getDayOfWeek())
                        .collect(Collectors.toList());
        }
    }
    

    输出:

    [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
    [SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]
    

    ONLINE DEMO

    如果您需要扩展它以打印该月的完整日历:

    其余的逻辑围绕着创建具有适当间距的标签,任何人都应该能够使用循环的基本知识。

    import java.time.DayOfWeek;
    import java.time.LocalDate;
    import java.time.YearMonth;
    import java.time.format.DateTimeFormatter;
    import java.time.temporal.TemporalAdjusters;
    import java.time.temporal.WeekFields;
    import java.util.Locale;
    import java.util.stream.Collectors;
    import java.util.stream.IntStream;
    
    public class Main {
        public static void main(String[] args) {
            // Let's test it
            System.out.println(getMonthCalendar(Locale.UK));
            System.out.println("-+-+-+-+-+-+-+-+-+-+-+-+-+-");
            System.out.println(getMonthCalendar(Locale.US));
        }
    
        static String getMonthCalendar(Locale locale) {
            LocalDate localDate = LocalDate.now();
            YearMonth ym = YearMonth.of(localDate.getYear(), localDate.getMonthValue());
            StringBuilder sb = new StringBuilder();
    
            // First day of week
            DayOfWeek firstDayOfWeek = WeekFields.of(locale).getFirstDayOfWeek();
    
            LocalDate date = localDate.with(TemporalAdjusters.dayOfWeekInMonth(0, firstDayOfWeek));
            DateTimeFormatter dtf = DateTimeFormatter.ofPattern("EEE", locale);
            sb.append(
                        IntStream.rangeClosed(0, 6)
                        .mapToObj(i -> dtf.format(date.plusDays(i)))
                        .collect(Collectors.joining(" "))
            )
            .append(System.lineSeparator());
    
            int counter = 1;
    
            // Print as many space as the difference between the day of week of 1st date of
            // the month and the first day of the week in that Locale
            int dayValue = localDate.withDayOfMonth(1).getDayOfWeek().getValue() - firstDayOfWeek.getValue();
            dayValue = dayValue < 0 ? 7 + dayValue : dayValue;
            for (int i = 0; i < dayValue; i++, counter++) {
                sb.append(String.format("%-4s", ""));
            }
    
            for (int i = 1; i <= ym.getMonth().length(ym.isLeapYear()); i++, counter++) {
                sb.append(String.format("%-4d", i));
    
                // Break the line if the value of the counter is multiple of 7
                if (counter % 7 == 0) {
                    sb.append(System.lineSeparator());
                }
            }
    
            return sb.toString();
        }
    }
    

    输出:

    Mon Tue Wed Thu Fri Sat Sun
            1   2   3   4   5   
    6   7   8   9   10  11  12  
    13  14  15  16  17  18  19  
    20  21  22  23  24  25  26  
    27  28  29  30  
    -+-+-+-+-+-+-+-+-+-+-+-+-+-
    Sun Mon Tue Wed Thu Fri Sat
                1   2   3   4   
    5   6   7   8   9   10  11  
    12  13  14  15  16  17  18  
    19  20  21  22  23  24  25  
    26  27  28  29  30  
    

    ONLINE DEMO

    检查UK CalendarUS Calendar 进行比较。

    通过 Trail: Date Time 了解有关 modern Date-Time API* 的更多信息。


    * 出于任何原因,如果您必须坚持使用 Java 6 或 Java 7,您可以使用 ThreeTen-Backport,它将大部分 java.time 功能向后移植到 Java 6 和 7 . 如果您正在为一个 Android 项目工作并且您的 Android API 级别仍然不符合 Java-8,请检查 Java 8+ APIs available through desugaringHow to use ThreeTenABP in Android Project

    【讨论】:

      【解决方案4】:

      我认为值得一试。尝试在 API 中结合使用 LocalTime 和 LocalDate。

      var dt = LocalDateTime.of(2021, 1, 1, 12, 45);// => 2021-01-01T12:45
      It is possible to easily decompose into components:
      
      var dt = LocalDateTime.of(2021, 1, 1, 12, 45);
      // => 2021-01-01T12:45
      
      var date = dt.toLocalDate();
      // => 2021-01-01
      
      var time = dt.toLocalTime();
      // => 12:45
      

      如果程序检测到错误,为了修复它,Time API 将责任划分为几个类。

      • ZoneOffset — 与 UTC/GMT 时间的偏移量,从 +14:00 到 -12:00。

      • ZoneRules — 更改单个时区偏移量的规则(例如,夏令时、历史更改)。 ZoneId 是时区的标识符,例如 Europe/Berlin。 有两种不同类型的时区可用。

      • ZonedDateTime 绑定到特定的 ZoneId。 OffsetDateTime / OffsetTime — 带有偏移量的日期/时间,但与特定时区无关。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-12-20
        • 2014-07-03
        • 2018-07-25
        • 2017-11-21
        • 1970-01-01
        • 1970-01-01
        • 2019-04-22
        • 2010-10-18
        相关资源
        最近更新 更多