Skip to content

Commit 6b47f1c

Browse files
committed
#276: Consider interruption in mandatory break
1 parent e8a9e42 commit 6b47f1c

File tree

9 files changed

+131
-31
lines changed

9 files changed

+131
-31
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
See [Release](https://github.com/itsallcode/white-rabbit/releases/tag/v1.10.0) / [Milestone](https://github.com/itsallcode/white-rabbit/milestone/12?closed=1)
1010

11-
## [1.9.0] - 2022-??-??
11+
## [1.9.0] - 2024-??-??
1212

1313
See [Release](https://github.com/itsallcode/white-rabbit/releases/tag/v1.9.0) / [Milestone](https://github.com/itsallcode/white-rabbit/milestone/11?closed=1)
1414

@@ -22,7 +22,8 @@ This release requires Java 21.
2222

2323
### New Features
2424

25-
* [#273](https://github.com/itsallcode/white-rabbit/pull/273): Added buttons to monthly report for jumping to the previous/next month
25+
* [#273](https://github.com/itsallcode/white-rabbit/pull/273): Added buttons to monthly report for jumping to the previous/next month.
26+
* [#276](https://github.com/itsallcode/white-rabbit/pull/276): Added config option `reduce_mandatory_break_by_interruption`.
2627

2728
### Bugfixes
2829

docs/user_guide.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,12 @@ Restart WhiteRabbit after changing the configuration file.
6969
* We recommend to configure this when starting to use WhiteRabbit.
7070
* `mandatory_break`: mandatory break per day (default: 45 minutes). Format: see [below](#duration-format). Caution: This setting will also affect the past, i.e. the overtime of **all** days will be re-calculated.
7171
* We recommend to configure this when starting to use WhiteRabbit.
72+
* `reduce_mandatory_break_by_interruption`: Reduce the mandatory break by entered interruption (`true` or `false`, default: `false`). If this is `true`, the mandatory break will be set to zero when the interruption is longer than the mandatory break.
73+
* We recommend to configure this when starting to use WhiteRabbit.
7274

7375
#### Duration format
7476

75-
Enter duration values in the format used by [Duration.parse()](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)). Examples:
77+
Enter duration values in the format used by [Duration.parse()](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)). Examples:
7678

7779
* `PT5H`: 5 hours
7880
* `PT5H30M`: 5 hours and 30 minutes
@@ -221,4 +223,4 @@ The default values are:
221223
csv.destination = $HOME
222224
csv.separator = ","
223225
csv.filter_for_weekdays = False
224-
```
226+
```

logic/src/main/java/org/itsallcode/whiterabbit/logic/Config.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public interface Config
2121

2222
Optional<Duration> getMandatoryBreak();
2323

24+
boolean reduceMandatoryBreakByInterruption();
25+
2426
default Path getProjectFile()
2527
{
2628
return getDataDir().resolve(PROJECTS_JSON);

logic/src/main/java/org/itsallcode/whiterabbit/logic/ConfigFile.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ public Optional<Duration> getMandatoryBreak()
7979
return getOptionalValue("mandatory_break").map(Duration::parse);
8080
}
8181

82+
@Override
83+
public boolean reduceMandatoryBreakByInterruption()
84+
{
85+
return getOptionalValue("reduce_mandatory_break_by_interruption").map(Boolean::valueOf).orElse(false);
86+
}
87+
8288
@Override
8389
public boolean allowMultipleInstances()
8490
{

logic/src/main/java/org/itsallcode/whiterabbit/logic/service/contract/ContractTermsService.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ public ContractTermsService(final Config config)
1818
this.config = config;
1919
}
2020

21-
2221
public Duration getMandatoryBreak(final DayRecord day)
2322
{
2423
if (!day.getType().isWorkDay())
@@ -28,7 +27,15 @@ public Duration getMandatoryBreak(final DayRecord day)
2827
final Duration workingTime = day.getRawWorkingTime().minus(day.getInterruption());
2928
if (workingTime.compareTo(MIN_WORKING_TIME_WITHOUT_BREAK) > 0)
3029
{
31-
return getMandatoryBreak();
30+
Duration mandatoryBreak = getMandatoryBreak();
31+
if (config.reduceMandatoryBreakByInterruption())
32+
{
33+
mandatoryBreak = mandatoryBreak.minus(day.getInterruption());
34+
}
35+
if (mandatoryBreak.isPositive())
36+
{
37+
return mandatoryBreak;
38+
}
3239
}
3340
return Duration.ZERO;
3441
}

logic/src/test/java/org/itsallcode/whiterabbit/logic/ConfigFileTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,27 @@ void getMandatoryBreak_returnsCustomValue()
114114
assertThat(configFile.getMandatoryBreak()).isPresent().hasValue(Duration.ofMinutes(0));
115115
}
116116

117+
@Test
118+
void reduceMandatoryBreakByInterruption_returnsDefault()
119+
{
120+
when(propertiesMock.getProperty("reduce_mandatory_break_by_interruption")).thenReturn(null);
121+
assertThat(configFile.reduceMandatoryBreakByInterruption()).isFalse();
122+
}
123+
124+
@Test
125+
void reduceMandatoryBreakByInterruption_returnsCustomValue()
126+
{
127+
when(propertiesMock.getProperty("reduce_mandatory_break_by_interruption")).thenReturn("true");
128+
assertThat(configFile.reduceMandatoryBreakByInterruption()).isTrue();
129+
}
130+
131+
@Test
132+
void reduceMandatoryBreakByInterruption_invalidValue()
133+
{
134+
when(propertiesMock.getProperty("reduce_mandatory_break_by_interruption")).thenReturn("invalid");
135+
assertThat(configFile.reduceMandatoryBreakByInterruption()).isFalse();
136+
}
137+
117138
@Test
118139
void getLocale_returnsDefault()
119140
{

logic/src/test/java/org/itsallcode/whiterabbit/logic/ConfigTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ public Optional<Duration> getMandatoryBreak()
5959
return Optional.empty();
6060
}
6161

62+
@Override
63+
public boolean reduceMandatoryBreakByInterruption()
64+
{
65+
return false;
66+
}
67+
6268
@Override
6369
public boolean allowMultipleInstances()
6470
{

logic/src/test/java/org/itsallcode/whiterabbit/logic/model/DayRecordTest.java

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import org.itsallcode.whiterabbit.logic.service.project.ProjectService;
1717
import org.itsallcode.whiterabbit.logic.storage.data.JsonModelFactory;
1818
import org.itsallcode.whiterabbit.logic.test.TestingConfig;
19+
import org.itsallcode.whiterabbit.logic.test.TestingConfig.Builder;
20+
import org.junit.jupiter.api.BeforeEach;
1921
import org.junit.jupiter.api.Test;
2022
import org.junit.jupiter.api.extension.ExtendWith;
2123
import org.mockito.Mock;
@@ -27,6 +29,13 @@ class DayRecordTest
2729
@Mock
2830
private ProjectService projectServiceMock;
2931
private final ModelFactory modelFactory = new JsonModelFactory();
32+
private Builder configBuilder;
33+
34+
@BeforeEach
35+
void createDefaultConfig()
36+
{
37+
configBuilder = TestingConfig.builder();
38+
}
3039

3140
@Test
3241
void mandatoryWorkingTimeIsZeroOnWeekend()
@@ -119,6 +128,30 @@ void testMandatoryBreakConsidersInterruptionLessThan6hours()
119128
Duration.ZERO);
120129
}
121130

131+
@Test
132+
void testMandatoryBreakReducedByInterruption()
133+
{
134+
configBuilder.withReduceMandatoryBreakByInterruption(true);
135+
assertMandatoryBreak(LocalDate.of(2018, 10, 1), LocalTime.of(8, 0), LocalTime.of(18, 0), Duration.ofMinutes(10),
136+
Duration.ofMinutes(35));
137+
}
138+
139+
@Test
140+
void testMandatoryBreakNotReducedByInterruptionWhenWorkingLessThan6h()
141+
{
142+
configBuilder.withReduceMandatoryBreakByInterruption(true);
143+
assertMandatoryBreak(LocalDate.of(2018, 10, 1), LocalTime.of(8, 0), LocalTime.of(13, 0), Duration.ofMinutes(10),
144+
Duration.ZERO);
145+
}
146+
147+
@Test
148+
void testMandatoryBreakZeroForLongerInterruption()
149+
{
150+
configBuilder.withReduceMandatoryBreakByInterruption(true);
151+
assertMandatoryBreak(LocalDate.of(2018, 10, 1), LocalTime.of(8, 0), LocalTime.of(18, 0), Duration.ofMinutes(50),
152+
Duration.ZERO);
153+
}
154+
122155
@Test
123156
void testWorkingTime1h()
124157
{
@@ -507,17 +540,17 @@ void dayWithActivitiesListIsNonDummy()
507540
assertNonDummyDay(day);
508541
}
509542

510-
private void assertDummyDay(DayRecord day)
543+
private void assertDummyDay(final DayRecord day)
511544
{
512545
assertThat(day.isDummyDay()).as("dummy").isTrue();
513546
}
514547

515-
private void assertNonDummyDay(DayRecord day)
548+
private void assertNonDummyDay(final DayRecord day)
516549
{
517550
assertThat(day.isDummyDay()).as("dummy").isFalse();
518551
}
519552

520-
private MonthIndex month(LocalDate date, Duration overtimePreviousMonth, DayData... days)
553+
private MonthIndex month(final LocalDate date, final Duration overtimePreviousMonth, final DayData... days)
521554
{
522555
final MonthData jsonMonth = modelFactory.createMonthData();
523556
jsonMonth.setDays(asList(days));
@@ -527,59 +560,63 @@ private MonthIndex month(LocalDate date, Duration overtimePreviousMonth, DayData
527560
return MonthIndex.create(contractTerms(), projectServiceMock, modelFactory, jsonMonth);
528561
}
529562

530-
private void assertOvertime(LocalDate date, LocalTime begin, LocalTime end, Duration expectedOvertime)
563+
private void assertOvertime(final LocalDate date, final LocalTime begin, final LocalTime end,
564+
final Duration expectedOvertime)
531565
{
532566
final DayRecord day = createDay(date, begin, end, null, null);
533567
assertThat(day.getOvertime()).as("overtime").isEqualTo(expectedOvertime);
534568
}
535569

536-
private void assertMandatoryBreak(LocalDate date, LocalTime begin, LocalTime end, Duration expectedDuration)
570+
private void assertMandatoryBreak(final LocalDate date, final LocalTime begin, final LocalTime end,
571+
final Duration expectedDuration)
537572
{
538573
assertMandatoryBreak(date, begin, end, Duration.ZERO, expectedDuration);
539574
}
540575

541-
private void assertMandatoryBreak(LocalDate date, LocalTime begin, LocalTime end, Duration interruption,
542-
Duration expectedDuration)
576+
private void assertMandatoryBreak(final LocalDate date, final LocalTime begin, final LocalTime end,
577+
final Duration interruption,
578+
final Duration expectedDuration)
543579
{
544580
assertThat(getMandatoryBreak(date, begin, end, interruption)).as("mandatory break").isEqualTo(expectedDuration);
545581
}
546582

547-
private void assertMandatoryWorkingTime(LocalDate date, LocalTime begin, LocalTime end,
548-
Duration expectedMandatoryWorkingTime)
583+
private void assertMandatoryWorkingTime(final LocalDate date, final LocalTime begin, final LocalTime end,
584+
final Duration expectedMandatoryWorkingTime)
549585
{
550586
assertThat(getMandatoryWorkingTime(date, begin, end)).as("mandatory working time")
551587
.isEqualTo(expectedMandatoryWorkingTime);
552588
}
553589

554-
private Duration getMandatoryWorkingTime(LocalDate date, LocalTime begin, LocalTime end)
590+
private Duration getMandatoryWorkingTime(final LocalDate date, final LocalTime begin, final LocalTime end)
555591
{
556592
final DayRecord day = createDay(date, begin, end, null, null);
557593
return day.getMandatoryWorkingTime();
558594
}
559595

560-
private Duration getMandatoryBreak(LocalDate date, LocalTime begin, LocalTime end, Duration interruption)
596+
private Duration getMandatoryBreak(final LocalDate date, final LocalTime begin, final LocalTime end,
597+
final Duration interruption)
561598
{
562599
final DayRecord day = createDay(date, begin, end, null, interruption);
563600
return day.getMandatoryBreak();
564601
}
565602

566-
private void assertWorkingDay(LocalDate date, boolean expected)
603+
private void assertWorkingDay(final LocalDate date, final boolean expected)
567604
{
568605
assertThat(createDay(date).getType().isWorkDay()).isEqualTo(expected);
569606
}
570607

571-
private void assertType(LocalDate date, DayType expected)
608+
private void assertType(final LocalDate date, final DayType expected)
572609
{
573610
final DayRecord day = createDay(date);
574611
assertDayType(day, expected);
575612
}
576613

577-
private void assertDayType(DayRecord day, DayType expected)
614+
private void assertDayType(final DayRecord day, final DayType expected)
578615
{
579616
assertThat(day.getType()).isEqualTo(expected);
580617
}
581618

582-
private DayRecord createDay(LocalDate date)
619+
private DayRecord createDay(final LocalDate date)
583620
{
584621
return createDay(date, null, null);
585622
}
@@ -589,29 +626,32 @@ private DayRecord createDummyDay()
589626
return createDummyDay(LocalDate.of(2021, 7, 20));
590627
}
591628

592-
private DayRecord createDummyDay(LocalDate date)
629+
private DayRecord createDummyDay(final LocalDate date)
593630
{
594631
return createDay(date);
595632
}
596633

597-
private DayRecord createDay(LocalDate date, LocalTime begin, LocalTime end)
634+
private DayRecord createDay(final LocalDate date, final LocalTime begin, final LocalTime end)
598635
{
599636
return createDay(date, begin, end, null, null);
600637
}
601638

602-
private DayRecord createDay(LocalDate date, LocalTime begin, LocalTime end, DayType type, Duration interruption)
639+
private DayRecord createDay(final LocalDate date, final LocalTime begin, final LocalTime end, final DayType type,
640+
final Duration interruption)
603641
{
604642
return createDay(date, begin, end, type, interruption, null);
605643
}
606644

607-
private DayRecord createDay(LocalDate date, LocalTime begin, LocalTime end, DayType type, Duration interruption,
608-
DayRecord previousDay)
645+
private DayRecord createDay(final LocalDate date, final LocalTime begin, final LocalTime end, final DayType type,
646+
final Duration interruption,
647+
final DayRecord previousDay)
609648
{
610649
return createDay(date, begin, end, type, interruption, previousDay, null);
611650
}
612651

613-
private DayRecord createDay(LocalDate date, LocalTime begin, LocalTime end, DayType type, Duration interruption,
614-
DayRecord previousDay, MonthIndex month)
652+
private DayRecord createDay(final LocalDate date, final LocalTime begin, final LocalTime end, final DayType type,
653+
final Duration interruption,
654+
final DayRecord previousDay, final MonthIndex month)
615655
{
616656
final DayData day = modelFactory.createDayData();
617657
day.setBegin(begin);
@@ -628,14 +668,14 @@ private DayRecord createDay(final DayData DayData)
628668
return new DayRecord(null, DayData, null, null, projectServiceMock, modelFactory);
629669
}
630670

631-
private DayRecord dayRecord(DayData day, DayRecord previousDay, MonthIndex month)
671+
private DayRecord dayRecord(final DayData day, final DayRecord previousDay, final MonthIndex month)
632672
{
633673
final ContractTermsService contractTerms = contractTerms();
634674
return new DayRecord(contractTerms, day, previousDay, month, projectServiceMock, modelFactory);
635675
}
636676

637677
private ContractTermsService contractTerms()
638678
{
639-
return new ContractTermsService(TestingConfig.builder().build());
679+
return new ContractTermsService(configBuilder.build());
640680
}
641681
}

logic/src/test/java/org/itsallcode/whiterabbit/logic/test/TestingConfig.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ public class TestingConfig implements Config
1313
private final Locale locale;
1414
private final Duration currentHoursPerDay;
1515
private final Duration mandatoryBreak;
16+
private final boolean reduceMandatoryBreakByInterruption;
1617

1718
private TestingConfig(final Builder builder)
1819
{
1920
this.dataDir = builder.dataDir;
2021
this.locale = builder.locale;
2122
this.currentHoursPerDay = builder.currentHoursPerDay;
2223
this.mandatoryBreak = builder.mandatoryBreak;
24+
this.reduceMandatoryBreakByInterruption = builder.reduceMandatoryBreakByInterruption;
2325
}
2426

2527
@Override
@@ -46,6 +48,12 @@ public Optional<Duration> getMandatoryBreak()
4648
return Optional.ofNullable(mandatoryBreak);
4749
}
4850

51+
@Override
52+
public boolean reduceMandatoryBreakByInterruption()
53+
{
54+
return reduceMandatoryBreakByInterruption;
55+
}
56+
4957
@Override
5058
public boolean allowMultipleInstances()
5159
{
@@ -77,7 +85,8 @@ public static Builder builder()
7785

7886
public static final class Builder
7987
{
80-
public Duration mandatoryBreak;
88+
private boolean reduceMandatoryBreakByInterruption = false;
89+
private Duration mandatoryBreak;
8190
private Path dataDir;
8291
private Locale locale;
8392
private Duration currentHoursPerDay;
@@ -110,6 +119,12 @@ public Builder withMandatoryBreak(final Duration mandatoryBreak)
110119
return this;
111120
}
112121

122+
public Builder withReduceMandatoryBreakByInterruption(final boolean reduceMandatoryBreakByInterruption)
123+
{
124+
this.reduceMandatoryBreakByInterruption = reduceMandatoryBreakByInterruption;
125+
return this;
126+
}
127+
113128
public TestingConfig build()
114129
{
115130
return new TestingConfig(this);

0 commit comments

Comments
 (0)