Java 挑战(五)
六、日期处理
在 Java 8 中,JDK 扩展了一些数据处理功能。首先,存在相当不直观的机器时间,它是线性进行的,由类java.time.Instant表示。但是,各种类更适合人类的思维方式。例如,包java.time中的类LocalDate、LocalTime和LocalDateTime以日期、时间及其组合的形式表示没有时区的日期值。
6.1 导言
接下来,在继续练习之前,我将在java.time下描述不同包中的一些枚举、类和接口。
6.1.1 星期和月份的计数
使用枚举java.time.DayOfWeek和java.time.Month提供了良好的可读性并避免了错误,因为您可以使用类型安全的常量来代替幻数。此外,可以使用这些枚举类型进行计算。我将在MonthAndDayOfTheWeekExample中演示这一点,如下所示,通过调用plus(),给一个星期天增加 5 天,给二月增加 13 个月:
public static void main(final String[] args)
{
final DayOfWeek sunday = DayOfWeek.SUNDAY;
final Month february = Month.FEBRUARY;
System.out.println(sunday.plus(5));
System.out.println(february.plus(13));
}
正如所料,如果你执行这个程序,你会在星期五或三月结束:
FRIDAY
MARCH
6 . 1 . 2 local date、LocalTime 和 LocalDateTime 类
如前所述,以毫秒为单位表示时间信息,有助于计算机处理,但与人类的思维方式和他们在时间系统中的取向关系不大。人类更喜欢用时间段或循环日期来思考,例如,12/24 代表平安夜,12/31 代表新年前夜,等等。(即没有时间和年份的日期)。有时你需要不完整的时间信息,比如不涉及日期的时间,比如下班后 6 点,或者作为组合,比如周二和周四晚上 7 点的空手道训练。 1 用 Java 8 之前就有的 API 表达类似的东西是相当困难的。现在让我们来看看自 JDK 8 以来的可能性。
类LocalDate表示一条日期信息,只包含年、月和日,没有时间信息。类别LocalTime模拟没有日期信息的时间(例如 6:00)。LocalDateTime是两者的结合。LocalDateAndTimeExample程序展示了这些类的用法,以及如何简单而有意义地实现日期算法。您会看到一个工作日的查询以及一个月或一年中某一天的查询,它们在每种情况下都有明确的方法名称:
public static void main(final String[] args)
{
final LocalDate michasBirthday = LocalDate.of(1971, Month.FEBRUARY, 7);
final LocalDate barbarasBirthday = michasBirthday.plusYears(2).
plusMonths(1).
plusDays(17);
final LocalDate lastDayInFebruary = michasBirthday.with(TemporalAdjusters. lastDayOfMonth());
System.out.println("michasBirthday: " + michasBirthday);
System.out.println("barbarasBirthday: " + barbarasBirthday);
System.out.println("lastDayInFebruary: " + lastDayInFebruary);
final LocalTime atTen = LocalTime.of(10,00,00);
final LocalTime tenFifteen = atTen.plusMinutes(15);
final LocalTime breakfastTime = tenFifteen.minusHours(2);
System.out.println("\natTen: " + atTen);
System.out.println("tenFifteen: " + tenFifteen);
System.out.println("breakfastTime: " + breakfastTime);
System.out.println("\nDay Of Week: " + michasBirthday.getDayOfWeek());
System.out.println("Day Of Month: " + michasBirthday.getDayOfMonth());
System.out.println("Day Of Year: " + michasBirthday.getDayOfYear());
}
代码显示了使用plusXyz()和minusXyz()方法的几个计算。您还可以使用实用程序类TemporalAdjusters,其中定义了各种实用程序方法。这就是例如lastDayOfMonth()确定一个月最后一天的方法,这里计算的是 1971 年 2 月的最后一天。执行该程序会产生以下输出:
michasBirthday: 1971-02-07
barbarasBirthday: 1973-03-24
lastDayInFebruary: 1971-02-28
atTen: 10:00
tenFifteen: 10:15
breakfastTime: 08:15
Day Of Week: SUNDAY
Day Of Month: 7
Day Of Year: 38
Java 9 中 LocalDate 类的扩展
在 JDK 9 中,类LocalDate得到了一个名为datesUntil()的重载方法。它在两个LocalDate实例之间创建了一个Stream<LocalDate>,并允许您随意指定步长。
DatesUntilExample用作者的生日和同年的平安夜来演示用datesUntil()方法的计算:
public static void main(final String[] args)
{
final LocalDate myBirthday = LocalDate.of(1971, Month.FEBRUARY, 7);
final LocalDate christmas = LocalDate.of(1971, Month.DECEMBER, 24);
System.out.println("Day-Stream");
final Stream<LocalDate> daysUntil = myBirthday.datesUntil(christmas);
daysUntil.skip(150).limit(4).forEach(System.out::println);
System.out.println("\n3-Month-Stream");
final Stream<LocalDate> monthsUntil =
myBirthday.datesUntil(christmas, Period.ofMonths(3));
monthsUntil.limit(3).forEach(System.out::println);
}
如果你执行这个程序,它从 2 月 7 日开始,跳到 150 天以后,也就是 7 月 7 日。然后你从一系列的日子里得到四个值:
Day-Stream 1971-07-07
1971-07-08
1971-07-09
1971-07-10
除此之外,第二个输出显示了增量的默认值,这里是三个月。从 2 月 7 日开始,除了这个日期之外,还列出了未来的两个日期:
3-Month-Stream 1971-02-07
1971-05-07
1971-08-07
6.1.3 类 ZonedDateTime
除了用于表示没有时区引用的日期和时间的类LocalDateTime之外,还有一个名为java.time. ZonedDateTime的类。它包括一个时区,计算不仅考虑了时区,还考虑了夏令时和冬令时的影响。
ZonedDateTimeExample 程序显示了使用类ZonedDateTime进行计算的几个示例,特别是还显示了年、月和日在两个变量中的变化以及不同时区的变化:
public static void main(final String[] args)
{
// determine current time as ZonedDateTime object
final ZonedDateTime someDay = ZonedDateTime.of(LocalDate.parse("2020-02-07"),
LocalTime.parse("17:30:15"),
ZoneId.of("Europe/Zurich"));
// modify the time and save it in a new object
final ZonedDateTime someDayChangedTime = someDay.withHour(11).withMinute(44);
// create new object with completely changed date
final ZonedDateTime dateAndTime = someDayChangedTime.withYear(2008).
withMonth(9).
withDayOfMonth(29);
// using a month constant and changing the time zone
final ZonedDateTime dateAndTime2 = someDayChangedTime.withYear(2008).
withMonth(Month.SEPTEMBER.getValue()).
withDayOfMonth(29).
withZoneSameInstant(ZoneId.of("GMT"));
System.out.println("someDay: " + someDay);
System.out.println("-> 11:44: " + someDayChangedTime);
System.out.println("-> 29.9.2008: " + dateAndTime);
System.out.println("-> 29.9.2008: " + dateAndTime2);
}
运行该程序最初将在 2020 年开始,但随后更改为 2008 年,这将产生以下输出。它们特别显示了夏季和冬季时间的影响,因此在 2008 年 9 月,显示了+02:00 的偏差。这是什么意思,可以通过将时区改为 GMT 来识别。
someDay: 2020-02-07T17:30:15+01:00[Europe/Zurich]
-> 11:44: 2020-02-07T11:44:15+01:00[Europe/Zurich]
-> 29.9.2008: 2008-09-29T11:44:15+02:00[Europe/Zurich]
-> 29.9.2008: 2008-09-29T09:44:15Z[GMT]
6.1.4 类区域 Id
现在让我们通过一个例子来了解时区处理。首先,基于一些文本时区标识符,通过调用ZoneId.of(String)并从每个实例中构造一个ZonedDateTime对象来确定相应的ZoneId实例。然后您调用ZoneId.getAvailableZoneIds()来检索所有可用的时区。使用 streams 和两种方法filter()和limit()你会得到三个来自欧洲的候选人:
public static void main(final String[] args)
{
final Stream<String> zoneIdNames = Stream.of("Africa/Nairobi",
"Europe/Zurich",
"America/Los_Angeles");
zoneIdNames.forEach(zoneIdName ->
{
final ZoneId zoneId = ZoneId.of(zoneIdName);
var someDay = ZonedDateTime.of(LocalDate.parse("2020-04-05"),
LocalTime.parse("17:30:15"),
zoneId);
System.out.println(zoneIdName + ": " + someDay);
});
final Set<String> allZones = ZoneId.getAvailableZoneIds();
final Predicate<String> inEurope = name -> name.startsWith("Europe/");
final List<String> threeFromEurope = allZones.stream().
filter(inEurope).limit(3).
collect(Collectors.toList());
System.out.println("\nSome timezones in europe:");
threeFromEurope.forEach(System.out::println);
}
程序ZoneIdExample产生以下输出:
Africa/Nairobi: 2020-04-05T17:30:15+03:00[Africa/Nairobi] Europe/Zurich: 2020-04-05T17:30:15+02:00[Europe/Zurich] America/Los_Angeles: 2020-04-05T17:30:15-07:00[America/Los_Angeles]
Some timezones in europe:
Europe/London
Europe/Brussels
Europe/Warsaw
课程持续时间
类java.time.Duration允许以纳秒为单位指定持续时间。类别Duration的实例可以通过调用各种方法(例如,从不同时间单位 2 的值)来构建,如下所示:
public static void main(final String[] args)
{
// creation with ofXyz() methods
final Duration durationFromNanos = Duration.ofNanos(3);
final Duration durationFromMillis = Duration.ofMillis(7);
final Duration durationFromSeconds = Duration.ofSeconds(15);
final Duration durationFromMinutes = Duration.ofMinutes(30);
final Duration durationFromHours = Duration.ofHours(45);
final Duration durationFromDays = Duration.ofDays(60);
System.out.println("From Nanos: " + durationFromNanos);
System.out.println("From Millis: " + durationFromMillis);;
System.out.println("From Seconds: " + durationFromSeconds);
System.out.println("From Minutes: " + durationFromMinutes);
System.out.println("From Hours: " + durationFromHours);
System.out.println("From Days: " + durationFromDays);
}
如果您运行这个程序DurationExample,您将得到如下所示的输出,其中特别感兴趣的是以下内容:时差显然以秒为时间单位的最小值和以小时为时间单位的最大值表示,结果是 60 天的值为 1440 小时:
From Nanos: PT0.000000003S
From Millis: PT0.007S
From Secs: PT15S
From Minutes: PT30M
From Hours: PT45H
From Days: PT1440H
当查看这个输出时,您可能会对Duration的字符串表示感到恼火。乍一看,这似乎有点不寻常。然而,它遵循 ISO 8601 标准。输出总是以缩写PT开始。 3 之后是小时(H)、分钟(M)、秒(S)的分段。如果需要,毫秒甚至纳秒都显示为十进制数。
此外,可以使用between()方法进行简单的计算,该方法从两个Instant对象的差异中计算出一个Duration。作为另一个特性,有一些with()方法可以返回一个Duration的新实例,并适当修改计时。相比之下,虽然对ofXyz()的多次级联调用是可能的,但它们导致最后的赢得,因此只有那一次是决定性的。有关这方面的更多信息,请参见下一节。
6.1.6 上课时间
与类Duration相似,类java.time.Period模拟一段时间,但是持续时间更长。例如 2 个月或 3 天。让我们构建几个Period的实例:
public static void main(final String[] args)
{
// create a Period with 1 year, 6 months and 3 days
final Period oneYear_sixMonths_ThreeDays = Period.ofYears(1).withMonths(6).
withDays(3);
// chaining of() works differently than you might expect!
// results in a Period with 3 days instead of 2 months, 1 week and 3 days
final Period twoMonths_OneWeek_ThreeDays = Period.ofMonths(2).ofWeeks(1).
ofDays(3);
final Period twoMonths_TenDays = Period.ofMonths(2).withDays(10);
final Period sevenWeeks = Period.ofWeeks(7);
final Period threeDays = Period.ofDays(3);
System.out.println("1 year 6 months ...: " + oneYear_sixMonths_ThreeDays);
System.out.println("Surprise just 3 days: " + twoMonths_OneWeek_ThreeDays);
System.out.println("2 months 10 days: " + twoMonths_TenDays);
System.out.println("sevenWeeks: " + sevenWeeks);
System.out.println("threeDays: " + threeDays);
}
如果运行这个程序,PeriodExample,输出如下:
1 year 6 months ...: P1Y6M3D
Surprise just 3 days: P3D
2 months 10 days: P2M10D
sevenWeeks: P49D
threeDays: P3D
从这个例子及其输出中,您可以了解到关于类Period的一些事情。首先,在 ISO 8601 之后还有一个有点神秘的字符串表示。这里P是开始的缩写(代表周期),然后Y代表年,M代表月,D代表日。作为一个特色,还有星期的换算:P14D代表 2 周。例如,它可以由Period.ofWeeks(2)生成。此外,还允许负偏移,例如P-2M4D。
除了输出的这些细节之外,您可以看到对ofXyz()的调用可以连续执行——但是最后调用的那个会胜出,如果您知道ofXyz()是静态方法,这是合乎逻辑的。因此,您不能以这种方式组合时间段;相反,您可以指定一个初始时间段。如果你想添加更多的时间段,你必须使用不同的withXyz()方法。这揭示了实现细节。类Period管理三个单一值,即年、月和日,但不管理周。因此没有方法withWeeks(),只有一个ofWeeks(),它在内部执行到天数的转换。
日期算法
为了完善你的知识,你会接触到更复杂的计算,比如跳到月初或者跳到未来或过去的几天甚至几个月。方便的是,日期运算的各种有用操作被捆绑在java.time.temporal包的TemporalAdjusters实用程序类中。类别LocalDate、LocalTime和LocalDateTime的例子给出了可能性的第一印象。现在你想扩展这方面的知识。
预定义的临时调节器
实用程序类TemporalAdjusters提供了一组常见的日期算术运算。一些例子如下:
-
firstDayOfMonth()、firstDayOfNextMonth()、lastDayOfMonth()计算(下个)月的第一天或最后一天。 -
firstDayOfYear()、firstDayOfNextYear()和lastDayOfYear()确定(下一年)的第一天或最后一天。 -
firstInMonth(DayOfWeek)和lastInMonth(DayOfWeek)跳转到当月一周的第一天或最后一天。 -
next(DayOfWeek)、nextOrSame(DayOfWeek)、previous(DayOfWeek)和previousOrSame(DayOfWeek)计算一周的下一天或前一天,例如下一个星期五。他们也可能会考虑你是否已经在那一天了。在这种情况下,当然不会发生调整。
更具体的预定义临时调节器
前面提到的TemporalAdjuster对于许多用例来说已经足够了。如果您需要更大的灵活性,还有两种方法:
-
dayOfWeekInMonth(int, DayOfWeek)计算一个月中一周的第 n 天。因此,例如,如果试图确定一个月的第 7 个星期二(不存在),它也会跳过月份边界。此外,负值是允许的,因此值 0 和-1 具有特殊的含义。0 确定上个月的最后一个工作日,而-1 确定本月的最后一个工作日。负值从该月的最后一个工作日开始向后移动给定的周数。 -
ofDateAdjuster(UnaryOperator<LocalDate>)创建TemporalAdjusters。使用UnaryOperator<LocalDate>描述所需的计算。这允许许多计算,例如,使用 lambdadate -> date.plusDays(5)跳跃到未来五天。
例子
为了澄清,让我们再看一个例子。在这里,您可以执行一些时间跳转到月初和月末,以及一周中的不同日子:
public static void main(final String[] args)
{
final LocalDate michasBirthday = LocalDate.of(1971, Month.FEBRUARY, 7);
var firstDayInFebruary =
michasBirthday.with(TemporalAdjusters.firstDayOfMonth());
var lastDayInFebruary =
michasBirthday.with(TemporalAdjusters.lastDayOfMonth());
var previousMonday =
michasBirthday.with(TemporalAdjusters.previous(DayOfWeek.MONDAY));
var nextFriday =
michasBirthday.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
System.out.println("michasBirthday: " + michasBirthday);
System.out.println("firstDayInFebruary: " + firstDayInFebruary);
System.out.println("lastDayInFebruary: " + lastDayInFebruary);
System.out.println("previousMonday: " + previousMonday);
System.out.println("nextFriday: " + nextFriday);
}
在代码中,您可以看到使用TemporalAdjusters的各种计算,比如调用方法lastDayOfMonth()来确定一个月中的最后一天,这里是确定 1971 年 2 月的最后一天。这些计算应用于在各自的LocalDate上调用with()。
运行TemporalAdjustersExample程序产生以下输出:
michasBirthday: 1971-02-07
firstDayInFebruary: 1971-02-01
lastDayInFebruary: 1971-02-28
previousMonday: 1971-02-01
nextFriday: 1971-02-12
示例:自己定义临时调整
事实上,预定义的TemporalAdjuster的可能性已经令人印象深刻,应该足以满足各种用例。然而,有时你可能想创建自己的变体。我们来看看例子FridayAfterMidOfMonth。在这里,从一个LocalDate开始,你要跳到月中后的星期五。为此,必须适当实现方法Temporal adjustInto(Temporal)。唯一的障碍是从通过的Temporal中获得一个LocalDate。这可以通过调用LocalDate.from()来实现。之后,您必须跳转到该月的第 15 天(或者 2 月的第 14 天),这很容易通过调用withDayOfMonth()来完成。最后,用nextOrSame(DayOfWeek)应用一个预定义的调整器。
public class FridayAfterMidOfMonth implements TemporalAdjuster
{
@Override
public Temporal adjustInto(final Temporal temporal)
{
final LocalDate startday = LocalDate.from(temporal);
final int dayOfMonth = startday.getMonth() == Month.FEBRUARY ? 14 : 15;
return startday.withDayOfMonth(dayOfMonth).
with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY));
}
}
如果使用适当的 API,实现功能是多么容易,这是非常值得注意的。
要在 JShell 中试用它,您需要以下导入:
jshell> import java.time.*
jshell> import java.time.temporal
.*
现在,您可以在那里定义上面的类并调用它,例如,如下所示:
jshell> LocalDate feb7 = LocalDate.of(2020, 2, 7) feb7 ==> 2020-02-07
jshell> var adjustedDay = feb7.with(new FridayAfterMidOfMonth()) adjustedDay ==> 2020-02-14
jshell> adjustedDay.getDayOfWeek()
$17 ==> FRIDAY
jshell> LocalDate mar24 = LocalDate.of(2020, 3, 24) mar24 ==> 2020-03-24
jshell> var adjustedDay2 = mar24.with(new FridayAfterMidOfMonth()) adjustedDay2 ==> 2020-03-20
jshell> adjustedDay2.getDayOfWeek()
$22 ==> FRIDAY
格式化和解析
类java.time.format.DateTimeFormatter对于格式化输出和解析日期值很有用。除了各种预定义的格式,您还可以提供各种自己的变体。这可以通过调用方法ofPattern()以及ofLocalizedDate()和parse()并指定格式来实现。在这里,可以方便地指定更复杂的格式化模式,甚至用它来进行解析。下面的代码演示了这一点,代码可作为FormattingAndParsingExample执行。各种其他的可能性存在,但不能在这里提出。因此,建议查看 JDK 的详细文档。
public static void main(final String[] args)
{
// definition of some special formatters
final DateTimeFormatter ddMMyyyyFormat = ofPattern("dd.MM.yyyy");
final DateTimeFormatter italiandMMMMy = ofPattern("d.MMMM y",
Locale.ITALIAN);
final DateTimeFormatter shortGerman =
DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).
withLocale(Locale.GERMAN);
// attention: the textual parts are to be enclosed in quotation marks
final String customPattern = "'Der 'dd'. Tag im 'MMMM' im Jahr 'yy'.'";
final DateTimeFormatter customFormat = ofPattern(customPattern);
System.out.println("Formatting:\n");
final LocalDate february7th = LocalDate.of(1971, 2, 7);
System.out.println("ddMMyyyyFormat: " + ddMMyyyyFormat.format(february7th));
System.out.println("italiandMMMMy: " + italiandMMMMy.format(february7th));
System.out.println("shortGerman: " + shortGerman.format(february7th));
System.out.println("customFormat: " + customFormat.format(february7th));
// Parsing date values
System.out.println("\nParsing:\n");
final LocalDate fromIsoDate = LocalDate.parse("1971-02-07");
final LocalDate fromddMMyyyyFormat = LocalDate.parse("18.03.2014",
ddMMyyyyFormat);
final LocalDate fromShortGerman = LocalDate.parse("18.03.14",
shortGerman);
final LocalDate fromCustomFormat =
LocalDate.parse("Der 31\. Tag im Dezember im Jahr 19.",
customFormat);
System.out.println("From ISO Date: " + fromIsoDate);
System.out.println("From ddMMyyyyFormat: " + fromddMMyyyyFormat);
System.out.println("From short german: " + fromShortGerman);
System.out.println("From custom format: " + fromCustomFormat);
}
该程序产生以下输出,这提供了对格式化和解析的初步理解:
Formatting:
ddMMyyyyFormat: 07.02.1971
italian_dMMMMy: 7.febbraio 1971
shortGerman: 07.02.71
customFormat: Der 07\. Tag im Februar im Jahr 71.
Parsing:
From ISO Date: 1971-02-07
From ddMMyyyyFormat: 2014-03-18
From short german: 2014-03-18
From custom format: 2019-12-31
6.2 练习
6.2.1 练习 1:闰年(★✩✩✩✩)
虽然在我们的日历中,一年通常被分为 365 天,但这在天文学上并不完全正确。一年大约有 365.25 天。因此,通过使用 366 天的闰年,几乎每 4 年就需要进行一次校正。有两个特殊方面需要考虑:
-
能被 100 整除的年份叫世俗年,不是闰年。
-
但是,同样能被 400 整除的世俗年份毕竟是闰年。
详见 https://en.wikipedia.org/wiki/Leap_year 。写方法boolean isLeap(int)—当然不用java.time.Year.isLeap()。
例题
|投入
|
规则
|
结果
| | --- | --- | --- | | One thousand nine hundred | 世俗年 | 没有闰年 | | Two thousand | 世俗的岁月,但能被 400 整除 | 闰年 | | Two thousand and twenty | 能被 4 整除 | 闰年 |
6.2.2 练习 2:基础知识数据-API (★★✩✩✩)
练习 2a:创造(★✩✩✩✩)
用LocalDate表示你的生日,用LocalTime表示下午 5:30 下班,现在用LocalDateTime表示。此外,建模时间跨度为 1 年 10 个月 20 天,持续时间为 7 小时 15 分钟。
练习 2b:持续时间(★★✩✩✩)
计算今天和你生日之间的时间长度,反之亦然。
6.2.3 练习 3:月份长度(★★✩✩✩)
练习 3a:计算(★✩✩✩✩)
使用类LocalDate的方法plusMonths()从 2012 年 2 月 2 日和 2014 年 2 月 2 日以及 2014 年 4 月 4 日和 2014 年 5 月 5 日跳转到未来一个月,并输出每种情况下的日期。
练习 3b:月份长度(★★✩✩✩)
对于计算,如果使用方法plusDays()而不是plusMonths()会发生什么?什么对应于一个月:28、29、30 或 31 天?你如何确定一个月的正确长度?
6.2.4 练习 4:时区(★★✩✩✩)
获取以America/L或Europe/S开始的所有时区,并相应地填充一个排序集。为此,写方法Set<String> selectedAmericanAndEuropeanTimeZones()。
例子
计算集应包含以下值:
[America/La_Paz, America/Lima, America/Los_Angeles, America/Louisville, America/Lower_Princes, Europe/Samara, Europe/San_Marino, Europe/Sarajevo, Europe/Saratov, Europe/Simferopol, Europe/Skopje, Europe/Sofia, Europe/Stockholm]
Tip
使用类ZoneId和它的方法getAvailableZoneIds()。利用 streams 和 filter-map-reduce 框架来查找与之前指定的前缀相匹配的时区 id。
6.2.5 练习 5:时区计算(★★✩✩✩)
如果从苏黎世到旧金山的航班需要 11 小时 50 分钟,于 2019 年 9 月 15 日下午 1 点 10 分起飞,到达旧金山的当地时间是几点?出发地点的时间是几点?
例子
应该为该航班确定以下到达时间:
2019-09-16T01:00+02:00[Europe/Zurich]
2019-09-15T16:00-07:00[America/Los_Angeles]
Tip
使用类Duration来模拟飞行时间。旧金山的时区是America/Los_Angeles。使用类ZonedDateTime中的withZoneSameInstant()方法。
6.2.6 练习 6:使用本地日期进行计算
练习 6a:13 号星期五(★★✩✩✩)
计算由两个LocalDate定义的范围内 13 号星期五的所有事件。编写通用方法List<LocalDate> allFriday13th(LocalDate, LocalDate),指定开始日期(含)和结束日期(不含)。
例子
对于 2013 年 1 月 1 日至 2015 年 12 月 31 日期间,应确定以下日期值:
|时期
|
结果
| | --- | --- | | 2013 – 2015 | [2013-09-13, 2013-12-13, 2014-06-13, 2015-02-13, 2015-03-13, 2015-11-13] |
练习 6b:13 号星期五(★★✩✩✩)的几次出现
13 号星期五在哪一年出现过几次?要回答这个问题,请计算一张地图,其中对应的星期五与每年相关联。为此编写方法Map<Integer, List<LocalDate>> friday13thGrouped(LocalDate, LocalDate)。
例子
|年
|
结果
| | --- | --- | | Two thousand and thirteen | [2013-09-13, 2013-12-13] | | Two thousand and fourteen | [2014-06-13] | | Two thousand and fifteen | [2015-02-13, 2015-03-13, 2015-11-13] |
6.2.7 练习 7:日历输出(★★★✩✩)
编写方法void printCalendar(Month, int),对于给定的月份和年份,将日历页面打印到控制台。
例子
对于 2020 年 4 月,您预计会得到以下结果:
Mon Tue Wed Thu Fri Sat Sun
.. .. 01 02 03 04 05
06 07 08 09 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 -- -- --
对于结束于周日的示例,您可以选择 2020 年 5 月。要验证在星期一开始,请使用 2020 年 6 月。
6.2.8 练习 8:工作日(★✩✩✩✩)
练习 8a:工作日(★✩✩✩✩)
平安夜 2019 年(2019 年 12 月 24 日)是星期几?2019 年 12 月的第一天和最后一天是星期几?
例子
一周中的以下几天应该是结果:
|投入
|
结果
| | --- | --- | | 2019 年 12 月 24 日 | 星期二 | | 2019 年 12 月 01 日 | 在星期日 | | 2019 年 12 月 31 日 | 星期二 |
练习 8b:日期(★✩✩✩✩)
用写法Map<String, LocalDate> firstAndLastFridayAndSunday(YearMonth)计算出 2019 年 3 月第一个和最后一个星期五和星期天各自的日期。一路上,了解用于建模年份和月份的YearMonth类。
例子
计算出的映射应包含以下值,并按键排序:
{firstFriday=2019-03-01, firstSunday=2019-03-03,
lastFriday=2019-03-29, lastSunday=2019-03-31}
练习 8c:月份或年份中的某一天(★✩✩✩✩)
在练习部分 8b 中,您确定了 2019 年 3 月的四个日期。这两种情况下都是在三月的哪一天?一年中的哪一天?
例子
|投入
|
一个月中的第几天
|
一年中的每一天
| | --- | --- | --- | | 第一个星期五=2019-03-01 | one | Sixty | | 第一个星期天=2019-03-03 | three | Sixty-two | | lastFriday=2019-03-29 | Twenty-nine | Eighty-eight | | 载荷空间=2019-03-31 | Thirty-one | Ninety |
6.2.9 练习 9:星期日和闰年(★★✩✩✩)
练习 9a:周日
在两个LocalDate给定的范围内计算星期日的数量。为此,编写方法Stream<LocalDate> allSundaysBetween(LocalDate, LocalDate),其中开始日期包含在内,结束日期不包含在内。
例子
|时期
|
结果
| | --- | --- | | 1.1.2017 – 1.1.2018 | Fifty-three | | 1.1.2019 – 7.2.2019 | five |
练习 9b:闰年
在Year实例给定的范围内计算闰年数。为此,编写方法long countLeapYears(Year, Year),其中起始年包含,结束年不包含。
例子
|时期
|
结果
| | --- | --- | | 2010 – 2019 | Two | | 2000 – 2019 | five |
6.2.10 练习 10:临时助理(★★★✩✩)
编写一个TemporalAdjuster,将日期值移动到每个季度的开始,比如从 2 月 7 日到 1 月 1 日。
例子
|投入
|
结果
| | --- | --- | | LocalDate.of(2014,3,15) | 2014-01-01 | | LocalDate.of(2014 年 6 月 15 日) | 2014-04-01 | | LocalDate.of(2014,9,15) | 2014-07-01 | | LocalDate.of(2014,11,15) | 2014-10-01 |
Tip
在Month和IsoFields类中四处看看。
6.2.11 练习 11:第 n 周工作日调整(★★★✩✩)
编写跳转到一周第 n 天的类NthWeekdayAdjuster,比如第三个星期五。这要从 a LocalDate给的月初说起。对于较大的 *n,*值,它应该跳到随后的月份。
例子
使用以下开始日期为 2015 年 8 月 15 日的时间跳转来验证此课:
|开始日期
|
跳跃目标
|
结果
| | --- | --- | --- | | 2015-08-15 (2015-08-15) | 第二个星期五 | 2015-08-14 | | 2015-08-15 (2015-08-15) | 第三个星期天 | 2015-08-16 | | 2015-08-15 (2015-08-15) | 第四个星期二 | 2015-08-25 |
6.2.12 练习 12:发薪日临时助理(★★★✩✩)
实现计算瑞士典型发薪日的类Ex12_NextPaydayAdjuster。这通常是每月的 25 号。如果这一天适逢周末,前一个星期五作为发薪日。如果你在付款日之后,那么这被认为是在下个月。作为一个自由式,你还是要对 12 月份的工资发放实行一个特殊的规则,这个时候发放应该在月中。如有必要,如果发薪日是周末,付款将被移到下一个星期一。
例子
基于以下日期值验证此类:
|投入
|
结果
|
规则
| | --- | --- | --- | | 2019-07-21 | 2019-07-25 | 正常调整 | | 2019-06-27 | 2019-07-25 | 正常调整,下个月 | | 2019-08-21 | 2019-08-23 | 星期五,如果 25 号是周末 | | 2019-12-06 | 2019-12-16 | 十二月:月中,周末后的星期一 | | 2019-12-23 | 2020-01-24 | 下个月和星期五,如果 25 号是周末 |
练习 13:格式化和解析(★★✩✩✩)
创建一个LocalDateTime对象,并以各种格式打印。格式化并解析这些格式:dd.MM.yyy HH、dd.MM.yy HH:mm、ISO_LOCAL_DATE_TIME、SHORT 和美国地区。
例子
对于 2017 年 7 月 27 日 13 时 14 分 15 秒,输出应如下所示:
|格式化
|
从语法上分析
|
格式
| | --- | --- | --- | | 27 07 2017 13 | 2017-07-27T13:00 | dd MM yyyy HH | | 27.07.17 13:14 | 2017-07-27T13:14 | dd。嗯。yy 时:分 | | 2017-07-27T13:14:15 | 2017-07-27T13:14:15 | ISO_LOCAL_DATE_TIME | | 2017 年 7 月 27 日下午 1 点 14 分 | 2017-07-27T13:14 | 短+区域设置。美国 |
Tip
使用DateTimeFormatter类和它的常量以及它的方法,比如ofPattern()和ofLocalizedDateTime()。
练习 14:容错解析(★★✩✩✩)
评估用户输入时,容错通常很重要。您的任务是创建方法Optional<LocalDate> faultTolerantParse(String, Set<DateTimeFormatter>),它允许您解析以下日期格式:dd.MM.yy、dd.MM.yyy、MM/dd/yyyy和yyyy-MM-dd。
例子
下表显示了解析不同输入的预期结果。请务必注意,对于模式中的两位数年份,有时它不会生成正确或预期的日期!
|投入
|
结果
| | --- | --- | | “07.02.71” | 2071-02-07 | | “07.02.1971” | 1971-02-07 | | “02/07/1971” | 1971-02-07 | | “1971-02-07” | 1971-02-07 |
6.3 解决方案
6.3.1 解决方案 1:闰年(★✩✩✩✩)
虽然在我们的日历中,一年通常被分为 365 天,但这在天文学上并不完全正确。一年大约有 365.25 天。因此,通过使用 366 天的闰年,几乎每 4 年就需要进行一次校正。有两个特殊方面需要考虑:
-
能被 100 整除的年份叫世俗年,不是闰年。
-
但是,同样能被 400 整除的世俗年份毕竟是闰年。
详见 https://en.wikipedia.org/wiki/Leap_year 。写方法boolean isLeap(int)——当然不用java.time.Year.isLeap()。
例子
|投入
|
规则
|
结果
| | --- | --- | --- | | One thousand nine hundred | 世俗年 | 没有闰年 | | Two thousand | 世俗的岁月,但能被 400 整除 | 闰年 | | Two thousand and twenty | 能被 4 整除 | 闰年 |
算法用模运算计算每个条件,并适当地组合它们:
static boolean isLeap(final int year)
{
final boolean everyFourthYear = year % 4 == 0;
final boolean isSecular = year % 100 == 0;
final boolean isSecularSpecial = year % 400 == 0;
return everyFourthYear && (!isSecular || isSecularSpecial)
}
如前所述,Year类已经有了一个预定义的闰年检查:
static boolean isLeap_Jdk8(final int year)
{
return Year.of(year).isLeap();
}
确认
让我们把它作为一个单元测试来尝试一下:
@ParameterizedTest(name = "isLeap({0} => {2}, Hinweis: {1}")
@CsvSource({ "1900, Secular, false",
"2000, Secular (but rule of 400), true",
"2020, Every 4th year, true" })
void testIsLeap(int year, String hint, boolean expected)
{
boolean result = Ex01_LeapYear.isLeap(year);
assertEquals(expected, result);
}
6.3.2 解决方案 2:基础知识日期-API (★★✩✩✩)
解决方案 2a:创造(★✩✩✩✩)
用LocalDate表示你的生日,用LocalTime表示下午 5:30 下班,现在用LocalDateTime表示。此外,建模时间跨度为 1 年 10 个月 20 天,持续时间为 7 小时 15 分钟。
算法适当使用 API:
final LocalDate myBirthday = LocalDate.of(1971, 2, 7);
final LocalTime time = LocalTime.of(17, 30);
final LocalDateTime now = LocalDateTime.now();
final Period oneYear10Month20Days = Period.of(1, 10, 20);
final Duration sevenHours15Minutes = Duration.ofHours(7).plusMinutes(15);
解决方案 2b:持续时间(★★✩✩✩)
计算今天和你生日之间的时间长度,反之亦然。
算法适当使用 API,尤其是until()和between()方法:
final LocalDate now = LocalDate.now();
final LocalDate birthday = LocalDate.of(1971, 2, 7);
System.out.println("Using until()");
System.out.println("now -> birthday: " + now.until(birthday)); System.out.println("birthday -> now: " + birthday.until(now)); System.out.println("\nUsing Period.between()");
System.out.println("now -> birthday: " + Period.between(now, birthday));
System.out.println("birthday -> now: " + Period.between(birthday, now));
6.3.3 解决方案 3:月份长度(★★✩✩✩)
解决方案 3a:计算(★✩✩✩✩)
使用类LocalDate的方法plusMonths()从 2012 年 2 月 2 日和 2014 年 2 月 2 日以及 2014 年 4 月 4 日和 2014 年 5 月 5 日跳转到未来一个月,并输出每种情况下的日期。
算法适当调用上述方法:
final LocalDate february_2_2012 = LocalDate.of(2012, 2, 2);
final LocalDate february_2_2014 = LocalDate.of(2014, 2, 2);
final LocalDate april_4_2014 = LocalDate.of(2014, 4, 4);
final LocalDate may_5_2014 = LocalDate.of(2014, 5, 5);
System.out.println("2/2/2012 + 1 month = " + february_2_2012.plusMonths(1));
System.out.println("2/2/2014 + 1 month = " + february_2_2014.plusMonths(1));
System.out.println("4/4/2014 + 1 month = " + april_4_2014.plusMonths(1));
System.out.println("5/5/2014 + 1 month = " + may_5_2014.plusMonths(1));
解决方案 3b:月份长度(★★✩✩✩)
对于计算,如果不使用plusMonths()而是使用方法plusDays()?会发生什么呢?对应于一个月的是:28、29、30 还是 31 天?你如何确定一个月的正确长度?
算法让我们为不同的值调用plusDays():
System.out.println("2/2/2012 + 28 days = " + february_2_2012.plusDays(28));
System.out.println("2/2/2014 + 28 days = " + february_2_2014.plusDays(28));
System.out.println("4/4/2014 + 30 days = " + april_4_2014.plusDays(30));
System.out.println("5/5/2014 + 31 days = " + may_5_2014.plusDays(31));
确认
使用plusMonths(),总是会添加相应月份的正确长度(也考虑到潜在的闰年)。但是,这不适用于plusDays()方法。这将添加指定的天数,强制您始终正确传递该值。为此,Month类提供了length(boolean)方法。或者,还有方法LocalDate.lengthOfMonth()。让我们来看看程序输出:
2/2/2012 + 1 month = 2012-03-02
2/2/2014 + 1 month = 2014-03-02
4/4/2014 + 1 month = 2014-05-04
5/5/2014 + 1 month = 2014-06-05
2/2/2012 + 28 days = 2012-03-01
2/2/2014 + 28 days = 2014-03-02
4/4/2014 + 30 days = 2014-05-04
5/5/2014 + 31 days = 2014-06-05
6.3.4 解决方案 4:时区(★★✩✩✩)
获取以America/L或Europe/S开始的所有时区,并相应地填充一个排序集。为此写法Set<String> selectedAmericanAndEuropeanTimeZones()。
例子
计算集应包含以下值:
[America/La_Paz, America/Lima, America/Los_Angeles, America/Louisville, America/Lower_Princes, Europe/Samara, Europe/San_Marino, Europe/Sarajevo, Europe/Saratov, Europe/Simferopol, Europe/Skopje, Europe/Sofia, Europe/Stockholm]
Tip
使用类ZoneId和它的方法getAvailableZoneIds()。利用 streams 和 filter-map-reduce 框架来查找与之前指定的前缀相匹配的时区 id。
算法首先,你确定所有时区 id。然后定义两个过滤条件,最后是它们的组合。这允许您使用流 API 的基本功能:
static Set<String> selectedAmericanAndEuropeanTimeZones()
{
final Set<String> allZones = ZoneId.getAvailableZoneIds();
final Predicate<String> inEuropeS = name -> name.startsWith("Europe/S");
final Predicate<String> inAmericaL = name -> name.startsWith("America/L");
final Predicate<String> europeOrAmerica = inEuropeS.or(inAmericaL);
return allZones.stream().
filter(europeOrAmerica).
collect(Collectors.toCollection(TreeSet::new));
}
确认
若要进行验证,请运行以下单元测试:
@Test
public void selectedAmericanAndEuropeanTimeZones()
{
var expected = Set.of("America/La_Paz", "America/Lima",
"America/Los_Angeles", "America/Louisville",
"America/Lower_Princes", "Europe/Samara",
"Europe/San_Marino", "Europe/Sarajevo",
"Europe/Saratov", "Europe/Simferopol",
"Europe/Skopje", "Europe/Sofia",
"Europe/Stockholm");
Set<String> result = Ex04_ZoneIds.selectedAmericanAndEuropeanTimeZones();
assertEquals(expected, result);
}
6.3.5 解决方案 5:时区计算(★★✩✩✩)
如果从苏黎世到旧金山的航班需要 11 小时 50 分钟,于 2019 年 9 月 15 日下午 1 点 10 分起飞,到达旧金山的当地时间是几点?出发地点的时间是几点?
例子
应该为该航班确定以下到达时间:
2019-09-16T01:00+02:00[Europe/Zurich]
2019-09-15T16:00-07:00[America/Los_Angeles]
Tip
使用类Duration来模拟飞行时间。旧金山的时区是America/Los_Angeles。使用类ZonedDateTime中的withZoneSameInstant()方法。
算法首先,构建出发日期和时间以及时区的对象,以创建相应的ZonedDateTime对象。然后你用一个Duration来定义飞行时间。简单地加上这个时差,你就可以得到到达时间,就像在欧洲一样。使用withZoneSameInstant()和时区America/Los_Angeles,您最终获得与美国时区相关的到达时间:
public static void main(final String[] args)
{
final LocalDate departureDate = LocalDate.of(2019, 9, 15);
final LocalTime departureTime = LocalTime.of(13, 10);
final ZoneId zoneEurope = ZoneId.of("Europe/Zurich");
// departure time
final ZonedDateTime departure = ZonedDateTime.of(departureDate,
departureTime, zoneEurope);
// flight duration
final Duration flightDuration = Duration.ofHours(11).plusMinutes(50);
// arrival time based on flight duration (and time zone)
final ZonedDateTime arrival1 = departure.plus(flightDuration);
final ZoneId zoneAmerica = ZoneId.of("America/Los_Angeles");
final ZonedDateTime arrival2 = arrival1.withZoneSameInstant(zoneAmerica);
System.out.println(arrival1); System.out.println(arrival2);
}
确认
让我们通过一个简单的程序运行来检查计算结果——作者又记起了那次飞行:-)
2019-09-16T01:00+02:00[Europe/Zurich]
2019-09-15T16:00-07:00[America/Los_Angeles]
6.3.6 解决方案 6:使用本地日期进行计算
练习 6a:13 号星期五(★★✩✩✩)
计算由两个LocalDate定义的范围内 13 号星期五的所有事件。编写通用方法List<LocalDate> allFriday13th(LocalDate, LocalDate),指定开始日期(含)和结束日期(不含)。
例子
对于 2013 年 1 月 1 日至 2015 年 12 月 31 日(含)这段时间,应确定以下日期值:
|时期
|
结果
| | --- | --- | | 2013 – 2015 | [2013-09-13, 2013-12-13, 2014-06-13, 2015-02-13, 2015-03-13, 2015-11-13] |
算法在这个练习中,您使用两个谓词来测试星期五和十三号。为了提供要测试的相应日期,Java 9 中引入的datesUntil()方法可以帮助您。然后使用流 API 中的标准方法执行过滤。
static List<LocalDate> allFriday13th(final LocalDate start,
final LocalDate end)
{
final Predicate<LocalDate> isFriday = day -> day.getDayOfWeek() ==
DayOfWeek.FRIDAY;
final Predicate<LocalDate> is13th = day -> day.getDayOfMonth() == 13;
final List<LocalDate> allFriday13th = start.datesUntil(end).
filter(isFriday).
filter(is13th).
collect(Collectors.toList());
return allFriday13th;
}
或者,您可以从星期五开始,然后以 7 天为周期调用datesUntil(end, period)。这可能会更有效率一点。
让我们在 JShell 或一个main()方法中使用适当的日期值执行上面的行,如下所示:
var allFriday13th = allFriday13th(LocalDate.of(2013, 1, 1),
LocalDate.of(2016, 1, 1));
System.out.println("allFriday13th: " + allFriday13th);
这提供了以下输出(这里的格式更好一点):
allFriday13th: [2013-09-13, 2013-12-13, 2014-06-13, 2015-02-13, 2015-03-13,
2015-11-13]
解决方案 6b:13 号星期五(★★✩✩✩)的多次出现
13 号星期五在哪一年出现过几次?要回答这个问题,请计算一张地图,其中对应的星期五与每年相关联。为此编写方法Map<Integer, List<LocalDate>> friday13thGrouped(LocalDate, LocalDate)。
例子
|年
|
结果
| | --- | --- | | Two thousand and thirteen | [2013-09-13, 2013-12-13] | | Two thousand and fourteen | [2014-06-13] | | Two thousand and fifteen | [2015-02-13, 2015-03-13, 2015-11-13] |
算法基于之前定义的方法,您可以再次使用 Stream API 通过groupingBy()方法按年份进行分组。最后,您希望对年份进行排序,这就是为什么您要将结果传输到一个TreeMap<K,V>中。
static Map<Integer, List<LocalDate>> friday13thGrouped(final LocalDate start,
final LocalDate end)
{
return new TreeMap<>(allFriday13th(start, end).stream().
collect(Collectors.groupingBy(LocalDate::getYear)));
}
确认
让我们快速看一下关联的单元测试可能是什么样子的:
@Test
void testAllFriday13th()
{
final LocalDate start = LocalDate.of(2013, 1, 1);
final LocalDate end = LocalDate.of(2016, 1, 1);
var result = Ex06_Friday13thExample.allFriday13th(start, end);
// trick: Use Stream.of() and map() to have less typing work
var expected = Stream.of("2013-09-13", "2013-12-13", "2014-06-13",
"2015-02-13", "2015-03-13", "2015-11-13").
map(str -> LocalDate.parse(str)).
collect(Collectors.toList());
assertEquals(expected, result);
}
您可以按如下方式测试分组:
@Test
void testFriday13thGrouped()
{
final LocalDate start = LocalDate.of(2013, 1, 1);
final LocalDate end = LocalDate.of(2016, 1, 1);
var result = Ex06_Friday13thExample.friday13thGrouped(start, end);
var expected = Map.of(2013, List.of(LocalDate.parse("2013-09-13"),
LocalDate.parse("2013-12-13")),
2014, List.of(LocalDate.parse("2014-06-13")),
2015, List.of(LocalDate.parse("2015-02-13"),
LocalDate.parse("2015-03-13"),
LocalDate.parse("2015-11-13")));
assertEquals(expected, result);
}
6.3.7 解决方案 7:日历输出(★★★✩✩)
编写方法void printCalendar(Month, int),对于给定的月份和年份,将日历页面打印到控制台。
例子
对于 2020 年 4 月,您预计会得到以下结果:
Mon Tue Wed Thu Fri Sat Sun
.. .. 01 02 03 04 05
06 07 08 09 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 -- -- --
对于结束于周日的示例,您可以选择 2020 年 5 月。要验证在星期一开始,请使用 2020 年 6 月。
**算法的初步考虑:**从 2020 年 4 月的日历页面,您已经可以看到一些等待您的特殊方面和挑战:
-
第一周和最后一周可能不完整
-
星期天总是有换行。
-
你如何把工作日和天数联系起来?
算法在开始时,你显示一个带有工作日的标题行。之后,您确定一个月开始的星期几,然后如果必要的话用skipTillFirstDayOfMonth()跳过一些星期几。要输出各个日子,您需要使用length()根据闰年信息指定月份长度。接下来是有一些技巧的部分。您遍历所有的日期值,并以格式化的方式打印数字。当你到了一个星期天,你继续下一行。要移动星期几,可以使用助手方法calcNextWeekDay()。最后,所有不再属于该月的日子都应该标上--。这就是fillFromMonthEndToSunday()法的任务。
static void printCalendar(final Month month, final int year)
{
System.out.println("Mon Tue Wed Thu Fri Sat Sun");
LocalDate cur = LocalDate.of(year, month, 1);
DayOfWeek firstInMonth = cur.getDayOfWeek();
skipTillFirstDayOfMonth(firstInMonth);
DayOfWeek currentWeekDay = firstInMonth;
int lengthOfMonth = month.length(Year.of(year).isLeap());
for (int i = 1; i <= lengthOfMonth; i++)
{
System.out.print(String.format("%03d", i) + " ");
if (currentWeekDay == DayOfWeek.SUNDAY) System.out.println();
currentWeekDay = nextWeekDay(currentWeekDay);
}
fillFromMonthEndToSunday(currentWeekDay);
// last day not Sunday, then pagination
if (currentWeekDay != DayOfWeek.MONDAY) System.out.println();
}
现在我们来看 helper 方法,它将一周中的某一天移动到下一天,并从周日循环到周一。这可以基于枚举DayOfWeek和模运算来实现。使用自动实现这种特殊处理的方法plus()甚至更容易。
static DayOfWeek calcNextWeekDay(final DayOfWeek nextWeekDay)
{
return nextWeekDay.plus(1);
}
打印不属于一个月的一周的第一天和最后一天的方法保持不变。对于前者,您使用所示的帮助器方法,从星期一进行到该月第一天的工作日:
static void skipTillFirstDayOfMonth(final DayOfWeek firstInMonth)
{
DayOfWeek currentWeekDay = DayOfWeek.MONDAY;
while (currentWeekDay != firstInMonth)
{
System.out.print(".. ");
currentWeekDay = nextWeekDay(currentWeekDay);
}
}
最后,为了完成日历的输出,从一个月的最后一天开始,用--填充直到到达一个星期天:
static void fillFromMonthEndToSunday(final DayOfWeek currentWeekDay)
{
DayOfWeek nextWeekDay = currentWeekDay;
while (nextWeekDay != DayOfWeek.MONDAY)
{
System.out.print("-- ");
nextWeekDay = nextWeekDay(nextWeekDay);
}
}
Hint: Helper Methods and Proper Structuring
正如多次演示的那样,创建适当的助手方法简化了问题的解决(通常是显著的)。这允许您在算法的实现中更多地停留在逻辑级别,并避免因实现细节而分心。
确认
将方法复制到 JShell 中,并记住导入import java.time.*。之后,您只需浏览三个示例性案例一次:
jshell> printCalendar(Month.APRIL, 2020)
Mon Tue Wed Thu Fri Sat Sun
.. .. 01 02 03 04 05
06 07 08 09 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 -- -- --
jshell> printCalendar(Month.MAY, 2020)
Mon Tue Wed Thu Fri Sat Sun
.. .. .. .. 01 02 03
04 05 06 07 08 09 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
jshell> printCalendar(Month.JUNE, 2020)
Mon Tue Wed Thu Fri Sat Sun
01 02 03 04 05 06 07
08 09 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 -- -- -- -- --
6.3.8 解决方案 8:工作日(★✩✩✩✩)
解决方案 8a:工作日(★✩✩✩✩)
平安夜 2019 年(2019 年 12 月 24 日)是星期几?2019 年 12 月的第一天和最后一天是星期几?
例子
一周中的以下几天应该是结果:
|投入
|
结果
| | --- | --- | | 2019 年 12 月 24 日 | 星期二 | | 2019 年 12 月 01 日 | 在星期日 | | 2019 年 12 月 31 日 | 星期二 |
算法你创建一个LocalDate对象并使用方法getDayOfWeek()或DayOfWeek.from()来确定相应的工作日。在使用匹配的TemporalAdjuster跳转到 12 月的第一天或最后一天之后,重复这个操作。
LocalDate christmasEve = LocalDate.of(2019, 12, 24); System.out.println("Dec. 24, 2019 = " + christmasEve.getDayOfWeek());
System.out.println("Dec. 24, 2019 = " + DayOfWeek.from(christmasEve));
var decemberFirst = christmasEve.with(TemporalAdjusters.firstDayOfMonth()); System.out.println("Dec. 01, 2019 = " + ecemberFirst.getDayOfWeek());
var decemberLast = christmasEve.with(TemporalAdjusters.lastDayOfMonth()); System.out.println("Dec. 31, 2019 = " + decemberLast.getDayOfWeek());
确认
上述行输出以下内容:
Dec. 24, 2019 = TUESDAY
Dec. 24, 2019 = TUESDAY
Dec. 01, 2019 = SUNDAY
Dec. 31, 2019 = TUESDAY
请不要忘记在 JShell 中执行以下导入操作:
jshell> import java.time.temporal.*
解决办法 8b:日期
用写法Map<String, LocalDate> firstAndLastFridayAndSunday(YearMonth)计算出 2019 年 3 月第一个和最后一个星期五和星期天各自的日期。一路上,了解用于建模年份和月份的YearMonth类。
例子
计算出的映射应包含以下值,并按键排序:
{firstFriday=2019-03-01, firstSunday=2019-03-03, lastFriday=2019-03-29, lastSunday=2019-03-31}
算法通过适当选择实用程序类TemporalAdjusters的方法,您可以通过指定所需的工作日来轻松定义适当的时间跳转:
static Map<String, LocalDate>
firstAndLastFridayAndSunday(final YearMonth yearMonth)
{
var toFirstFriday = TemporalAdjusters.firstInMonth(DayOfWeek.FRIDAY);
var toFirstSunday = TemporalAdjusters.firstInMonth(DayOfWeek.SUNDAY);
var toLastFriday = TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY);
var toLastSunday = TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY);
var day = LocalDate.of(yearMonth.getYear(), yearMonth.getMonth(), 15);
return new TreeMap<>(Map.of("firstFriday", day.with(toFirstFriday),
"firstSunday", day.with(toFirstSunday),
"lastFriday", day.with(toLastFriday),
"lastSunday", day.with(toLastSunday)));
}
确认
让我们为验证创建一个单元测试:
@Test
void testFirstAndLastFridayAndSunday()
{
final YearMonth march2019 = YearMonth.of(2019, Month.MARCH);
var expected = Map.of("firstFriday", LocalDate.of(2019, 3, 1),
"firstSunday", LocalDate.of(2019, 3, 3),
"lastFriday", LocalDate.of(2019, 3, 29),
"lastSunday", LocalDate.of(2019, 3, 31));
var result = Ex08_WeekDays.firstAndLastFridayAndSunday(march2019);
assertEquals(expected, result);
}
解决方案 8c:月份或年份中的某一天(★✩✩✩✩)
在练习部分 8b 中,您确定了 2019 年 3 月的四个日期。这两种情况下都是在三月的哪一天?一年中的哪一天?
例子
|投入
|
一个月中的第几天
|
一年中的每一天
| | --- | --- | --- | | 第一个星期五=2019-03-01 | one | Sixty | | 第一个星期天=2019-03-03 | three | Sixty-two | | lastFriday=2019-03-29 | Twenty-nine | Eighty-eight | | 载荷空间=2019-03-31 | Thirty-one | Ninety |
算法通过适当的调用,分配很容易实现。为了获得吸引人的输出,您进一步使用具有适当格式规范的String.format():
static void dayOfMonthAndDayInYear(final Map<String, LocalDate> days)
{
System.out.println("Day Of Month / Of Year");
for (final String key : List.of("firstFriday", "firstSunday",
"lastFriday", "lastSunday"))
{
final LocalDate day = days.getOrDefault(key, LocalDate.now());
System.out.println(String.format("%-12s %9d %9d",
key,
day.getDayOfMonth(),
day.getDayOfYear()));
}
}
确认
当使用练习部分 8b 中计算的映射作为输入进行调用时,上述行输出以下内容:
Day Of Month / Of Year
firstFriday 1 60
firstSunday 3 62
lastFriday 29 88
lastSunday 31 90
6.3.9 解决方案 9:星期日和闰年(★★✩✩✩)
解决方案 9a:周日
计算由两个LocalDate给定的范围内的星期日数。为此,编写方法Stream<LocalDate> allSundaysBetween(LocalDate, LocalDate),其中开始日期包含在内,结束日期不包含在内。
例子
|时期
|
结果
| | --- | --- | | 1.1.2017 – 1.1.2018 | Fifty-three | | 1.1.2019 – 7.2.2019 | five |
算法基于流 API,赋值可以很容易实现:
static Stream<LocalDate> allSundaysBetween(final LocalDate start,
final LocalDate end)
{
final Predicate<LocalDate> isSunday =
day -> day.getDayOfWeek() == DayOfWeek.SUNDAY;
return start.datesUntil(end).filter(isSunday);
}
确认
对于 2017 年,确定了 53 个星期日。从 2019 年 1 月 1 日到 2019 年 7 月 2 日,数字是 5,根据预期。下面的单元测试可以确保:
@ParameterizedTest(name = "allSundaysBetween({0}, {1}) => {2}")
@CsvSource({ "2017-01-01, 2018-01-01, 53", "2019-01-01, 2019-02-07, 5" })
void allSundaysBetween(LocalDate start, LocalDate end, int expected)
{
var result = Ex09_CountSundays.allSundaysBetween(start, end);
assertEquals(expected, result.count());
}
您可以再次看到集成到 JUnit 5 中的参数转换器是多么有用。有了它们,日期被自动转换成一个LocalDate。这导致非常可读和可理解的测试。
解决方案 9b:闰年
在Year实例给定的范围内计算闰年数。为此,编写方法long countLeapYears(Year, Year),其中起始年包含,结束年不包含。
例子
|时期
|
结果
| | --- | --- | | 2010 – 2019 | Two | | 2000 – 2019 | five |
算法虽然通过一些思考和技巧你可以回到datesUntil()方法,但这里你想采取不同的方法。使用流 API 和方法range(),可以将数值创建为流。现在你过滤所有闰年。之后,你只需要调用count()就可以从 Stream API 中统计出剩余年限。
static long countLeapYears(final Year start, final Year end)
{
return IntStream.range(start.getValue(), end.getValue()).
filter(Year::isLeap).
count();
}
确认
如果您检查 2010 年到 2019 年期间的功能,结果应该是两个闰年。从 2000 年到 2019 年,有五个。
@ParameterizedTest(name = "countLeepYears({0}, {1}) => {2}")
@CsvSource({ "2010, 2019, 2", "2000, 2019, 5" })
public void countLeapYears(Year start, Year end, int expected)
{
long result = Ex09_CountSundays.countLeapYears(start, end);
assertEquals(expected, result);
}
同样,JUnit 5 内置的参数处理自动化可以帮助您将数字转换成类型为Year的对象。这使得这个测试又好又短,简洁又精确。
6.3.10 解决方案 10:临时助理(★★★✩✩)
编写一个TemporalAdjuster,将日期值移动到每个季度的开始,比如从 2 月 7 日到 1 月 1 日。
例子
|投入
|
结果
| | --- | --- | | LocalDate.of(2014,3,15) | 2014-01-01 | | LocalDate.of(2014 年 6 月 15 日) | 2014-04-01 | | LocalDate.of(2014,9,15) | 2014-07-01 | | LocalDate.of(2014,11,15) | 2014-10-01 |
Tip
在Month和IsoFields类中四处看看。
算法如果您稍微研究一下日期和时间 API,您可能会发现在Month类中有一个方法getLong(TemporalField),可用于获取一个月的季度。现在,基于此,您可以在数组中确定该季度合适的开始月份。
public class Ex10_FirstDayOfQuarter implements TemporalAdjuster
{
private static final Month[] startMonthOfQuarter = { Month.JANUARY,
Month.APRIL,
Month.JULY,
Month.OCTOBER };
@Override
public Temporal adjustInto(final Temporal temporal)
{
final int currentQuarter = getQuarter(temporal);
final Month startMonth = startMonthOfQuarter[currentQuarter - 1];
return LocalDate.from(temporal).
withMonth(startMonth.getValue()).
with(firstDayOfMonth());
}
private int getQuarter(final Temporal temporal)
{
return (int)Month.from(temporal).
getLong(IsoFields.QUARTER_OF_YEAR);
}
}
优化算法刚才展示的解决方案是通用的,可以适应其他需求。对日期和时间 API 的进一步实验为我们带来了一个非常简洁但同样有说服力的解决方案:
public class Ex10_FirstDayOfQuarterOptimized implements TemporalAdjuster
{
@Override
public Temporal adjustInto(final Temporal temporal)
{
return LocalDate.from(temporal).with(IsoFields.DAY_OF_QUARTER, 1);
}
}
确认
使用这两种变体的单元测试如下所示:
@ParameterizedTest(name = "move {0} to first of quarter: {1}")
@CsvSource({ "2014-03-15, 2014-01-01", "2014-06-16, 2014-04-01",
"2014-09-15, 2014-07-01", "2014-11-15, 2014-10-01" })
void adjustToFirstDayOfQuarter(LocalDate startDate, LocalDate expected)
{
var result = new Ex10_FirstDayOfQuarter().adjustInto(startDate);
assertEquals(expected, result);
}
@ParameterizedTest(name = "move {0} to first of quarter: {1}")
@CsvSource({ "2014-03-15, 2014-01-01", "2014-06-16, 2014-04-01",
"2014-09-15, 2014-07-01", "2014-11-15, 2014-10-01" })
void adjustToFirstDayOfQuarterOptimized(LocalDate startDate, LocalDate expected)
{
var result = new Ex10_FirstDayOfQuarterOptimized().adjustInto(startDate);
assertEquals(expected, result);
}
6.3.11 解决方案 11:第 n 周工作日调整(★★★✩✩)
编写跳转到一周第 n 天的类NthWeekdayAdjuster,比如第三个星期五。这要从 a LocalDate给的月初说起。对于较大的 *n,*值,它应该跳到随后的月份。
例子
使用以下开始日期为 2015 年 8 月 15 日的时间跳转来验证此课:
|开始日期
|
跳跃目标
|
结果
| | --- | --- | --- | | 2015-08-15 (2015-08-15) | 第二个星期五 | 2015-08-14 | | 2015-08-15 (2015-08-15) | 第三个星期天 | 2015-08-16 | | 2015-08-15 (2015-08-15) | 第四个星期二 | 2015-08-25 |
算法定义类Ex11_NthWeekdayAdjuster,向其传递所需的一周中的某一天及其编号,以调整到第 n 个工作日。通过从LocalDate类调用from(TemporalAccessor)方法,您创建了一个对应于传入的Temporal对象的LocalDate实例,您使用它的firstInMonth(DayOfWeek)方法将它移动到一周的第一天。之后,您使用next(DayOfWeek)方法,这将使您前进到下一个指定的工作日。为了计数,你必须记住你说的是第四个星期天,但是当然,计数从 1 开始。
public class Ex11_NthWeekdayAdjuster implements TemporalAdjuster
{
private final DayOfWeek dayToAdjust;
private final int count;
public Ex11_NthWeekdayAdjuster(final DayOfWeek dayToAdjust, final int count)
{
this.dayToAdjust = dayToAdjust;
this.count = count;
}
public Temporal adjustInto(final Temporal input)
{
final LocalDate startday = LocalDate.from(input);
LocalDate adjustedDay =
startday.with(TemporalAdjusters.firstInMonth(dayToAdjust));
for (int i = 1; i < count; i++)
{
adjustedDay = adjustedDay.with(TemporalAdjusters.next(dayToAdjust));
}
return input.with(adjustedDay);
}
}
当实现移动到一周的第 n 天时,记得从值 1 开始,因为人类的思维模式(通常)不是从 0 开始的!
确认
让我们使用以下单元测试来验证您的解决方案:
@ParameterizedTest(name = "adjusting {0} to {1}. {2} => {3}")
@CsvSource({ "2015-08-15, 2, FRIDAY, 2015-08-14",
"2015-08-15, 3, SUNDAY, 2015-08-16",
"2015-08-15, 4, TUESDAY, 2015-08-25" })
void adjustToFirstDayOfQuarter(LocalDate startDay, int count,
DayOfWeek dayOfWeek, LocalDate expected)
{
var nthWeekdayAdjuster = new Ex11_NthWeekdayAdjuster(dayOfWeek, count);
var result = nthWeekdayAdjuster.adjustInto(startDay);
assertEquals(expected, result);
}
6.3.12 解决方案 12:发薪日临时助理(★★★✩✩)
实现计算瑞士典型发薪日的类Ex12_NextPaydayAdjuster。这通常是每月的 25 号。如果这一天适逢周末,前一个星期五作为发薪日。如果你在付款日之后,那么这被认为是在下个月。作为一个自由式,你仍然需要对 12 月份的工资支付执行一个特殊的规则。在这里,付款应该在月中进行。如有必要,如果发薪日是周末,付款将被移到下一个星期一。
例子
基于以下日期值验证此类:
|投入
|
结果
|
规则
| | --- | --- | --- | | 2019-07-21 | 2019-07-25 | 正常调整 | | 2019-06-27 | 2019-07-25 | 正常调整,下个月 | | 2019-08-21 | 2019-08-23 | 星期五,如果 25 号是周末 | | 2019-12-06 | 2019-12-16 | 十二月:月中,周末后的星期一 | | 2019-12-23 | 2020-01-24 | 下个月和星期五,如果 25 号是周末 |
算法让我们试着一步步开发需求。首先将任意日期调整为该月的第 25 天。
计算一个月的 25 号要实现工作日的期望调整,您需要依赖接口TemporalAdjuster,特别是方法adjustInto(Temporal)。首先,通过从类LocalDate中调用方法from(TemporalAccessor),您确定了一个对应于传递的Temporal对象的LocalDate实例。要将此移动到第 25 个到第个,使用codeLocalDate的方法withDayOfMonth(int):
class Ex12_NextPaydayAdjuster implements TemporalAdjuster
{
@Override
public Temporal adjustInto(final Temporal temporal)
{
LocalDate date = LocalDate.from(temporal);
date = date.withDayOfMonth(25);
return temporal.with(date);
}
}
让我们在 JShell 中尝试一下:
jshell> import java.time.*
jshell> import java.time.temporal.*
jshell> var jan31 = LocalDate.of(2015, Month.JANUARY, 31)
jshell> jan31.with(new Ex12_NextPaydayAdjuster())
$24 ==> 2015-01-25
jshell> var feb7 = LocalDate.of(2015, Month.FEBRUARY, 7);
jshell> feb7.with(new Ex12_NextPaydayAdjuster())
$26 ==> 2015-02-25
如此处所示,调整已经非常有效。然而,有一件小事需要考虑:在 25 号以后的日子里,下一个发薪日不会在同一个月。从 26 号到月底的天数将计入下个月。因此,您可以按如下方式调整计算:
@Override
public Temporal adjustInto(final Temporal temporal)
{
LocalDate date = LocalDate.from(temporal);
if (date.getDayOfMonth() > 25)
{
date = date.plusMonths(1);
}
date = date.withDayOfMonth(25);
return temporal.with(date);
}
这是很好的一步。你仍然缺少的是周末的考虑。
周末特别修正如果发薪日是在周末,选择它之前的周五。这导致以粗体标记的添加:
@Override
public Temporal adjustInto(final Temporal temporal)
{
LocalDate date = LocalDate.from(temporal);
if (date.getDayOfMonth() > 25)
{
date = date.plusMonths(1);
}
date = date.withDayOfMonth(25);
if (date.getDayOfWeek() == SATURDAY ||
date.getDayOfWeek() == SUNDAY)
{
date = date.with(TemporalAdjusters.previous(FRIDAY));
}
return temporal.with(date);
}
您可以看到,通过整合现实世界的需求,原本简单的方法变得有点复杂。尽管如此,由于有了日期和时间 API,它仍然具有很好的可读性!
12 月的特殊待遇您现在整合了 12 月的特殊待遇。在那里,工资在月中支付,如果 15 号是周末,则在下一个星期一支付。
当思考问题和解决方案时,可能会想到预定义的TemporalAdjuster类。这里,方法nextOrSame(DayOfWeek)和previousOrSame(DayOfWeek)以及匹配的TemporalAdjuster实例可以如下使用:
if (isDecember)
{
date = date.with(TemporalAdjusters.nextOrSame(MONDAY));
}
else
{
date = date.with(TemporalAdjusters.previousOrSame(FRIDAY));
}
完成实现在完成这些单独的步骤后,让我们来看看最终的完整实现,以便更好地理解:
public class Ex12_NextPaydayAdjuster implements TemporalAdjuster
{
@Override
public Temporal adjustInto(final Temporal temporal)
{
LocalDate date = LocalDate.from(temporal);
boolean isDecember = date.getMonth() == Month.DECEMBER;
int paymentDay = isDecember ? 15 : 25;
if (date.getDayOfMonth() > paymentDay)
{
date = date.plusMonths(1);
// queries necessary again, as possibly postponed by one month
isDecember = date.getMonth().equals(Month.DECEMBER);
paymentDay = isDecember ? 15 : 25;
}
date = date.withDayOfMonth(paymentDay);
if (date.getDayOfWeek() == DayOfWeek.SATURDAY ||
date.getDayOfWeek() == DayOfWeek.SUNDAY)
{
if (isDecember)
date = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
else
date = date.with(TemporalAdjusters.previousOrSame(DayOfWeek.FRIDAY));
}
return temporal.with(date);
}
}
确认
因为这个实现已经相当复杂了,所以强烈建议用几个测试用例进行彻底的单元测试。下面您将学习如何使用 JUnit 5 创建重要的测试。为此,您将使用以下技巧:
-
您包括了一个没有用于测试用例的附加参数,它提供了关于测试用例的信息。
-
到目前为止,您已经使用了
@CsvSource来指定逗号分隔的值。这在这里不太可能,因为提示文本中使用了逗号。作为一种变通方法,@CsvSource允许通过提供delimiter来指定所需的分隔符。
@ParameterizedTest(name="adjustToPayday({0}) => {1}, {2}")
@CsvSource(value= { "2019-07-21; 2019-07-25; nnormal adjustment",
"2019-06-27; 2019-07-25; normal adjustment, next month",
"2019-08-21; 2019-08-23; Friday, if 25th in weekend",
"2019-12-06; 2019-12-16; December: mid of month, Monday after weekend",
"2019-12-23; 2020-01-24; next month and Friday if 25th on weekend" }},
delimiterString=";")
public void adjustInto(LocalDate startDay, LocalDate expected, String info)
{
final TemporalAdjuster paydayAdjuster = new Ex12_NextPaydayAdjuster();
final Temporal result = paydayAdjuster.adjustInto(startDay);
assertEquals(expected, result);
}
因为整个事情应该真正说服甚至最后的测试马弗,我仍然想在 Eclipse 中映射信息输出(见图 6-1 )。
图 6-1
Ex12_NextPaydayAdjuster 的单元测试输出
6.3.13 解决方案 13:格式化和解析(★★✩✩✩)
创建一个LocalDateTime对象,并以各种格式打印。格式化并解析这些格式:dd.MM.yyy HH、dd.MM.yy HH:mm、ISO_LOCAL_DATE_TIME、SHORT 和美国地区。
例子
对于 2017 年 7 月 27 日 13 时 14 分 15 秒,输出应如下所示:
|格式化
|
从语法上分析
|
格式
| | --- | --- | --- | | 27 07 2017 13 | 2017-07-27T13:00 | dd MM yyyy HH | | 27.07.17 13:14 | 2017-07-27T13:14 | dd。嗯。yy 时:分 | | 2017-07-27T13:14:15 | 2017-07-27T13:14:15 | ISO_LOCAL_DATE_TIME | | 2017 年 7 月 27 日下午 1 点 14 分 | 2017-07-27T13:14 | 短+区域设置。美国 |
Tip
使用DateTimeFormatter类和它的常量以及它的方法,比如ofPattern()和ofLocalizedDateTime()。
算法用于格式化和解析,使用了类DateTimeFormatter及其各种实用方法。首先转换为所需格式,然后从中进行解析的实用程序方法实现如下:
static void applyFormatters(final LocalDateTime base,
final List<DateTimeFormatter> formatters)
{
formatters.forEach((formatter) -> {
final String formatted = base.format(formatter);
try
{
// attention: pitfall
// TemporalAccessor parsed = formatter.parse(formatted);
LocalDateTime parsed = LocalDateTime.parse(formatted, formatter);
System.out.println("Formatted: " + formatted + " / " +
"Parsed: " + parsed);
}
catch (DateTimeParseException ignore)
{
}
});
}
因此,你认为这是一个暗示,一个人们喜欢上当的陷阱。格式化程序提供了一个用于解析的方法parse(),但是这返回了一个通用的TemporalAccessor对象,而不是所需的专门化。
确认
要在 JShell 中试用它,除了众所周知的第一个导入之外,还需要另一个导入:
import java.time.*
import java.time.format.*
然后,您可以键入以下几行并理解不同的格式:
jshell> var someday = LocalDateTime.of(2017, 7, 27, 13, 14, 15)
...>
...> var format1 = DateTimeFormatter.ofPattern("dd.MM.yyyy HH")
...> var format2 = DateTimeFormatter.ofPattern("dd.MM.yy HH:mm")
...> var format3 = DateTimeFormatter.ISO_LOCAL_DATE_TIME
...> var format4 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT,
...> FormatStyle.SHORT)
...> .withLocale(Locale.US);
...>
...> applyFormatters(someday, List.of(format1, format2,
...> format3, format4));
Formatted: 27.07.2017 13 / Parsed: 2017-07-27T13:00
Formatted: 27.07.17 13:14 / Parsed: 2017-07-27T13:14
Formatted: 2017-07-27T13:14:15 / Parsed: 2017-07-27T13:14:15
Formatted: 7/27/17, 1:14 PM / Parsed: 2017-07-27T13:14
6.3.14 解决方案 14:容错解析(★★✩✩✩)
评估用户输入时,容错通常很重要。您的任务是创建方法Optional<LocalDate> faultTolerantParse(String, Set<DateTimeFormatter>),它允许您解析以下日期格式:dd.MM.yy、dd.MM.yyy、MM/dd/yyyy和yyyy-MM-dd。
例子
下表显示了解析不同输入的预期结果。请务必注意,对于模式中的两位数年份,有时它不会生成正确或预期的日期!
|投入
|
结果
| | --- | --- | | “07.02.71” | 2071-02-07 | | “07.02.1971” | 1971-02-07 | | “02/07/1971” | 1971-02-07 | | “1971-02-07” | 1971-02-07 |
算法容错解析涉及一次尝试解析一组提供的DateTimeFormatter s。如果出现异常,则该格式不合适,只要有更多的可用格式,就会尝试下一个格式。任务所需的格式化程序是在一个单独的方法中创建的:
static Optional<LocalDate> faultTolerantParse(final CharSequence input,
final Set<DateTimeFormatter>
formatters)
{
LocalDate result = null;
var it = formatters.iterator();
while (result == null && it.hasNext())
{
var entry = it.next();
try
{
result = LocalDate.parse(input, entry);
}
catch (DateTimeParseException ignore)
{
// try next
}
}
return Optional.ofNullable(result);
}
static Set<DateTimeFormatter> populateFormatters()
{
final Set<DateTimeFormatter> formatters = new LinkedHashSet<>();
formatters.add(DateTimeFormatter.ofPattern("dd.MM.yy"));
formatters.add(DateTimeFormatter.ofPattern("dd.MM.yyyy"));
formatters.add(DateTimeFormatter.ofPattern("MM/dd/yyyy"));
formatters.add(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
return formatters;
}
确认
您定义了要支持的四种不同的日期格式,然后定义了一些与这些格式相对应的日期符号。测试表明,四种格式的输入都能被成功识别和解析:
@ParameterizedTest(name = "faultTolerantParse({0}) expected date {1}")
@CsvSource({ "07.02.71, 2071-02-07", "07.02.1971, 1971-02-07",
"02/07/1971, 1971-02-07", "1971-02-07, 1971-02-07" })
void faultTolerantParse(String dateAsString, LocalDate expected)
{
var formatters = Ex14_FaultTolerantParser.populateFormatters();
var optParsedLocalDate =
Ex14_FaultTolerantParser.faultTolerantParse(dateAsString, formatters);
assertTrue(optParsedLocalDate.isPresent());
assertEquals(expected, optParsedLocalDate.get());
}
最后,让我们看看容错解析对于两种不同的格式是如何表现的。您认为由于不匹配的格式,您不能提供一个LocalDate:
@ParameterizedTest(name = "faultTolerantParse({0}) expected empty")
@CsvSource({ "31-01-1940", "1940/01/31" })
void faultTolerantParseInvalidFormats(String dateAsString)
{
var formatters = Ex14_FaultTolerantParser.populateFormatters();
var optParsedLocalDate =
Ex14_FaultTolerantParser.faultTolerantParse(dateAsString, formatters);
assertTrue(optParsedLocalDate.isEmpty());
}
Footnotes 1
特别是,你通常对约会发生的时区不感兴趣——例如,除了与海外商业伙伴的电话约会。
2
不支持可变长度的时间单位,如月。
3
参见 http://en.wikipedia.org/wiki/ISO_8601#Durations; 这源于历史命名时期,所以 P,而 T 代表时间。