diff --git a/concepts/temporal-adjusters/.meta/config.json b/concepts/temporal-adjusters/.meta/config.json new file mode 100644 index 000000000..48d931526 --- /dev/null +++ b/concepts/temporal-adjusters/.meta/config.json @@ -0,0 +1,7 @@ +{ + "blurb": "Perform date adjustments using Java's `TemporalAdjuster` interface and `TemporalAdjusters` utility class.", + "authors": [ + "rabestro" + ], + "contributors": [] +} diff --git a/concepts/temporal-adjusters/about.md b/concepts/temporal-adjusters/about.md new file mode 100644 index 000000000..66291ab02 --- /dev/null +++ b/concepts/temporal-adjusters/about.md @@ -0,0 +1,143 @@ +# 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 +``` + +## 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/en/java/javase/25/docs/api/java.base/java/time/temporal/TemporalAdjuster.html +[temporaladjusters-docs]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/temporal/TemporalAdjusters.html +[functionalinterface-docs]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/FunctionalInterface.html diff --git a/concepts/temporal-adjusters/introduction.md b/concepts/temporal-adjusters/introduction.md new file mode 100644 index 000000000..d1bee817f --- /dev/null +++ b/concepts/temporal-adjusters/introduction.md @@ -0,0 +1,93 @@ +# 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 +``` + +## 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 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 +``` + +## Custom Adjusters + +`TemporalAdjuster` is a functional interface, so you can write an adjuster of your own as a lambda that takes the temporal to adjust and returns the adjusted one. +It is applied with `.with()` just like the predefined adjusters, and it can use them internally. + +```java +import java.time.temporal.TemporalAdjuster; + +// An adjuster that moves a weekend date to the following Monday +TemporalAdjuster skipWeekend = temporal -> { + DayOfWeek day = DayOfWeek.from(temporal); + if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) { + return temporal.with(TemporalAdjusters.next(DayOfWeek.MONDAY)); + } + return temporal; +}; + +LocalDate saturday = LocalDate.of(2024, Month.AUGUST, 17); +LocalDate monday = saturday.with(skipWeekend); +// => 2024-08-19 +``` + +Using these adjusters from the `TemporalAdjusters` class can greatly simplify date manipulation tasks. diff --git a/concepts/temporal-adjusters/links.json b/concepts/temporal-adjusters/links.json new file mode 100644 index 000000000..ca655ff49 --- /dev/null +++ b/concepts/temporal-adjusters/links.json @@ -0,0 +1,18 @@ +[ + { + "url": "https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/temporal/TemporalAdjuster.html", + "description": "TemporalAdjuster interface documentation" + }, + { + "url": "https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/temporal/TemporalAdjusters.html", + "description": "TemporalAdjusters utility class documentation" + }, + { + "url": "https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/temporal/Temporal.html", + "description": "Temporal interface documentation" + }, + { + "url": "https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/DayOfWeek.html", + "description": "DayOfWeek enum documentation" + } +] diff --git a/config.json b/config.json index cfa24a7a8..09dfb543a 100644 --- a/config.json +++ b/config.json @@ -328,6 +328,19 @@ "generic-types" ], "status": "active" + }, + { + "slug": "beauty-salon-regulars", + "name": "Beauty Salon Regulars", + "uuid": "61084de5-1385-4589-acf7-b81d99b0d4b4", + "concepts": [ + "temporal-adjusters" + ], + "prerequisites": [ + "datetime", + "enums" + ], + "status": "active" } ], "practice": [ @@ -1607,7 +1620,8 @@ "practices": [], "prerequisites": [ "datetime", - "enums" + "enums", + "temporal-adjusters" ], "difficulty": 7 }, @@ -2090,6 +2104,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": [ diff --git a/exercises/concept/beauty-salon-regulars/.docs/hints.md b/exercises/concept/beauty-salon-regulars/.docs/hints.md new file mode 100644 index 000000000..8f5cb6ae4 --- /dev/null +++ b/exercises/concept/beauty-salon-regulars/.docs/hints.md @@ -0,0 +1,42 @@ +# Hints + +## General + +- The [`TemporalAdjusters`][temporaladjusters-docs] class (note the plural) is a collection of static methods that return ready-made adjusters. +- An adjuster is applied to a `LocalDate` with its [`with()`][localdate-with] method, which returns a new `LocalDate` and leaves the original unchanged. +- To adjust "within a month", start from any date in that month, for example its first day: `LocalDate.of(year, month, 1)`. + +## 1. Find a regular's standing appointment + +- There is an [adjuster][dayofweekinmonth-docs] that finds the n-th occurrence of a day of the week in the month of the date it is applied to. + +## 2. Find a regular's last-of-the-month appointment + +- There is an [adjuster][lastinmonth-docs] that finds the last occurrence of a day of the week in the month of the date it is applied to. + +## 3. Find the next available day for a walk-in + +- There are two adjusters that move forward to a given day of the week: one that always moves to a later date, and [one][nextorsame-docs] that keeps the date if it already matches. + +## 4. Find the bookkeeping day + +- There is an [adjuster][lastdayofmonth-docs] that moves to the last day of the month. +- A `LocalDate` can tell you its [day of the week][localdate-getdayofweek]. +- When the last day falls on a weekend, there is an [adjuster][previous-docs] that moves back to the previous occurrence of a day of the week. + +## 5. Reuse the weekend rule for payday + +- [`TemporalAdjuster`][temporaladjuster-docs] is a functional interface with a single method, `adjustInto()`, so it can be implemented with a lambda. +- Inside the lambda, [`DayOfWeek.from()`][dayofweek-from] gives the day of the week of the temporal being adjusted. +- Adjusters can be chained: the result of applying one adjuster is a temporal you can apply another adjuster to. + +[temporaladjusters-docs]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/temporal/TemporalAdjusters.html +[temporaladjuster-docs]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/temporal/TemporalAdjuster.html +[localdate-with]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/LocalDate.html#with(java.time.temporal.TemporalAdjuster) +[localdate-getdayofweek]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/LocalDate.html#getDayOfWeek() +[dayofweek-from]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/DayOfWeek.html#from(java.time.temporal.TemporalAccessor) +[dayofweekinmonth-docs]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/temporal/TemporalAdjusters.html#dayOfWeekInMonth(int,java.time.DayOfWeek) +[lastinmonth-docs]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/temporal/TemporalAdjusters.html#lastInMonth(java.time.DayOfWeek) +[nextorsame-docs]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/temporal/TemporalAdjusters.html#nextOrSame(java.time.DayOfWeek) +[lastdayofmonth-docs]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/temporal/TemporalAdjusters.html#lastDayOfMonth() +[previous-docs]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/temporal/TemporalAdjusters.html#previous(java.time.DayOfWeek) diff --git a/exercises/concept/beauty-salon-regulars/.docs/instructions.md b/exercises/concept/beauty-salon-regulars/.docs/instructions.md new file mode 100644 index 000000000..8652b0160 --- /dev/null +++ b/exercises/concept/beauty-salon-regulars/.docs/instructions.md @@ -0,0 +1,75 @@ +# Instructions + +The beauty salon in New York that you helped in the Booking Up For Beauty exercise has been doing well, and many of its clients have become regulars. +Instead of booking every visit separately, a regular has a standing arrangement: "every second Tuesday", "the last Friday of the month", and so on. +The owner has asked you for a helper that works out the dates behind these arrangements, as well as the dates of the salon's own monthly routine. + +## 1. Find a regular's standing appointment + +Implement the `SalonCalendar.standingAppointment()` method that takes a year, a month, an occurrence (`1` for the first, `2` for the second, and so on) and a day of the week. +It returns the date of that occurrence of the day of the week within the month: + +```java +SalonCalendar calendar = new SalonCalendar(); +calendar.standingAppointment(2024, Month.AUGUST, 2, DayOfWeek.TUESDAY); +// => LocalDate.of(2024, 8, 13) +``` + +## 2. Find a regular's last-of-the-month appointment + +Some regulars prefer the last occurrence of their day in the month. +Implement the `SalonCalendar.lastAppointment()` method that takes a year, a month and a day of the week, and returns the date of the last occurrence of that day of the week within the month: + +```java +SalonCalendar calendar = new SalonCalendar(); +calendar.lastAppointment(2024, Month.AUGUST, DayOfWeek.FRIDAY); +// => LocalDate.of(2024, 8, 30) +``` + +## 3. Find the next available day for a walk-in + +A client at the front desk asks for "the next Saturday". +If the day they ask on is already a Saturday, they mean that very day. +Implement the `SalonCalendar.nextAvailable()` method that takes a date and a day of the week, and returns the first date on or after the given date that falls on that day of the week: + +```java +SalonCalendar calendar = new SalonCalendar(); +calendar.nextAvailable(LocalDate.of(2024, 8, 15), DayOfWeek.SATURDAY); +// => LocalDate.of(2024, 8, 17) +calendar.nextAvailable(LocalDate.of(2024, 8, 17), DayOfWeek.SATURDAY); +// => LocalDate.of(2024, 8, 17) +``` + +## 4. Find the bookkeeping day + +The salon closes its books on the last day of each month. +The salon is closed on weekends, though, so when the last day of the month falls on a Saturday or a Sunday, the books are closed on the Friday before. +Implement the `SalonCalendar.bookkeepingDay()` method that takes a year and a month, and returns the bookkeeping day of that month: + +```java +SalonCalendar calendar = new SalonCalendar(); +calendar.bookkeepingDay(2024, Month.SEPTEMBER); +// => LocalDate.of(2024, 9, 30) +calendar.bookkeepingDay(2024, Month.AUGUST); +// => LocalDate.of(2024, 8, 30) +``` + +## 5. Reuse the weekend rule for payday + +Staff are paid on the 15th of every month, with the same weekend rule: when the 15th falls on a Saturday or a Sunday, payday is the Friday before. +Rather than writing the rule a second time, turn it into a reusable adjuster. + +Implement the `SalonCalendar.precedingWorkday()` method that returns a `TemporalAdjuster` which leaves a weekday unchanged and moves a Saturday or a Sunday back to the preceding Friday. +Then implement the `SalonCalendar.payday()` method, which takes a year and a month, using that adjuster: + +```java +SalonCalendar calendar = new SalonCalendar(); +LocalDate.of(2024, 6, 15).with(calendar.precedingWorkday()); +// => LocalDate.of(2024, 6, 14) +calendar.payday(2024, Month.JUNE); +// => LocalDate.of(2024, 6, 14) +calendar.payday(2024, Month.AUGUST); +// => LocalDate.of(2024, 8, 15) +``` + +Feel free to go back and use the adjuster in `bookkeepingDay()` as well. diff --git a/exercises/concept/beauty-salon-regulars/.docs/introduction.md b/exercises/concept/beauty-salon-regulars/.docs/introduction.md new file mode 100644 index 000000000..62c695095 --- /dev/null +++ b/exercises/concept/beauty-salon-regulars/.docs/introduction.md @@ -0,0 +1,95 @@ +# Introduction + +## Temporal Adjusters + +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 +``` + +### 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 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 +``` + +### Custom Adjusters + +`TemporalAdjuster` is a functional interface, so you can write an adjuster of your own as a lambda that takes the temporal to adjust and returns the adjusted one. +It is applied with `.with()` just like the predefined adjusters, and it can use them internally. + +```java +import java.time.temporal.TemporalAdjuster; + +// An adjuster that moves a weekend date to the following Monday +TemporalAdjuster skipWeekend = temporal -> { + DayOfWeek day = DayOfWeek.from(temporal); + if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) { + return temporal.with(TemporalAdjusters.next(DayOfWeek.MONDAY)); + } + return temporal; +}; + +LocalDate saturday = LocalDate.of(2024, Month.AUGUST, 17); +LocalDate monday = saturday.with(skipWeekend); +// => 2024-08-19 +``` + +Using these adjusters from the `TemporalAdjusters` class can greatly simplify date manipulation tasks. diff --git a/exercises/concept/beauty-salon-regulars/.docs/introduction.md.tpl b/exercises/concept/beauty-salon-regulars/.docs/introduction.md.tpl new file mode 100644 index 000000000..40626f4f4 --- /dev/null +++ b/exercises/concept/beauty-salon-regulars/.docs/introduction.md.tpl @@ -0,0 +1,3 @@ +# Introduction + +%{concept:temporal-adjusters} diff --git a/exercises/concept/beauty-salon-regulars/.meta/config.json b/exercises/concept/beauty-salon-regulars/.meta/config.json new file mode 100644 index 000000000..0fcdb96f4 --- /dev/null +++ b/exercises/concept/beauty-salon-regulars/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "rabestro" + ], + "files": { + "solution": [ + "src/main/java/SalonCalendar.java" + ], + "test": [ + "src/test/java/SalonCalendarTest.java" + ], + "exemplar": [ + ".meta/src/reference/java/SalonCalendar.java" + ], + "invalidator": [ + "build.gradle" + ] + }, + "blurb": "Learn about temporal adjusters by scheduling standing appointments for a beauty salon's regular clients." +} diff --git a/exercises/concept/beauty-salon-regulars/.meta/design.md b/exercises/concept/beauty-salon-regulars/.meta/design.md new file mode 100644 index 000000000..65b6b81db --- /dev/null +++ b/exercises/concept/beauty-salon-regulars/.meta/design.md @@ -0,0 +1,42 @@ +# Design + +## Learning objectives + +- Know about the `java.time.temporal.TemporalAdjuster` interface and its purpose. +- Know about the `java.time.temporal.TemporalAdjusters` utility class and its static factory methods. +- Know how to apply an adjuster to a `LocalDate` using the `with()` method. +- Know how to use the common predefined adjusters: `dayOfWeekInMonth()`, `lastInMonth()`, `nextOrSame()`, `lastDayOfMonth()` and `previous()`. +- Know how to write a simple custom adjuster as a lambda and how to chain adjusters. +- Understand that `java.time` objects are immutable and that adjustment methods return new instances. + +## Out of scope + +- Time zones, offsets and `ZonedDateTime`. +- `LocalDateTime` and `LocalTime`. +- `Duration` and `Period`. +- Parsing and formatting dates. +- Anything depending on the current date (`LocalDate.now()`). + +## Concepts + +- `temporal-adjusters` + +## Prerequisites + +This exercise's prerequisites Concepts are: + +- `datetime`: creating and using `LocalDate` and `Month`. +- `enums`: working with enum constants such as `DayOfWeek.MONDAY`. + +## Analyzer + +This exercise could benefit from the following rules in the [analyzer]: + +- `essential`: If a solution computes dates by looping over days or by arithmetic with `plusDays()`/`minusDays()` instead of using `TemporalAdjusters`, instruct the student that adjusters are the idiomatic approach for this exercise and link to the `temporal-adjusters` concept. +- `actionable`: If `nextAvailable()` uses `next()` instead of `nextOrSame()`, point out that a date that already matches must be kept. +- `actionable`: If `payday()` or `bookkeepingDay()` spells out the weekend rule instead of reusing the `precedingWorkday()` adjuster, suggest reusing it. + +If the solution does not receive any of the above feedback, it must be exemplar. +Leave a `celebratory` comment to celebrate the success! + +[analyzer]: https://github.com/exercism/java-analyzer diff --git a/exercises/concept/beauty-salon-regulars/.meta/src/reference/java/SalonCalendar.java b/exercises/concept/beauty-salon-regulars/.meta/src/reference/java/SalonCalendar.java new file mode 100644 index 000000000..6462bf9ea --- /dev/null +++ b/exercises/concept/beauty-salon-regulars/.meta/src/reference/java/SalonCalendar.java @@ -0,0 +1,40 @@ +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.Month; +import java.time.temporal.TemporalAdjuster; +import java.time.temporal.TemporalAdjusters; + +class SalonCalendar { + + public LocalDate standingAppointment(int year, Month month, int occurrence, DayOfWeek dayOfWeek) { + return LocalDate.of(year, month, 1).with(TemporalAdjusters.dayOfWeekInMonth(occurrence, dayOfWeek)); + } + + public LocalDate lastAppointment(int year, Month month, DayOfWeek dayOfWeek) { + return LocalDate.of(year, month, 1).with(TemporalAdjusters.lastInMonth(dayOfWeek)); + } + + public LocalDate nextAvailable(LocalDate from, DayOfWeek dayOfWeek) { + return from.with(TemporalAdjusters.nextOrSame(dayOfWeek)); + } + + public LocalDate bookkeepingDay(int year, Month month) { + return LocalDate.of(year, month, 1) + .with(TemporalAdjusters.lastDayOfMonth()) + .with(precedingWorkday()); + } + + public LocalDate payday(int year, Month month) { + return LocalDate.of(year, month, 15).with(precedingWorkday()); + } + + public TemporalAdjuster precedingWorkday() { + return temporal -> { + DayOfWeek dayOfWeek = DayOfWeek.from(temporal); + if (dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY) { + return temporal.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY)); + } + return temporal; + }; + } +} diff --git a/exercises/concept/beauty-salon-regulars/build.gradle b/exercises/concept/beauty-salon-regulars/build.gradle new file mode 100644 index 000000000..dd3862eb9 --- /dev/null +++ b/exercises/concept/beauty-salon-regulars/build.gradle @@ -0,0 +1,25 @@ +plugins { + id "java" +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform("org.junit:junit-bom:5.10.0") + testImplementation "org.junit.jupiter:junit-jupiter" + testImplementation "org.assertj:assertj-core:3.25.1" + + testRuntimeOnly "org.junit.platform:junit-platform-launcher" +} + +test { + useJUnitPlatform() + + testLogging { + exceptionFormat = "full" + showStandardStreams = true + events = ["passed", "failed", "skipped"] + } +} diff --git a/exercises/concept/beauty-salon-regulars/gradle/wrapper/gradle-wrapper.jar b/exercises/concept/beauty-salon-regulars/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..b1b8ef56b Binary files /dev/null and b/exercises/concept/beauty-salon-regulars/gradle/wrapper/gradle-wrapper.jar differ diff --git a/exercises/concept/beauty-salon-regulars/gradle/wrapper/gradle-wrapper.properties b/exercises/concept/beauty-salon-regulars/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..6394b4647 --- /dev/null +++ b/exercises/concept/beauty-salon-regulars/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/exercises/concept/beauty-salon-regulars/gradlew b/exercises/concept/beauty-salon-regulars/gradlew new file mode 100755 index 000000000..b9bb139f7 --- /dev/null +++ b/exercises/concept/beauty-salon-regulars/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/exercises/concept/beauty-salon-regulars/gradlew.bat b/exercises/concept/beauty-salon-regulars/gradlew.bat new file mode 100644 index 000000000..24c62d56f --- /dev/null +++ b/exercises/concept/beauty-salon-regulars/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/exercises/concept/beauty-salon-regulars/src/main/java/SalonCalendar.java b/exercises/concept/beauty-salon-regulars/src/main/java/SalonCalendar.java new file mode 100644 index 000000000..929735c88 --- /dev/null +++ b/exercises/concept/beauty-salon-regulars/src/main/java/SalonCalendar.java @@ -0,0 +1,31 @@ +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.Month; +import java.time.temporal.TemporalAdjuster; + +class SalonCalendar { + + public LocalDate standingAppointment(int year, Month month, int occurrence, DayOfWeek dayOfWeek) { + throw new UnsupportedOperationException("Please implement the SalonCalendar.standingAppointment() method"); + } + + public LocalDate lastAppointment(int year, Month month, DayOfWeek dayOfWeek) { + throw new UnsupportedOperationException("Please implement the SalonCalendar.lastAppointment() method"); + } + + public LocalDate nextAvailable(LocalDate from, DayOfWeek dayOfWeek) { + throw new UnsupportedOperationException("Please implement the SalonCalendar.nextAvailable() method"); + } + + public LocalDate bookkeepingDay(int year, Month month) { + throw new UnsupportedOperationException("Please implement the SalonCalendar.bookkeepingDay() method"); + } + + public LocalDate payday(int year, Month month) { + throw new UnsupportedOperationException("Please implement the SalonCalendar.payday() method"); + } + + public TemporalAdjuster precedingWorkday() { + throw new UnsupportedOperationException("Please implement the SalonCalendar.precedingWorkday() method"); + } +} diff --git a/exercises/concept/beauty-salon-regulars/src/test/java/SalonCalendarTest.java b/exercises/concept/beauty-salon-regulars/src/test/java/SalonCalendarTest.java new file mode 100644 index 000000000..8b9657b32 --- /dev/null +++ b/exercises/concept/beauty-salon-regulars/src/test/java/SalonCalendarTest.java @@ -0,0 +1,199 @@ +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.Month; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SalonCalendarTest { + + private final SalonCalendar calendar = new SalonCalendar(); + + @Test + @Tag("task:1") + @DisplayName("Second Tuesday of August 2024") + public void testStandingAppointmentSecondTuesday() { + var expected = LocalDate.of(2024, 8, 13); + + assertThat(calendar.standingAppointment(2024, Month.AUGUST, 2, DayOfWeek.TUESDAY)).isEqualTo(expected); + } + + @Test + @Tag("task:1") + @DisplayName("First Monday of September 2024") + public void testStandingAppointmentFirstMonday() { + var expected = LocalDate.of(2024, 9, 2); + + assertThat(calendar.standingAppointment(2024, Month.SEPTEMBER, 1, DayOfWeek.MONDAY)).isEqualTo(expected); + } + + @Test + @Tag("task:1") + @DisplayName("Fourth Friday of November 2024") + public void testStandingAppointmentFourthFriday() { + var expected = LocalDate.of(2024, 11, 22); + + assertThat(calendar.standingAppointment(2024, Month.NOVEMBER, 4, DayOfWeek.FRIDAY)).isEqualTo(expected); + } + + @Test + @Tag("task:2") + @DisplayName("Last Friday of August 2024") + public void testLastAppointmentFriday() { + var expected = LocalDate.of(2024, 8, 30); + + assertThat(calendar.lastAppointment(2024, Month.AUGUST, DayOfWeek.FRIDAY)).isEqualTo(expected); + } + + @Test + @Tag("task:2") + @DisplayName("Last Monday of May 2024") + public void testLastAppointmentMonday() { + var expected = LocalDate.of(2024, 5, 27); + + assertThat(calendar.lastAppointment(2024, Month.MAY, DayOfWeek.MONDAY)).isEqualTo(expected); + } + + @Test + @Tag("task:2") + @DisplayName("Last Sunday of February in a leap year") + public void testLastAppointmentSundayInLeapFebruary() { + var expected = LocalDate.of(2024, 2, 25); + + assertThat(calendar.lastAppointment(2024, Month.FEBRUARY, DayOfWeek.SUNDAY)).isEqualTo(expected); + } + + @Test + @Tag("task:3") + @DisplayName("Next Saturday later in the same week") + public void testNextAvailableLaterInWeek() { + var thursday = LocalDate.of(2024, 8, 15); + var expected = LocalDate.of(2024, 8, 17); + + assertThat(calendar.nextAvailable(thursday, DayOfWeek.SATURDAY)).isEqualTo(expected); + } + + @Test + @Tag("task:3") + @DisplayName("Next Saturday when asked on a Saturday is the same day") + public void testNextAvailableSameDay() { + var saturday = LocalDate.of(2024, 8, 17); + + assertThat(calendar.nextAvailable(saturday, DayOfWeek.SATURDAY)).isEqualTo(saturday); + } + + @Test + @Tag("task:3") + @DisplayName("Next Saturday when asked on a Sunday is in the following week") + public void testNextAvailableNextWeek() { + var sunday = LocalDate.of(2024, 8, 18); + var expected = LocalDate.of(2024, 8, 24); + + assertThat(calendar.nextAvailable(sunday, DayOfWeek.SATURDAY)).isEqualTo(expected); + } + + @Test + @Tag("task:3") + @DisplayName("Next Monday crosses into the following month") + public void testNextAvailableCrossesMonth() { + var friday = LocalDate.of(2024, 8, 30); + var expected = LocalDate.of(2024, 9, 2); + + assertThat(calendar.nextAvailable(friday, DayOfWeek.MONDAY)).isEqualTo(expected); + } + + @Test + @Tag("task:4") + @DisplayName("Bookkeeping day is the last day of the month when it is a weekday") + public void testBookkeepingDayOnWeekday() { + var expected = LocalDate.of(2024, 9, 30); + + assertThat(calendar.bookkeepingDay(2024, Month.SEPTEMBER)).isEqualTo(expected); + } + + @Test + @Tag("task:4") + @DisplayName("Bookkeeping day moves to Friday when the month ends on a Saturday") + public void testBookkeepingDayOnSaturday() { + var expected = LocalDate.of(2024, 8, 30); + + assertThat(calendar.bookkeepingDay(2024, Month.AUGUST)).isEqualTo(expected); + } + + @Test + @Tag("task:4") + @DisplayName("Bookkeeping day moves to Friday when the month ends on a Sunday") + public void testBookkeepingDayOnSunday() { + var expected = LocalDate.of(2024, 6, 28); + + assertThat(calendar.bookkeepingDay(2024, Month.JUNE)).isEqualTo(expected); + } + + @Test + @Tag("task:4") + @DisplayName("Bookkeeping day in February of a leap year") + public void testBookkeepingDayInLeapFebruary() { + var expected = LocalDate.of(2024, 2, 29); + + assertThat(calendar.bookkeepingDay(2024, Month.FEBRUARY)).isEqualTo(expected); + } + + @Test + @Tag("task:5") + @DisplayName("Preceding workday adjuster keeps a weekday") + public void testPrecedingWorkdayKeepsWeekday() { + var thursday = LocalDate.of(2024, 8, 15); + + assertThat(thursday.with(calendar.precedingWorkday())).isEqualTo(thursday); + } + + @Test + @Tag("task:5") + @DisplayName("Preceding workday adjuster moves a Saturday to Friday") + public void testPrecedingWorkdayMovesSaturday() { + var saturday = LocalDate.of(2024, 6, 15); + var expected = LocalDate.of(2024, 6, 14); + + assertThat(saturday.with(calendar.precedingWorkday())).isEqualTo(expected); + } + + @Test + @Tag("task:5") + @DisplayName("Preceding workday adjuster moves a Sunday to Friday") + public void testPrecedingWorkdayMovesSunday() { + var sunday = LocalDate.of(2024, 9, 15); + var expected = LocalDate.of(2024, 9, 13); + + assertThat(sunday.with(calendar.precedingWorkday())).isEqualTo(expected); + } + + @Test + @Tag("task:5") + @DisplayName("Payday is the 15th when it is a weekday") + public void testPaydayOnWeekday() { + var expected = LocalDate.of(2024, 8, 15); + + assertThat(calendar.payday(2024, Month.AUGUST)).isEqualTo(expected); + } + + @Test + @Tag("task:5") + @DisplayName("Payday moves to Friday when the 15th is a Saturday") + public void testPaydayOnSaturday() { + var expected = LocalDate.of(2024, 6, 14); + + assertThat(calendar.payday(2024, Month.JUNE)).isEqualTo(expected); + } + + @Test + @Tag("task:5") + @DisplayName("Payday moves to Friday when the 15th is a Sunday") + public void testPaydayOnSunday() { + var expected = LocalDate.of(2024, 9, 13); + + assertThat(calendar.payday(2024, Month.SEPTEMBER)).isEqualTo(expected); + } +} diff --git a/exercises/settings.gradle b/exercises/settings.gradle index 28f6eca2d..019a3ee94 100644 --- a/exercises/settings.gradle +++ b/exercises/settings.gradle @@ -2,6 +2,7 @@ rootProject.name = 'exercism-java' // concept exercises include 'concept:annalyns-infiltration' +include 'concept:beauty-salon-regulars' include 'concept:bird-watcher' // include 'concept:blackjack' // deprecated include 'concept:booking-up-for-beauty'