Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2978 (2024-07-22 05:50:54)

refactor: create `Occurrence` class
# Types of changes

Internal changes:

- [x] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)

## Description

- Extract occurrence-related data to from `Reccurrence` to `Occurrence` class
  - More PRs will follow (I already have the changes in my local repos, submitting slowly)

## Motivation and Context

- Separate concerns in `Recurrence` - the recurring part itself and the dates (occurrence)
- Better code readability

## How has this been tested?

- Unit tests

## Checklist

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

- [x] My contribution follows this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/src/Suggestor/Suggestor.ts b/src/Suggestor/Suggestor.ts
index 7fac1c18..c3cce272 100644
--- a/src/Suggestor/Suggestor.ts
+++ b/src/Suggestor/Suggestor.ts
@@ -2,7 +2,7 @@ import type { Editor, EditorPosition } from 'obsidian';
 import type { Settings } from '../Config/Settings';
 import { DateParser } from '../Query/DateParser';
 import { doAutocomplete } from '../lib/DateAbbreviations';
-import { Recurrence } from '../Task/Recurrence';
+import { Occurrence, Recurrence } from '../Task/Recurrence';
 import {
     type DefaultTaskSerializerSymbols,
     allTaskPluginEmojis,
@@ -381,9 +381,11 @@ function addRecurrenceSuggestions(
             // We also add a nice checkmark in this case to denote it's a complete valid recurrence description
             const parsedRecurrence = Recurrence.fromText({
                 recurrenceRuleText: recurrenceString,
-                startDate: null,
-                scheduledDate: null,
-                dueDate: null,
+                occurrence: new Occurrence({
+                    startDate: null,
+                    scheduledDate: null,
+                    dueDate: null,
+                }),
             })?.toText();
             if (parsedRecurrence) {
                 const appendedText = `${recurrencePrefix} ${parsedRecurrence}` + postfix;
diff --git a/src/Task/Recurrence.ts b/src/Task/Recurrence.ts
index 2b08f10b..d3b2440c 100644
--- a/src/Task/Recurrence.ts
+++ b/src/Task/Recurrence.ts
@@ -5,12 +5,30 @@ import type { Moment } from 'moment';
 import { RRule } from 'rrule';
 import { compareByDate } from '../lib/DateTools';
 
+export class Occurrence {
+    readonly startDate: Moment | null;
+    readonly scheduledDate: Moment | null;
+    readonly dueDate: Moment | null;
+
+    constructor({
+        startDate,
+        scheduledDate,
+        dueDate,
+    }: {
+        startDate: Moment | null;
+        scheduledDate: Moment | null;
+        dueDate: Moment | null;
+    }) {
+        this.startDate = startDate;
+        this.scheduledDate = scheduledDate;
+        this.dueDate = dueDate;
+    }
+}
+
 export class Recurrence {
     private readonly rrule: RRule;
     private readonly baseOnToday: boolean;
-    private readonly startDate: Moment | null;
-    private readonly scheduledDate: Moment | null;
-    private readonly dueDate: Moment | null;
+    readonly occurrence: Occurrence;
 
     /**
      * The reference date is used to calculate future occurrences.
@@ -31,35 +49,25 @@ export class Recurrence {
         rrule,
         baseOnToday,
         referenceDate,
-        startDate,
-        scheduledDate,
-        dueDate,
+        occurrence,
     }: {
         rrule: RRule;
         baseOnToday: boolean;
         referenceDate: Moment | null;
-        startDate: Moment | null;
-        scheduledDate: Moment | null;
-        dueDate: Moment | null;
+        occurrence: Occurrence;
     }) {
         this.rrule = rrule;
         this.baseOnToday = baseOnToday;
         this.referenceDate = referenceDate;
-        this.startDate = startDate;
-        this.scheduledDate = scheduledDate;
-        this.dueDate = dueDate;
+        this.occurrence = occurrence;
     }
 
     public static fromText({
         recurrenceRuleText,
-        startDate,
-        scheduledDate,
-        dueDate,
+        occurrence,
     }: {
         recurrenceRuleText: string;
-        startDate: Moment | null;
-        scheduledDate: Moment | null;
-        dueDate: Moment | null;
+        occurrence: Occurrence;
     }): Recurrence | null {
         try {
             const match = recurrenceRuleText.match(/^([a-zA-Z0-9, !]+?)( when done)?$/i);
@@ -76,12 +84,12 @@ export class Recurrence {
                 // Assuming due date has the highest priority.
                 let referenceDate: Moment | null = null;
                 // Clone the moment objects.
-                if (dueDate) {
-                    referenceDate = window.moment(dueDate);
-                } else if (scheduledDate) {
-                    referenceDate = window.moment(scheduledDate);
-                } else if (startDate) {
-                    referenceDate = window.moment(startDate);
+                if (occurrence.dueDate) {
+                    referenceDate = window.moment(occurrence.dueDate);
+                } else if (occurrence.scheduledDate) {
+                    referenceDate = window.moment(occurrence.scheduledDate);
+                } else if (occurrence.startDate) {
+                    referenceDate = window.moment(occurrence.startDate);
                 }
 
                 if (!baseOnToday && referenceDate !== null) {
@@ -95,9 +103,7 @@ export class Recurrence {
                     rrule,
                     baseOnToday,
                     referenceDate,
-                    startDate,
-                    scheduledDate,
-                    dueDate,
+                    occurrence,
                 });
             }
         } catch (e) {
@@ -147,9 +153,9 @@ export class Recurrence {
         }
 
         return {
-            startDate: this.nextOccurrence(next, this.startDate),
-            scheduledDate: this.nextOccurrence(next, this.scheduledDate),
-            dueDate: this.nextOccurrence(next, this.dueDate),
+            startDate: this.nextOccurrence(next, this.occurrence.startDate),
+            scheduledDate: this.nextOccurrence(next, this.occurrence.scheduledDate),
+            dueDate: this.nextOccurrence(next, this.occurrence.dueDate),
         };
     }
 
@@ -181,13 +187,13 @@ export class Recurrence {
         }
 
         // Compare Date fields
-        if (compareByDate(this.startDate, other.startDate) !== 0) {
+        if (compareByDate(this.occurrence.startDate, other.occurrence.startDate) !== 0) {
             return false;
         }
-        if (compareByDate(this.scheduledDate, other.scheduledDate) !== 0) {
+        if (compareByDate(this.occurrence.scheduledDate, other.occurrence.scheduledDate) !== 0) {
             return false;
         }
-        if (compareByDate(this.dueDate, other.dueDate) !== 0) {
+        if (compareByDate(this.occurrence.dueDate, other.occurrence.dueDate) !== 0) {
             return false;
         }
 
diff --git a/src/TaskSerializer/DefaultTaskSerializer.ts b/src/TaskSerializer/DefaultTaskSerializer.ts
index 46e9361c..b66ff8ba 100644
--- a/src/TaskSerializer/DefaultTaskSerializer.ts
+++ b/src/TaskSerializer/DefaultTaskSerializer.ts
@@ -1,6 +1,6 @@
 import type { Moment } from 'moment';
 import { TaskLayoutComponent, TaskLayoutOptions } from '../Layout/TaskLayoutOptions';
-import { Recurrence } from '../Task/Recurrence';
+import { Occurrence, Recurrence } from '../Task/Recurrence';
 import { Task } from '../Task/Task';
 import { Priority } from '../Task/Priority';
 import { TaskRegularExpressions } from '../Task/TaskRegularExpressions';
@@ -367,9 +367,7 @@ export class DefaultTaskSerializer implements TaskSerializer {
         if (recurrenceRule.length > 0) {
             recurrence = Recurrence.fromText({
                 recurrenceRuleText: recurrenceRule,
-                startDate,
-                scheduledDate,
-                dueDate,
+                occurrence: new Occurrence({ startDate, scheduledDate, dueDate }),
             });
         }
         // Add back any trailing tags to the description. We removed them so we can parse the rest of the
diff --git a/src/ui/EditableTask.ts b/src/ui/EditableTask.ts
index 217bb502..0e384564 100644
--- a/src/ui/EditableTask.ts
+++ b/src/ui/EditableTask.ts
@@ -3,7 +3,7 @@ import { parseTypedDateForSaving } from '../lib/DateTools';
 import { replaceTaskWithTasks } from '../Obsidian/File';
 import type { Status } from '../Statuses/Status';
 import { Priority } from '../Task/Priority';
-import { Recurrence } from '../Task/Recurrence';
+import { Occurrence, Recurrence } from '../Task/Recurrence';
 import { Task } from '../Task/Task';
 import { addDependencyToParent, ensureTaskHasId, generateUniqueId, removeDependency } from '../Task/TaskDependency';
 
@@ -157,9 +157,7 @@ export class EditableTask {
         if (this.recurrenceRule) {
             recurrence = Recurrence.fromText({
                 recurrenceRuleText: this.recurrenceRule,
-                startDate,
-                scheduledDate,
-                dueDate,
+                occurrence: new Occurrence({ startDate, scheduledDate, dueDate }),
             });
         }
 
@@ -250,9 +248,7 @@ export class EditableTask {
         const recurrenceFromText = Recurrence.fromText({
             recurrenceRuleText: this.recurrenceRule,
             // Only for representation in the modal, no dates required.
-            startDate: null,
-            scheduledDate: null,
-            dueDate: null,
+            occurrence: new Occurrence({ startDate: null, scheduledDate: null, dueDate: null }),
         })?.toText();
 
         if (!recurrenceFromText) {


Test patch:
diff --git a/tests/Task/Recurrence.test.ts b/tests/Task/Recurrence.test.ts
index 5664dc0d..9a371408 100644
--- a/tests/Task/Recurrence.test.ts
+++ b/tests/Task/Recurrence.test.ts
@@ -2,7 +2,7 @@
  * @jest-environment jsdom
  */
 import moment from 'moment';
-import { Recurrence } from '../../src/Task/Recurrence';
+import { Occurrence, Recurrence } from '../../src/Task/Recurrence';
 import { RecurrenceBuilder } from '../TestingTools/RecurrenceBuilder';
 
 window.moment = moment;
@@ -12,9 +12,11 @@ describe('Recurrence', () => {
         // Arrange
         const recurrence = Recurrence.fromText({
             recurrenceRuleText: 'every week',
-            startDate: null,
-            scheduledDate: null,
-            dueDate: null,
+            occurrence: new Occurrence({
+                startDate: null,
+                scheduledDate: null,
+                dueDate: null,
+            }),
         });
 
         // Act
@@ -32,9 +34,11 @@ describe('Recurrence', () => {
         // Arrange
         const recurrence = Recurrence.fromText({
             recurrenceRuleText: 'every month',
-            startDate: null,
-            scheduledDate: null,
-            dueDate: moment('2022-01-31').startOf('day'),
+            occurrence: new Occurrence({
+                startDate: null,
+                scheduledDate: null,
+                dueDate: moment('2022-01-31').startOf('day'),
+            }),
         });
 
         // Act
@@ -50,9 +54,11 @@ describe('Recurrence', () => {
         // Arrange
         const recurrence = Recurrence.fromText({
             recurrenceRuleText: 'every 3 months',
-            startDate: null,
-            scheduledDate: null,
-            dueDate: moment('2022-01-31').startOf('day'),
+            occurrence: new Occurrence({
+                startDate: null,
+                scheduledDate: null,
+                dueDate: moment('2022-01-31').startOf('day'),
+            }),
         });
 
         // Act
@@ -68,9 +74,11 @@ describe('Recurrence', () => {
         // Arrange
         const recurrence = Recurrence.fromText({
             recurrenceRuleText: 'every 2 months',
-            startDate: null,
-            scheduledDate: null,
-            dueDate: moment('2023-12-31').startOf('day'),
+            occurrence: new Occurrence({
+                startDate: null,
+                scheduledDate: null,
+                dueDate: moment('2023-12-31').startOf('day'),
+            }),
         });
 
         // Act
@@ -86,9 +94,11 @@ describe('Recurrence', () => {
         // Arrange
         const recurrence = Recurrence.fromText({
             recurrenceRuleText: 'every 2 years',
-            startDate: null,
-            scheduledDate: null,
-            dueDate: moment('2024-02-29').startOf('day'),
+            occurrence: new Occurrence({
+                startDate: null,
+                scheduledDate: null,
+                dueDate: moment('2024-02-29').startOf('day'),
+            }),
         });
 
         // Act
@@ -104,9 +114,11 @@ describe('Recurrence', () => {
         // Arrange
         const recurrence = Recurrence.fromText({
             recurrenceRuleText: 'every 11 months',
-            startDate: null,
-            scheduledDate: null,
-            dueDate: moment('2020-03-31').startOf('day'),
+            occurrence: new Occurrence({
+                startDate: null,
+                scheduledDate: null,
+                dueDate: moment('2020-03-31').startOf('day'),
+            }),
         });
 
         // Act
@@ -122,9 +134,11 @@ describe('Recurrence', () => {
         // Arrange
         const recurrence = Recurrence.fromText({
             recurrenceRuleText: 'every 13 months',
-            startDate: null,
-            scheduledDate: null,
-            dueDate: moment('2020-01-31').startOf('day'),
+            occurrence: new Occurrence({
+                startDate: null,
+                scheduledDate: null,
+                dueDate: moment('2020-01-31').startOf('day'),
+            }),
         });
 
         // Act
@@ -144,9 +158,11 @@ describe('Recurrence - with invalid dates in tasks', () => {
         // Arrange
         const recurrence = Recurrence.fromText({
             recurrenceRuleText: 'every day',
-            startDate: null,
-            scheduledDate: null,
-            dueDate: moment('2022-02-30').startOf('day'), // 30th February: invalid date
+            occurrence: new Occurrence({
+                startDate: null,
+                scheduledDate: null,
+                dueDate: moment('2022-02-30').startOf('day'), // 30th February: invalid date
+            }),
         });
 
         // Assert
@@ -164,9 +180,11 @@ describe('Recurrence - with invalid dates in tasks', () => {
         // Arrange
         const recurrence = Recurrence.fromText({
             recurrenceRuleText: 'every day',
-            startDate: null,
-            scheduledDate: moment('2022-02-30').startOf('day'), // 30th February: invalid date
-            dueDate: moment('2022-02-27').startOf('day'),
+            occurrence: new Occurrence({
+                startDate: null,
+                scheduledDate: moment('2022-02-30').startOf('day'), // 30th February: invalid date
+                dueDate: moment('2022-02-27').startOf('day'),
+            }),
         });
 
         // Act
diff --git a/tests/TestingTools/RecurrenceBuilder.ts b/tests/TestingTools/RecurrenceBuilder.ts
index 39338607..b8118c5f 100644
--- a/tests/TestingTools/RecurrenceBuilder.ts
+++ b/tests/TestingTools/RecurrenceBuilder.ts
@@ -1,6 +1,6 @@
 // Builder
 import type { Moment } from 'moment';
-import { Recurrence } from '../../src/Task/Recurrence';
+import { Occurrence, Recurrence } from '../../src/Task/Recurrence';
 import { DateParser } from '../../src/Query/DateParser';
 
 /**
@@ -35,9 +35,11 @@ export class RecurrenceBuilder {
     public build(): Recurrence {
         return Recurrence.fromText({
             recurrenceRuleText: this._recurrenceRuleText,
-            startDate: this._startDate,
-            scheduledDate: this._scheduledDate,
-            dueDate: this._dueDate,
+            occurrence: new Occurrence({
+                startDate: this._startDate,
+                scheduledDate: this._scheduledDate,
+                dueDate: this._dueDate,
+            }),
         }) as Recurrence;
     }
 
diff --git a/tests/TestingTools/SampleTasks.ts b/tests/TestingTools/SampleTasks.ts
index 720a5dcb..b84b9ee6 100644
--- a/tests/TestingTools/SampleTasks.ts
+++ b/tests/TestingTools/SampleTasks.ts
@@ -1,5 +1,5 @@
 import { Task } from '../../src/Task/Task';
-import { Recurrence } from '../../src/Task/Recurrence';
+import { Occurrence, Recurrence } from '../../src/Task/Recurrence';
 import { Status } from '../../src/Statuses/Status';
 import { StatusType } from '../../src/Statuses/StatusConfiguration';
 import { Priority } from '../../src/Task/Priority';
@@ -55,9 +55,11 @@ export class SampleTasks {
                 .recurrence(
                     Recurrence.fromText({
                         recurrenceRuleText: recurrenceRule,
-                        startDate: null,
-                        scheduledDate: null,
-                        dueDate: null,
+                        occurrence: new Occurrence({
+                            startDate: null,
+                            scheduledDate: null,
+                            dueDate: null,
+                        }),
                     }),
                 )
                 .build();
diff --git a/tests/TestingTools/TaskBuilder.ts b/tests/TestingTools/TaskBuilder.ts
index a8fa90c3..d4b90181 100644
--- a/tests/TestingTools/TaskBuilder.ts
+++ b/tests/TestingTools/TaskBuilder.ts
@@ -3,7 +3,7 @@ import type { Moment } from 'moment';
 import { TasksFile } from '../../src/Scripting/TasksFile';
 import { Status } from '../../src/Statuses/Status';
 import { Task } from '../../src/Task/Task';
-import { Recurrence } from '../../src/Task/Recurrence';
+import { Occurrence, Recurrence } from '../../src/Task/Recurrence';
 import { DateParser } from '../../src/Query/DateParser';
 import { StatusConfiguration, StatusType } from '../../src/Statuses/StatusConfiguration';
 import { TaskLocation } from '../../src/Task/TaskLocation';
@@ -132,9 +132,11 @@ export class TaskBuilder {
         taskBuilder.recurrence(
             Recurrence.fromText({
                 recurrenceRuleText: 'every day when done',
-                startDate: taskBuilder._startDate,
-                scheduledDate: taskBuilder._scheduledDate,
-                dueDate: taskBuilder._dueDate,
+                occurrence: new Occurrence({
+                    startDate: taskBuilder._startDate,
+                    scheduledDate: taskBuilder._scheduledDate,
+                    dueDate: taskBuilder._dueDate,
+                }),
             }),
         );