Skip to content

docs: Introduce new concept for TemporalAdjusters #2944

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions concepts/temporal-adjusters/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"blurb": "Perform date adjustments using Java's `TemporalAdjuster` interface and `TemporalAdjusters` utility class.",
"authors": [
"rabestro"
],
"contributors": []
}
140 changes: 140 additions & 0 deletions concepts/temporal-adjusters/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# About

While the core `java.time` classes like `LocalDate` and `LocalDateTime` provide basic methods for adding or subtracting time units, more complex date adjustments often require more sophisticated logic (e.g., finding the "second Tuesday of the month" or the "last day of the year").

The `java.time.temporal` package provides the [`TemporalAdjuster`][temporaladjuster-docs] functional interface for this purpose. It represents a strategy for adjusting a temporal object (like a `LocalDate`). The [`TemporalAdjusters`][temporaladjusters-docs] (note the plural 's') utility class contains static factory methods providing many common, predefined adjuster implementations.

## Using TemporalAdjusters

Temporal objects like `LocalDate` and `LocalDateTime` have a `with()` method that accepts a `TemporalAdjuster`. This method returns a *new* temporal object with the adjustment applied.

```exercism/note
Using `with(TemporalAdjuster)` returns a _new_ instance and does not update the existing instance, as all `java.time` temporal classes are immutable.
```

```java
import java.time.LocalDate;
import java.time.Month;
import java.time.DayOfWeek;
import java.time.temporal.TemporalAdjusters;

LocalDate date = LocalDate.of(2024, Month.AUGUST, 15); // 2024-08-15 (a Thursday)

// Use a predefined adjuster from TemporalAdjusters
LocalDate firstDayOfNextMonth = date.with(TemporalAdjusters.firstDayOfNextMonth());
// => 2024-09-01

LocalDate nextSunday = date.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
// => 2024-08-18
```

## Common Predefined Adjusters

The `TemporalAdjusters` class provides many useful static methods. Here are some common examples:

### Finding Ordinal Day of Week

The `dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek)` adjuster finds the nth occurrence of a specific day of the week within the month.

```java
LocalDate date = LocalDate.of(2024, Month.AUGUST, 15); // 2024-08-15

// Find the second Tuesday of August 2024
LocalDate secondTuesday = date.with(TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.TUESDAY));
// => 2024-08-13

// Find the fourth Friday of August 2024
LocalDate fourthFriday = date.with(TemporalAdjusters.dayOfWeekInMonth(4, DayOfWeek.FRIDAY));
// => 2024-08-23
```

### Finding First/Last Day of Week in Month

The `firstInMonth(DayOfWeek dayOfWeek)` and `lastInMonth(DayOfWeek dayOfWeek)` adjusters find the first or last occurrence of a day of the week within the month.

```java
LocalDate date = LocalDate.of(2024, Month.AUGUST, 15); // 2024-08-15

// Find the first Sunday of August 2024
LocalDate firstSunday = date.with(TemporalAdjusters.firstInMonth(DayOfWeek.SUNDAY));
// => 2024-08-04

// Find the last Monday of August 2024
LocalDate lastMonday = date.with(TemporalAdjusters.lastInMonth(DayOfWeek.MONDAY));
// => 2024-08-26
```

### Finding First/Last Day of Month/Year

Simpler adjusters find the first or last day of the current month or year.

```java
LocalDate date = LocalDate.of(2024, Month.AUGUST, 15); // 2024-08-15

LocalDate firstDay = date.with(TemporalAdjusters.firstDayOfMonth());
// => 2024-08-01

LocalDate lastDay = date.with(TemporalAdjusters.lastDayOfMonth());
// => 2024-08-31

LocalDate lastDayOfYear = date.with(TemporalAdjusters.lastDayOfYear());
// => 2024-12-31
```

### Relative Adjustments (Next/Previous)

Adjusters like `next()`, `previous()`, `nextOrSame()`, and `previousOrSame()` find the next or previous occurrence of a given day of the week, relative to the current date.

- `next()`/`previous()`: Excludes the current date if it matches.
- `nextOrSame()`/`previousOrSame()`: Includes the current date if it matches.

