【问题标题】:How can I check with joda-time is it first, second, ... , or last week of a month by given date?如何使用 joda-time 检查给定日期是一个月的第一周、第二周、...还是最后一周?
【发布时间】:2016-02-04 21:59:37
【问题描述】:

例如:

  • 5 February 2016 - 第一周,
  • 12 February 2016- 第二周,
  • 28 February 2016- 上周

【问题讨论】:

  • 这被标记为java-time。您是否也在寻找 Java Time 解决方案? (新的 Java 8 Time API)?
  • 其实不,我正在寻找 joda-time 解决方案。只是认为它可能是适用于 Joda-Time 的 Java-Time 的 API 和解决方案之间的相似性。
  • 恐怕 Joda-Time 不支持开箱即用的月份中的一周......必须想出一个棘手的实现。它包含在 Java Time 中。
  • @Tunaki 我担心如果不编写自己的复杂调整器或字段,Java 8 (JSR-310) 甚至是不可能的,因为weekOfMonth 也定义了第零周,这可能不是什么OP想要。 java.time 中缺少相当于 weekOfWeekBasedYear() 的月份。
  • @MenoHochschild 我注意到它使用了第 0 周是的。这实际上取决于OP想要拥有的“每月一周”的定义。 ISO-8601,Java Time 有,说“第一周至少有 4 天”...

标签: java datetime calendar jodatime java-time


【解决方案1】:

嗯,您如何定义月份中的星期还不是很清楚。对于以下讨论,我假设您谈论的是从星期一开始的几周(如 ISO-8601 标准和大多数欧洲国家/地区)。

有两种可能的方法来定义如何在计算周数时处理月份的开始和结束。

由于周一的一周开始不一定与每月的第一天相同,一周可以从上个月开始,也可以属于下个月

带有字段模式符号 w 的 JDK 类 SimpleDateFormat(以及新的 JSR-310 字段 WeekFields.weekOfMonth())使用以下策略:

如果一个月的第一天是星期一到星期四,那么 相关周在本月至少有 4 天,将是 计为第 1 周,否则计为第 0 周(零)。始终是最后一个 一个月中的某天将始终使用递增的数字,即使它属于 到下个月的第一周。

与该定义相反,CLDR 日期-时间-模式规范和 ISO-8601 几乎没有提及月周上下文中的细节。然而,这些标准并没有对他们描述另一种策略的一年中的一周保持沉默。和CLDR explicitly says about week-of-month(第 8.4 节):

一年中的 8.4 周

为“一年中的周”字段计算的值范围为 1 到 53 公历(它们可能有不同的范围 日历)。一年的第 1 周是第一周至少包含 从该年起指定的最少天数。几周之间 一年的第 1 周和下一年的第 1 周有编号 依次从 2 到 52 或 53(如果需要)。例如,1 月 1 日, 1998 年是一个星期四。如果一周的第一天是星期一,并且 一周中的最少天数为 4(这些是反映 ISO 8601 的值 和许多国家标准),然后 1998 年的第 1 周从 12 月开始 1997 年 1 月 29 日,并于 1998 年 1 月 4 日结束。但是,如果 星期是星期天,然后 1998 年的第 1 周从 1998 年 1 月 4 日开始,并且 结束于 1998 年 1 月 10 日。然后是 1998 年的前三天 1997 年第 53 周。

值的计算方法与月份的一周类似。

在 2016 年 2 月 29 日应用的两种策略之间的区别是:

  • 根据 JDK 的第 5 周
  • 根据 CLDR/ISO 的第 1 周

现在我提出一个Joda-Time 的解决方案

public static void main(String[] args) throws Throwable {

    System.out.println(getWeekOfMonth(false)); // CLDR/ISO-spec
    // 1 for today=2016-02-05
    // 2 for today=2016-02-12
    // 4 for today=2016-02-28
    // 1 for today=2016-02-29

    System.out.println(getWeekOfMonth(true)); // JDK-behaviour
    // 1 for today=2016-02-05
    // 2 for today=2016-02-12
    // 4 for today=2016-02-28
    // 5 for today=2016-02-29

}

private static int getWeekOfMonth(boolean bounded) {
    int weekOfMonth;
    LocalDate today = LocalDate.now();

    LocalDate first = today.dayOfMonth().withMinimumValue();
    int dowFirst = first.getDayOfWeek();

    if (dowFirst <= DateTimeConstants.THURSDAY) {
        // we are in week 1 and go to Monday as start of week
        first = first.minusDays(dowFirst - DateTimeConstants.MONDAY);

        // first try: we determine the week of current month
        weekOfMonth = Days.daysBetween(first, today).getDays() / 7 + 1;

        if (!bounded) {
            // edge case: are we in first week of next month?
            LocalDate next = first.plusMonths(1);
            int dowNext = next.getDayOfWeek();

            if (dowNext <= DateTimeConstants.THURSDAY) {
                next = next.minusDays(dowNext - DateTimeConstants.MONDAY);
                if (!next.isAfter(today)) {
                    weekOfMonth = 1;
                }
            }
        }
    } else if (bounded) {
        weekOfMonth = 0;
    } else {
        // we are in last week of previous month so let's check the start of previous month
        LocalDate previous = first.minusMonths(1);
        int dowPrevious = previous.getDayOfWeek();

        if (dowPrevious <= DateTimeConstants.THURSDAY) {
            previous = previous.minusDays(dowPrevious - DateTimeConstants.MONDAY);
        } else {
            previous = previous.plusDays(DateTimeConstants.MONDAY - dowPrevious + 7);
        }

        weekOfMonth = Days.daysBetween(previous, today).getDays() / 7 + 1;
    }

    return weekOfMonth;
}

