Skip to content

Commit

Permalink
fix calculation of calendar weeks
Browse files Browse the repository at this point in the history
TimeEntryWeek `int week()` uses hard coded Locale.GERMANY because unit test is only green with this locale.
it does not work with Locale.forLanguageTag()... I don't get it...

therefore using hard coded locale to fix this issue...
we may have to have a look at this in the future?
  • Loading branch information
bseber authored and derTobsch committed Jan 6, 2025
1 parent f05ad5f commit 4dfb71f
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 160 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import java.math.BigDecimal;
import java.math.MathContext;
import java.time.Clock;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDate;
Expand All @@ -32,7 +33,6 @@
import java.time.Year;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.WeekFields;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
Expand All @@ -52,21 +52,24 @@ class TimeEntryController implements HasTimeClock, HasLaunchpad {
private final TimeEntryService timeEntryService;
private final UserSettingsProvider userSettingsProvider;
private final DateFormatter dateFormatter;
private final Clock clock;

public TimeEntryController(TimeEntryService timeEntryService, UserSettingsProvider userSettingsProvider, DateFormatter dateFormatter) {
public TimeEntryController(TimeEntryService timeEntryService, UserSettingsProvider userSettingsProvider,
DateFormatter dateFormatter, Clock clock) {
this.timeEntryService = timeEntryService;
this.userSettingsProvider = userSettingsProvider;
this.dateFormatter = dateFormatter;
this.clock = clock;
}

@GetMapping("/timeentries")
public String items(Model model, Locale locale, @AuthenticationPrincipal OidcUser principal) {

final ZoneId userZoneId = userSettingsProvider.zoneId();
final LocalDate nowUserAware = LocalDate.now(userZoneId);
final LocalDate now = LocalDate.now(clock);
final ZonedDateTime userStartOfDay = now.atStartOfDay(userSettingsProvider.zoneId());

final int year = nowUserAware.getYear();
final int weekOfYear = nowUserAware.get(WEEK_OF_WEEK_BASED_YEAR);
final int year = userStartOfDay.getYear();
final int weekOfYear = userStartOfDay.get(WEEK_OF_WEEK_BASED_YEAR);

return prepareTimeEntriesView(year, weekOfYear, model, principal, locale);
}
Expand Down Expand Up @@ -107,7 +110,7 @@ public String save(
final int weekOfYear;

try {
final LocalDate firstDateOfWeek = localDateToFirstDateOfWeek(timeEntryDTO.getDate(), DayOfWeek.MONDAY);
final LocalDate firstDateOfWeek = localDateToFirstDateOfWeek(timeEntryDTO.getDate());
year = firstDateOfWeek.getYear();
weekOfYear = firstDateOfWeek.get(WEEK_OF_WEEK_BASED_YEAR);
} catch(NullPointerException exception) {
Expand Down Expand Up @@ -138,7 +141,7 @@ public ModelAndView update(
final int weekOfYear;

try {
final LocalDate firstDateOfWeek = localDateToFirstDateOfWeek(timeEntryDTO.getDate(), DayOfWeek.MONDAY);
final LocalDate firstDateOfWeek = localDateToFirstDateOfWeek(timeEntryDTO.getDate());
year = firstDateOfWeek.getYear();
weekOfYear = firstDateOfWeek.get(WEEK_OF_WEEK_BASED_YEAR);
} catch(NullPointerException exception) {
Expand Down Expand Up @@ -440,25 +443,19 @@ private static String toTimeEntryDTODurationString(Duration duration) {
}

private int lastWeekOfYear(int year) {
return Year.of(year).atMonth(DECEMBER).atEndOfMonth().get(WEEK_OF_WEEK_BASED_YEAR);
}

private LocalDate localDateToFirstDateOfWeek(LocalDate localDate, DayOfWeek firstDayOfWeek) {
// using minimalDaysInFirstWeek = 4 since it is defined by ISO-8601 (starting week with monday)
// I have no glue whether this value can be use here or not. Some unit tests say we can... so...
final WeekFields userWeekFields = WeekFields.of(firstDayOfWeek, 4);

final int temporalYear = localDate.getYear();
final int temporalWeekOfYear = localDate.get(WEEK_OF_WEEK_BASED_YEAR);

final LocalDate previousOrSame = localDate.with(previousOrSame(firstDayOfWeek));

final int year = previousOrSame.getYear();
final int week = previousOrSame.get(userWeekFields.weekOfWeekBasedYear());
if (year == temporalYear && week > temporalWeekOfYear) {
return previousOrSame.minusWeeks(1);
final LocalDate date = Year.of(year).atMonth(DECEMBER).atEndOfMonth();
final int week = date.get(WEEK_OF_WEEK_BASED_YEAR);
if (week == 1) {
// last week cannot be the first one of a year :-)
// e.g. 2024-12-31 is calWeek 1 (of next year) -> wee need calWeek 52 2024-12-23 to 2024-12-29
return date.minusWeeks(1).get(WEEK_OF_WEEK_BASED_YEAR);
} else {
return week;
}
}

return previousOrSame;
private LocalDate localDateToFirstDateOfWeek(LocalDate localDate) {
final DayOfWeek firstDayOfWeek = userSettingsProvider.firstDayOfWeek();
return localDate.with(previousOrSame(firstDayOfWeek));
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package de.focusshift.zeiterfassung.timeentry;

import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours;
import org.threeten.extra.YearWeek;

import java.time.Duration;
import java.time.LocalDate;
import java.time.Year;
import java.time.temporal.WeekFields;
import java.util.Collection;
import java.util.List;

import static java.time.Month.DECEMBER;
import static java.time.temporal.WeekFields.ISO;
import static java.util.Locale.GERMANY;
import static java.util.function.Predicate.not;

/**
* Represents a week of time entries.
*
* @param firstDateOfWeek the first date of the week, maybe monday or sunday.
* @param plannedWorkingHours {@link PlannedWorkingHours} for this week
* @param days sorted list of days
*/
record TimeEntryWeek(
LocalDate firstDateOfWeek,
PlannedWorkingHours plannedWorkingHours,
Expand Down Expand Up @@ -46,21 +51,10 @@ public WorkDuration workDuration() {
.reduce(WorkDuration.ZERO, WorkDuration::plus);
}

public int year() {
return firstDateOfWeek.getYear();
}

public int week() {
final int isoWeekOfYear = firstDateOfWeek.get(ISO.weekOfYear());
if (isoWeekOfYear != 0) {
return isoWeekOfYear;
}

final Year year = Year.of(firstDateOfWeek.getYear());
final Year previousYear = year.minusYears(1);
final LocalDate endOfDecember = previousYear.atMonth(DECEMBER).atEndOfMonth();
final int week = endOfDecember.get(ISO.weekOfYear());
return YearWeek.of(previousYear, week).getWeek();
final int minimalDaysInFirstWeek = WeekFields.of(GERMANY).getMinimalDaysInFirstWeek();
final WeekFields weekFields = WeekFields.of(firstDateOfWeek.getDayOfWeek(), minimalDaysInFirstWeek);
return firstDateOfWeek.get(weekFields.weekOfWeekBasedYear());
}

public LocalDate lastDateOfWeek() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.stereotype.Service;
import org.threeten.extra.YearWeek;

import java.time.Clock;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Year;
Expand All @@ -11,14 +12,17 @@

import static java.time.temporal.IsoFields.WEEK_OF_WEEK_BASED_YEAR;
import static java.time.temporal.TemporalAdjusters.previousOrSame;
import static java.util.Locale.GERMANY;

@Service
class UserDateServiceImpl implements UserDateService {

private final UserSettingsProvider userSettingsProvider;
private final Clock clock;

public UserDateServiceImpl(UserSettingsProvider userSettingsProvider) {
public UserDateServiceImpl(UserSettingsProvider userSettingsProvider, Clock clock) {
this.userSettingsProvider = userSettingsProvider;
this.clock = clock;
}

@Override
Expand All @@ -35,15 +39,18 @@ public LocalDate localDateToFirstDateOfWeek(LocalDate localDate) {

@Override
public LocalDate firstDayOfWeek(Year year, int weekOfYear) {

// `YearWeek.of` throws when the weekOfYear is invalid
final YearWeek yearWeek = YearWeek.of(year, weekOfYear);
YearWeek.of(year, weekOfYear);

final int minimalDaysInFirstWeek = WeekFields.of(GERMANY).getMinimalDaysInFirstWeek();
final DayOfWeek usersFirstDayOfWeek = userSettingsProvider.firstDayOfWeek();
final WeekFields weekFields = WeekFields.of(usersFirstDayOfWeek, minimalDaysInFirstWeek);

return LocalDate.now()
.withYear(yearWeek.getYear())
.with(usersFirstDayOfWeek)
.with(WEEK_OF_WEEK_BASED_YEAR, weekOfYear);
return LocalDate.now(clock)
.withYear(year.getValue())
.with(weekFields.weekOfWeekBasedYear(), weekOfYear)
.with(previousOrSame(usersFirstDayOfWeek));
}

private LocalDate localDateToFirstDateOfWeek(LocalDate localDate, DayOfWeek firstDayOfWeek) {
Expand Down
Loading

0 comments on commit 4dfb71f

Please sign in to comment.