```java
LocalDate thursday = LocalDate.of(2024, Month.AUGUST, 15); // A Thursday

// Find the next Sunday (exclusive)
LocalDate nextSunday = thursday.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
// => 2024-08-18

// Find the next Thursday (exclusive)
LocalDate nextThursday = thursday.with(TemporalAdjusters.next(DayOfWeek.THURSDAY));
// => 2024-08-22

// Find the next Thursday (inclusive) - returns the same date
LocalDate nextOrSameThursday = thursday.with(TemporalAdjusters.nextOrSame(DayOfWeek.THURSDAY));
// => 2024-08-15

// Find the previous Monday (exclusive)
LocalDate previousMonday = thursday.with(TemporalAdjusters.previous(DayOfWeek.MONDAY));
// => 2024-08-12
```

The `nextOrSame()` adjuster is particularly useful for finding dates in ranges like the "teenth" days (13th-19th) of a month, as seen in the Meetup exercise. Applying `nextOrSame(targetDayOfWeek)` to the 13th day of the month will find the correct "teenth" date.

## Custom Adjusters

Since `TemporalAdjuster` is a [`@FunctionalInterface`][functionalinterface-docs], you can create your own custom adjusters using lambda expressions for logic not covered by the predefined methods. The interface requires implementing a single method: `Temporal adjustInto(Temporal temporal)`.

```java
// Example: An adjuster that finds the next workday (Mon-Fri), skipping weekends.
TemporalAdjuster nextWorkday = temporal -> {
LocalDate result = (LocalDate) temporal; // Assuming input is LocalDate
do {
result = result.plusDays(1);
} while (result.getDayOfWeek() == DayOfWeek.SATURDAY || result.getDayOfWeek() == DayOfWeek.SUNDAY);
return result;
};

LocalDate friday = LocalDate.of(2024, Month.AUGUST, 16);
LocalDate nextWorkdayDate = friday.with(nextWorkday);
// => 2024-08-19 (Monday)

LocalDate sunday = LocalDate.of(2024, Month.AUGUST, 18);
LocalDate nextWorkdayFromSunday = sunday.with(nextWorkday);
// => 2024-08-19 (Monday)
```

While custom adjusters are powerful, the predefined adjusters in the `TemporalAdjusters` class cover a wide range of common use cases.

[temporaladjuster-docs]: https://docs.oracle.com/javase/8/docs/api/java/time/temporal/TemporalAdjuster.html
[temporaladjusters-docs]: https://docs.oracle.com/javase/8/docs/api/java/time/temporal/TemporalAdjusters.html
[functionalinterface-docs]: https://docs.oracle.com/javase/8/docs/api/java/lang/FunctionalInterface.html
62 changes: 62 additions & 0 deletions concepts/temporal-adjusters/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Introduction

When working with `java.time` objects like `LocalDate`, you often need to perform complex date adjustments, such as finding the "second Tuesday of the month". Instead of calculating this manually, you can use **`TemporalAdjusters`**.

The `java.time.temporal.TemporalAdjusters` (note the plural 's') utility class provides static methods for many common date adjustments. You apply these adjustments using the `.with()` method on a temporal object (like `LocalDate`).

```exercism/note
Using `with(TemporalAdjuster)` returns a _new_ instance and does not update the existing instance, as `java.time` objects are immutable.
```

```java
import java.time.LocalDate;
import java.time.Month;
import java.time.DayOfWeek;
import java.time.temporal.TemporalAdjusters;

LocalDate date = LocalDate.of(2024, Month.AUGUST, 15); // 2024-08-15 (a Thursday)

// Use a predefined adjuster: find the last day of the month
LocalDate lastDay = date.with(TemporalAdjusters.lastDayOfMonth());
// => 2024-08-31
```

Here are some key adjusters useful for scheduling calculations:

## Finding Ordinal Day of Week

Use `dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek)` to find the nth occurrence of a day of the week.

```java
// Find the second Tuesday of August 2024
LocalDate secondTuesday = date.with(TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.TUESDAY));
// => 2024-08-13
```

## Finding First/Last Day of Week in Month

Use `firstInMonth(DayOfWeek dayOfWeek)` or `lastInMonth(DayOfWeek dayOfWeek)`.

```java
// Find the last Monday of August 2024
LocalDate lastMonday = date.with(TemporalAdjusters.lastInMonth(DayOfWeek.MONDAY));
// => 2024-08-26
```

## Finding Relative Occurrences ("Teenth" dates)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a left over from writing for the "meetup" exercise? Since this is an introduction to the temporal adjusters concept, I think we can copy the Relative Adjustments (Next/Previous) content from the about.md here.