希望对你来说不要太复杂。


顺便说一下,如果您有兴趣在早于 Java-8 的平台上适用的简单替代方案是什么样的:

Time4J(我的图书馆)

private static int time4j(boolean bounded) { // supports both definitions
  PlainDate today = SystemClock.inLocalView().today(); // using system timezone
    return today.get(
          (bounded ? Weekmodel.ISO.boundedWeekOfMonth() : Weekmodel.ISO.weekOfMonth()));
}

Threeten-BP (backport of Java-8):

private static int threeten() { // only JDK-definition (code similar to Java-8)
    org.threeten.bp.LocalDate today = org.threeten.bp.LocalDate.now();
    return today.get(WeekFields.ISO.weekOfMonth());
}

Old JDK:

private static int oldJDK() { // only JDK-definition
    GregorianCalendar gcal = new GregorianCalendar();
    gcal.setMinimalDaysInFirstWeek(4);
    gcal.setFirstDayOfWeek(Calendar.MONDAY);
    return gcal.get(Calendar.WEEK_OF_MONTH);
}

如您所见,使用这些替代方案很容易将基础周模型更改为非 ISO 案例(如美国周)。如果您希望在 Joda-Time 中这样做,那么我将任务留给您重写所提供的 Joda-solution。

由于以下关于该主题的评论而更新:

所以整个事情都是关于每月的星期几。 Joda-Time 也不支持开箱即用的这个元素/字段。对不起。但是您也许可以研究 necessary algorithm 以了解其他库中使用的此类字段。

Time4J 中的一个演示示例,用于对评论中提到的 rfc2445 规则进行建模:

    PlainDate dtStart = PlainDate.of(2016, Month.FEBRUARY, 4);
    int count = 5;
    Weekday byday = Weekday.FRIDAY; // first
    int interval = 1;
    CalendarUnit frequency = CalendarUnit.MONTHS;

    List<PlainDate> sequence = new ArrayList<>(count);
    PlainDate wim = dtStart;
    for (int i = 0; i < count; i++) {
        wim = wim.with(PlainDate.WEEKDAY_IN_MONTH.setToFirst(byday));
        sequence.add(wim);
        wim = wim.with(PlainDate.DAY_OF_MONTH, 1).plus(interval, frequency);
    }
    if (!sequence.isEmpty() && !sequence.get(0).equals(dtStart)) {
        sequence.remove(0); // Not quite sure - do you need another condition?
    }
    System.out.println(sequence); // [2016-03-04, 2016-04-01, 2016-05-06, 2016-06-03]

Java-8 中,还通过 specialized adjuster 提供支持,因此您可以使用 java.time.LocalDate 轻松地将给定的演示示例转移到 Java-8。

【讨论】:

  • 感谢您的详细解答。实际上,这个答案是我之前的答案的续集:stackoverflow.com/questions/35197441/… 如果不在 RRULE 中,我需要排除 DTSTART 定义的第一天。在 rfc-2445 中,我可以通过 1MO,2MO,-1MO 将重复规则定义为 1st monday2nd mondaylast monday。要知道 DTSTART 是否在这个星期一中并保留它,或者如果不删除它,我应该知道 DTSTART 是哪一天和哪一周。
  • @Anatoly 嗯,你还在谈论每月的星期,还是你的意思是每月的星期几?这不太一样。你的重复规则听起来很像SimpleDateFormat-模式符号F,或者特殊的JSR-310-adjuster,或者在Time4J中这个interface.。对于 Joda-Time,也没有这样的等价物,因此需要另一种解决方法。
  • 我将举一个简短的例子。我有以下 RRULE:FREQ=MONTHLY;COUNT=5;BYDAY=1FR;INTERVAL=1; 其中 DTSTART 是 2016 年 2 月 4 日,即每个月,每个月的第一个星期五应该选择 5 次,日期分别为:2016 年 2 月 5 日、2016 年 3 月 4 日、2016 年 1 月 1 日、2016 年 6 月 6 日、2016 年 3 月 6 日。但用户选择在 2016 年 2 月 4 日开始这个序列,这不是第一个星期五而是第一个星期四。我需要检查哪个并将其保留或删除。明天我会实施你的例子,看看什么更适合我,谢谢。唯一的问题是关于你图书馆的 LPGL 许可证,不知道我是否可以使用它。
  • 哦...我很抱歉,但我真的需要DAY_OF_WEEK_IN_MONTH,如下所述:docs.oracle.com/javase/7/docs/api/java/util/… 我很惭愧,但我错误地提出了我的问题。无论如何,您对我的问题的回答是正确的,至少我希望它会帮助某人
  • @Anatoly 感谢您的澄清。关于 LGPL,我只是担心任何用户可能拥有过时的版本并要求您在您的应用程序中嵌入更新版本的 Time4J。当然,这是开源和免费的源代码。
【解决方案2】:

在 jdk 8 中,您可以实现自己的 TemporalAdjusters 并与 LocalDate.with() 一起使用

【讨论】:

    猜你喜欢
    • 2021-01-11
    • 1970-01-01
    • 2021-09-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多