Use `nextOrSame(DayOfWeek dayOfWeek)`. This is useful for finding dates like the "teenth" days (13th-19th). Apply it to the 13th day of the month to find the target day of the week within that "teenth" range.

```java
LocalDate thirteenth = LocalDate.of(2024, Month.AUGUST, 13); // Tuesday the 13th

// Find the first Saturday on or after the 13th ("Teenth" Saturday)
LocalDate teenthSaturday = thirteenth.with(TemporalAdjusters.nextOrSame(DayOfWeek.SATURDAY));
// => 2024-08-17

// Find the first Tuesday on or after the 13th ("Teenth" Tuesday)
LocalDate teenthTuesday = thirteenth.with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY));
// => 2024-08-13 (returns the same date as it matches)
```

Using these adjusters from the `TemporalAdjusters` class can greatly simplify date manipulation tasks.
18 changes: 18 additions & 0 deletions concepts/temporal-adjusters/links.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"url": "https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/temporal/TemporalAdjuster.html",
"description": "TemporalAdjuster interface documentation"
},
{
"url": "https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/temporal/TemporalAdjusters.html",
"description": "TemporalAdjusters utility class documentation"
},
{
"url": "https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/temporal/Temporal.html",
"description": "Temporal interface documentation"
},
{
"url": "https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/DayOfWeek.html",
"description": "DayOfWeek enum documentation"
}
]
28 changes: 17 additions & 11 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,18 @@
"foreach-loops",
"generic-types"
]
},
{
"slug": "meetup",
"name": "Meetup",
"uuid": "602511d5-7e89-4def-b072-4dd311816810",
"concepts": [
"temporal-adjusters"
],
"prerequisites": [
"datetime",
"enums"
]
}
],
"practice": [
Expand Down Expand Up @@ -1481,17 +1493,6 @@
],
"difficulty": 7
},
{
"slug": "meetup",
"name": "Meetup",
"uuid": "602511d5-7e89-4def-b072-4dd311816810",
"practices": [],
"prerequisites": [
"datetime",
"enums"
],
"difficulty": 7
},
{
"slug": "poker",
"name": "Poker",
Expand Down Expand Up @@ -1971,6 +1972,11 @@
"uuid": "78f3c7b2-cb9c-4d21-8cb4-7106a188f713",
"slug": "ternary-operators",
"name": "Ternary Operators"
},
{
"uuid": "cc7a06d1-ddb3-4722-88a5-f762254d664e",
"slug": "temporal-adjusters",
"name": "Temporal Adjusters"
}
],
"key_features": [
Expand Down
43 changes: 43 additions & 0 deletions exercises/concept/meetup/.docs/hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Hints

## 1. Representing the Date

- The core class for representing dates without time in Java is [`java.time.LocalDate`][localdate-docs].
- You'll need to work with the `year` and `month` provided in the constructor.
- The target day of the week is conveniently provided directly as a [`java.time.DayOfWeek`][dayofweek-docs] enum instance in the `day()` method.

## 2. Starting Point for Adjustments

- `LocalDate` objects are immutable. To find the target meetup date, you typically start with *any* date within the target `month` and `year` and then apply an "adjuster" to find the correct day.
- For example, you could create a `LocalDate` for the 1st of the month: `LocalDate.of(year, month, 1)`.
- For "Teenth" calculations, starting from the 13th of the month (`LocalDate.of(year, month, 13)`) can be helpful.

## 3. Adjusting Dates with `TemporalAdjusters`

- The key to solving this idiomatically is the [`java.time.temporal.TemporalAdjusters`][temporaladjusters-docs] utility class (note the plural 's'). It provides static methods that return [`TemporalAdjuster`][temporaladjuster-docs] instances for common date calculations.
- You apply an adjuster to a `LocalDate` using its `.with()` method: `someLocalDate.with(TemporalAdjusters.someAdjuster(...))`
- This returns a *new* `LocalDate` instance with the adjustment applied.

## 4. Handling "Ordinal" Schedules (FIRST, SECOND, THIRD, FOURTH)

- Look for a method in `TemporalAdjusters` that allows you to find a date based on its ordinal position (1st, 2nd, 3rd, 4th) and its `DayOfWeek` within the month.
- You might need to convert the `MeetupSchedule` enum values (`FIRST`, `SECOND`, etc.) into the corresponding integer ordinals (1, 2, 3, 4) to use this adjuster.

## 5. Handling the "LAST" Schedule

- `TemporalAdjusters` provides a specific adjuster to find the *last* occurrence of a given `DayOfWeek` within the month.

## 6. Handling the "TEENTH" Schedule

- "Teenth" days run from the 13th to the 19th of the month.
- Consider starting from the 13th day of the month (`LocalDate.of(year, month, 13)`).
- Look for an adjuster in `TemporalAdjusters` that finds the *next* occurrence of the target `DayOfWeek`, potentially *including* the date you're adjusting if it already matches the target day of the week.

## 7. Selecting the Right Adjuster

- Use the input `schedule` (which is a `MeetupSchedule` enum value) to determine which specific `TemporalAdjuster` method to use. A `switch` statement or `if-else if` chain on the `schedule` value is a common approach.

[localdate-docs]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html
[dayofweek-docs]: https://docs.oracle.com/javase/8/docs/api/java/time/DayOfWeek.html
[temporaladjuster-docs]: https://docs.oracle.com/javase/8/docs/api/java/time/temporal/TemporalAdjuster.html
[temporaladjusters-docs]: https://docs.oracle.com/javase/8/docs/api/java/time/temporal/TemporalAdjusters.html
3 changes: 3 additions & 0 deletions exercises/concept/meetup/.docs/introduction.md.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Introduction

%{concept:strings}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"test": [
"src/test/java/MeetupTest.java"
],
"example": [
"exemplar": [
".meta/src/reference/java/Meetup.java"
],
"editor": [
Expand Down
45 changes: 45 additions & 0 deletions exercises/concept/meetup/.meta/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Design

## Learning objectives

- Know about the `java.time.temporal.TemporalAdjuster` interface and its purpose.
- Know about the `java.time.temporal.TemporalAdjusters` utility class and how to use its static factory methods.
- Know how to apply a `TemporalAdjuster` to a `LocalDate` using the `.with()` method.
- Know how to select and use common predefined adjusters like `TemporalAdjusters.dayOfWeekInMonth()`, `TemporalAdjusters.lastInMonth()`, and `TemporalAdjusters.nextOrSame()`.
- Understand that `java.time` objects are immutable and adjustment methods return new instances.
- Know how to use a `java.time.DayOfWeek` enum value.
- Know how to use a custom `enum` (like `MeetupSchedule`) in conditional logic (e.g., `switch` statement).

## Out of scope

- Creating complex custom `TemporalAdjuster` implementations.
- Time zones, offsets, `ZonedDateTime`.
- `LocalDateTime`, `LocalTime`.
- Durations (`java.time.Duration`) and Periods (`java.time.Period`).
- Date/time parsing and formatting.

## Concepts

- `temporal-adjusters`: This exercise introduces and focuses on the `TemporalAdjusters` concept.
- `datetime`: Reinforces basic usage of `LocalDate` and `DayOfWeek`.
- `enums`: Reinforces usage of enums.

## Prerequisites

- `datetime`: Must know how to create and use basic `java.time.LocalDate` objects and understand `DayOfWeek`.
- `enums`: Must know how to work with `enum` types, including using them in `switch` statements or accessing properties like `ordinal()`.
- `classes`: Basic understanding of Java classes and methods.
- `methods`: Basic understanding of Java methods.

## Analyzer

This exercise could benefit from the following rules in the [analyzer]:

- `essential` (replaces `actionable` if it MUST be fixed): If the solution involves significant manual date calculation logic (e.g., loops checking day of week, complex `plusDays`/`minusDays` sequences) instead of using `TemporalAdjusters`, instruct the student that using the `TemporalAdjusters` class is the idiomatic approach for this exercise. Provide a link to the `TemporalAdjusters` concept documentation.
- `actionable`: If the student uses `TemporalAdjusters.dayOfWeekInMonth()`, check if they correctly map the `MeetupSchedule` enum ordinal (0-based) to the required 1-based ordinal argument. If not, provide a hint.
- `actionable`: If the logic for `MeetupSchedule.TEENTH` seems incorrect or overly complex, suggest using `TemporalAdjusters.nextOrSame()` applied to the 13th day of the month.
- `informative`: If the student uses `TemporalAdjusters` correctly, mention the specific adjusters used (`dayOfWeekInMonth`, `lastInMonth`, `nextOrSame`) as good choices for their respective cases.

If the solution uses `TemporalAdjusters` correctly for all `MeetupSchedule` cases, it should be considered exemplar. Leave a `celebratory` comment to celebrate the success!

[analyzer]: https://github.com/exercism/java-analyzer
File renamed without changes.
Loading