Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: CustomPeriod and additional methods to handle any flavour of period #58

Merged
merged 6 commits into from
Feb 23, 2025

Conversation

Seddryck
Copy link
Owner

@Seddryck Seddryck commented Feb 22, 2025

Summary by CodeRabbit

  • New Features
    • Introduced an enhanced period type that supports robust date range validations and a wide array of period operations, including containment, overlap, adjacency, ordering, intersection, merging, and gap calculation.
    • Upgraded period comparison capabilities with improved operators and increased null-safety and error handling for more reliable period manipulations.
    • Added comprehensive unit tests to validate the functionality of the new period type and its methods.

Copy link

coderabbitai bot commented Feb 22, 2025

Walkthrough

This pull request introduces a new CustomPeriod struct that implements the IPeriod interface along with comprehensive period manipulation methods such as containment, overlap, meeting, precedence, succession, intersection, span, and gap calculation. The IPeriod interface itself is updated to include these methods and implement IEquatable<IPeriod>. Alongside, new unit test classes validate the behavior of both CustomPeriod and additional period methods. Enhancements to the source generator, including improved error handling and new Scriban templates, support the automated generation of period-related methods.

Changes

File(s) Change Summary
src/Chrononuensis.Core.Testing/CustomPeriodTests.cs
src/Chrononuensis.Core.Testing/PeriodMethodsTests.cs
Added new unit test classes to validate the CustomPeriod struct and period method functionalities using NUnit.
src/Chrononuensis.Core/CustomPeriod.cs
src/Chrononuensis.Core/IPeriod.cs
Introduced the CustomPeriod struct implementing IPeriod with properties, validation, methods for period manipulation, and operator overloads; updated the IPeriod interface to include new period-method signatures and implement IEquatable<IPeriod>.
src/Chrononuensis.SourceGenerator/StructGenerator.cs
src/Chrononuensis.SourceGenerator/Chrononuensis.SourceGenerator.csproj
Enhanced the source generator to generate period methods for structs by adding a try-catch block, new method GeneratePeriodMethods, and an embedded resource configuration for the Scriban template.
src/Chrononuensis.SourceGenerator/Templates/Period.scriban-cs
src/Chrononuensis.SourceGenerator/Templates/Period.Methods.scriban-cs
Updated templates by enabling nullable reference types and adding an Equals method; introduced a new Scriban template for generating period method implementations in partial record structs.

Sequence Diagram(s)

Loading
sequenceDiagram
    participant SD as StructDefinition
    participant SG as StructGenerator
    participant ST as ScribanTemplate Engine
    participant GS as Generated Source File
    participant DR as Diagnostic Reporter

    SD->>SG: Pass structDefinition (with Period property)
    alt Period is not null
        SG->>SG: Execute try block
        SG->>ST: Load and parse embedded template
        ST-->>SG: Render period methods code
        SG->>GS: Add generated source file ("<StructName>.Period.Methods.g.cs")
    else Exception occurs
        SG->>DR: Report diagnostic error with code "CHRONO006"
    end

Possibly related PRs

Suggested labels

new-feature

Poem

Oh, I’m a rabbit, hopping through the code,
Carrots of logic in every new node.
Structs and tests make my heart skip a beat,
Scriban templates and periods—oh, so neat!
With every fresh change, I cheer with delight,
Happy ears and whiskers dancing in flight!
🥕🐰

✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Sorry, something went wrong.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (9)
src/Chrononuensis.Core/DateOnlyExtensions.cs (2)

9-9: Consider making the class public for reusability.

The utility methods in this class could be useful outside the assembly. Consider making it public unless there's a specific reason to keep it internal.

-internal static class DateOnlyExtensions
+public static class DateOnlyExtensions

11-18: Consider using LINQ for a more concise implementation.

The current implementation is correct but could be more concise using LINQ's Max and Min methods.

-    public static DateOnly Max(DateOnly dateOnly, params DateOnly[] others)
-    {
-        var max = dateOnly;
-        foreach (var other in others)
-            if (other > max)
-                max = other;
-        return max;
-    }
+    public static DateOnly Max(DateOnly dateOnly, params DateOnly[] others)
+        => others.Append(dateOnly).Max();

-    public static DateOnly Min(DateOnly dateOnly, params DateOnly[] others)
-    {
-        var min = dateOnly;
-        foreach (var other in others)
-            if (other < min)
-                min = other;
-        return min;
-    }
+    public static DateOnly Min(DateOnly dateOnly, params DateOnly[] others)
+        => others.Append(dateOnly).Min();

Also applies to: 20-27

src/Chrononuensis.Core.Testing/CustomPeriodTests.cs (1)

1-44: Add tests for new IPeriod methods.

The test class is missing coverage for the new methods added to IPeriod interface:

  • Contains
  • Overlaps
  • Meets
  • Precedes
  • Succeeds
  • Intersect
  • Span
  • Gap

Would you like me to generate test cases for these methods?

src/Chrononuensis.Core/IPeriod.cs (2)

77-80: Enhance Gap method documentation.

The documentation should clarify the return value when periods overlap.

     /// <summary>
     /// Determines the gap (number of days) between two non-overlapping periods.
+    /// Returns a negative value if the periods overlap.
     /// </summary>

72-75: Add example to Span method documentation.

The documentation would benefit from an example to illustrate the concept of "smallest enclosing period".

     /// <summary>
     /// Returns the span of two periods, merging them into the smallest enclosing period.
+    /// For example, if period1 is [2024-01-01, 2024-01-10] and period2 is [2024-01-05, 2024-01-15],
+    /// the span would be [2024-01-01, 2024-01-15].
     /// </summary>
src/Chrononuensis.Core/CustomPeriod.cs (4)

14-19: Enhance error message clarity.

While the validation logic is correct, consider making the error message more user-friendly.

-            throw new ArgumentException($"The start date ({firstDate}) cannot be later than the end date ({lastDate}).", nameof(firstDate));
+            throw new ArgumentException($"Invalid period: Start date {firstDate:d} must be on or before end date {lastDate:d}.", nameof(firstDate));

76-79: Consider simplifying the Gap calculation.

The current implementation can be made more readable by extracting the gap calculation logic.

-    public int Gap(IPeriod other) =>
-        Overlaps(other) || Meets(other) ? 0 :
-        other.FirstDate > LastDate ? other.FirstDate.DayNumber - LastDate.DayNumber - 1 :
-        FirstDate.DayNumber - other.LastDate.DayNumber - 1;
+    public int Gap(IPeriod other)
+    {
+        if (Overlaps(other) || Meets(other))
+            return 0;
+            
+        var (earlierPeriod, laterPeriod) = other.FirstDate > LastDate 
+            ? (this, other) 
+            : (other, this);
+            
+        return laterPeriod.FirstDate.DayNumber - earlierPeriod.LastDate.DayNumber - 1;
+    }

81-81: Consider using a standard date format string.

The current format could be made more standard and culture-invariant.

-    public override string ToString() => $"Custom period: {FirstDate} - {LastDate}";
+    public override string ToString() => $"{FirstDate:yyyy-MM-dd} to {LastDate:yyyy-MM-dd}";

91-100: Consider reducing duplication in operator implementations.

The <= and >= operators have duplicated logic across multiple overloads.

Consider extracting the common logic into private methods:

+    private static bool IsLessThanOrEqual(IPeriod left, IPeriod right) 
+        => left.Precedes(right) || left.LastDate == right.FirstDate;
+        
+    private static bool IsGreaterThanOrEqual(IPeriod left, IPeriod right)
+        => left.Succeeds(right) || left.LastDate == right.FirstDate;
+        
-    public static bool operator <=(CustomPeriod left, CustomPeriod right) => left.Precedes(right) || left.LastDate == right.FirstDate;
-    public static bool operator >=(CustomPeriod left, CustomPeriod right) => left.Succeeds(right) || left.LastDate == right.FirstDate;
-    public static bool operator <=(CustomPeriod left, IPeriod right) => left.Precedes(right) || left.LastDate == right.FirstDate;
-    public static bool operator >=(CustomPeriod left, IPeriod right) => left.Succeeds(right) || left.LastDate == right.FirstDate;
-    public static bool operator <=(IPeriod left, CustomPeriod right) => left.Precedes(right) || left.LastDate == right.FirstDate;
-    public static bool operator >=(IPeriod left, CustomPeriod right) => left.Succeeds(right) || left.LastDate == right.FirstDate;
+    public static bool operator <=(CustomPeriod left, CustomPeriod right) => IsLessThanOrEqual(left, right);
+    public static bool operator >=(CustomPeriod left, CustomPeriod right) => IsGreaterThanOrEqual(left, right);
+    public static bool operator <=(CustomPeriod left, IPeriod right) => IsLessThanOrEqual(left, right);
+    public static bool operator >=(CustomPeriod left, IPeriod right) => IsGreaterThanOrEqual(left, right);
+    public static bool operator <=(IPeriod left, CustomPeriod right) => IsLessThanOrEqual(left, right);
+    public static bool operator >=(IPeriod left, CustomPeriod right) => IsGreaterThanOrEqual(left, right);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b717041 and a547c12.

📒 Files selected for processing (5)
  • src/Chrononuensis.Core.Testing/CustomPeriodTests.cs (1 hunks)
  • src/Chrononuensis.Core/CustomPeriod.cs (1 hunks)
  • src/Chrononuensis.Core/DateOnlyExtensions.cs (1 hunks)
  • src/Chrononuensis.Core/IPeriod.cs (2 hunks)
  • src/Chrononuensis.SourceGenerator/Templates/Period.scriban-cs (2 hunks)
🔇 Additional comments (3)
src/Chrononuensis.SourceGenerator/Templates/Period.scriban-cs (1)

1-1: LGTM! The changes enhance type safety and equality comparison.

The addition of nullable reference types and the Equals method implementation follow best practices:

  • Proper null check using pattern matching
  • Correct property comparison for equality

Also applies to: 36-37

src/Chrononuensis.Core/CustomPeriod.cs (2)

8-12: Well-designed immutable struct implementation!

Good use of readonly struct with init properties to ensure immutability.


27-54: Excellent implementation of period comparison methods!

The methods are well-documented, efficient, and use clear boolean expressions for comparisons.

Comment on lines 26 to 27
public void LessThanOrEqual_CustomPeriod_False()
=> Assert.That(new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)) <= new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)), Is.False);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect test assertion.

The test LessThanOrEqual_CustomPeriod_False compares equal periods but expects false. For equal periods, <= should return true.

-    public void LessThanOrEqual_CustomPeriod_False()
-        => Assert.That(new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)) <= new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)), Is.False);
+    public void LessThanOrEqual_CustomPeriod_True()
+        => Assert.That(new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)) <= new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)), Is.True);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void LessThanOrEqual_CustomPeriod_False()
=> Assert.That(new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)) <= new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)), Is.False);
public void LessThanOrEqual_CustomPeriod_True()
=> Assert.That(new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)) <= new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)), Is.True);

Comment on lines 38 to 39
public void NotEqual_CustomPeriod_True()
=> Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31)) != new YearMonth(2024, 1), Is.False);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix misleading test name.

The test name NotEqual_CustomPeriod_True suggests expecting true but asserts false, making it confusing.

-    public void NotEqual_CustomPeriod_True()
-        => Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31)) != new YearMonth(2024, 1), Is.False);
+    public void NotEqual_CustomPeriod_False()
+        => Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31)) != new YearMonth(2024, 1), Is.False);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void NotEqual_CustomPeriod_True()
=> Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31)) != new YearMonth(2024, 1), Is.False);
public void NotEqual_CustomPeriod_False()
=> Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31)) != new YearMonth(2024, 1), Is.False);

(FirstDate, LastDate) = (firstDate, lastDate);
}

public int Days => FirstDate.DayNumber - LastDate.DayNumber + 1;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect Days calculation.

The current calculation will give negative results as it subtracts LastDate from FirstDate. The order should be reversed.

-    public int Days => FirstDate.DayNumber - LastDate.DayNumber + 1;
+    public int Days => LastDate.DayNumber - FirstDate.DayNumber + 1;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public int Days => FirstDate.DayNumber - LastDate.DayNumber + 1;
public int Days => LastDate.DayNumber - FirstDate.DayNumber + 1;

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/Chrononuensis.Core.Testing/CustomPeriodTests.cs (1)

53-55: ⚠️ Potential issue

Fix incorrect test assertion and name.

The test name suggests expecting false but asserts true. This is inconsistent and confusing.

-    public void LessThanOrEqual_CustomPeriod_False()
-        => Assert.That(new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10))
-            <= new CustomPeriod(new DateOnly(2025, 1, 10), new DateOnly(2025, 1, 20)), Is.True);
+    public void LessThanOrEqual_CustomPeriod_True()
+        => Assert.That(new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10))
+            <= new CustomPeriod(new DateOnly(2025, 1, 10), new DateOnly(2025, 1, 20)), Is.True);
🧹 Nitpick comments (5)
src/Chrononuensis.Core.Testing/CustomPeriodTests.cs (5)

11-14: Improve constructor test by asserting state.

Instead of using DoesNotThrow, consider asserting the actual state of the created object to ensure it's initialized correctly.

-    public void Ctor_ValidValues_Expected()
-        => Assert.DoesNotThrow(() => new CustomPeriod(new DateOnly(2025,1,1), new DateOnly(2025, 1, 10)));
+    public void Ctor_ValidValues_Expected()
+    {
+        var period = new CustomPeriod(new DateOnly(2025,1,1), new DateOnly(2025, 1, 10));
+        Assert.Multiple(() =>
+        {
+            Assert.That(period.FirstDate, Is.EqualTo(new DateOnly(2025,1,1)));
+            Assert.That(period.LastDate, Is.EqualTo(new DateOnly(2025, 1, 10)));
+        });
+    }

15-19: Verify exception message in constructor test.

The test captures the exception but doesn't verify its message. Consider asserting the exception message to ensure it provides meaningful feedback.

     public void Ctor_InvalidValues_Throws()
     {
-        var ex = Assert.Throws<ArgumentException>(() => new CustomPeriod(new DateOnly(2025, 1, 10), new DateOnly(2025, 1, 1)));
+        var ex = Assert.Throws<ArgumentException>(() => 
+            new CustomPeriod(new DateOnly(2025, 1, 10), new DateOnly(2025, 1, 1)));
+        Assert.That(ex.Message, Does.Contain("LastDate must be greater than or equal to FirstDate"));
     }

26-27: Fix typo in test method name.

The test method name has a typo: "FirsDate" should be "FirstDate".

-    public void FirsDate_ValidValues_Expected()
+    public void FirstDate_ValidValues_Expected()

21-24: Add edge cases for Days property test.

Consider adding test cases for edge cases like single-day periods and month boundaries.

+    [TestCase("2025-01-01", "2025-01-01", 1, Description = "Single day")]
+    [TestCase("2025-01-31", "2025-02-01", 2, Description = "Month boundary")]
+    [TestCase("2024-02-28", "2024-03-01", 3, Description = "Month boundary in leap year")]
+    public void Days_ValidValues_Expected(string start, string end, int expectedDays)
+    {
+        var period = new CustomPeriod(DateOnly.Parse(start), DateOnly.Parse(end));
+        Assert.That(period.Days, Is.EqualTo(expectedDays));
+    }
-    public void Days_ValidValues_Expected()
-        => Assert.That(new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)).Days, Is.EqualTo(10));

77-93: Add missing equality test cases.

Consider adding tests for null comparison and different type comparison to ensure complete coverage of equality scenarios.

+    [Test]
+    public void Equal_Null_False()
+    {
+        var period = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31));
+        Assert.That(period.Equals(null), Is.False);
+    }
+
+    [Test]
+    public void Equal_DifferentType_False()
+    {
+        var period = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31));
+        Assert.That(period.Equals("not a period"), Is.False);
+    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a547c12 and cb41959.

📒 Files selected for processing (2)
  • src/Chrononuensis.Core.Testing/CustomPeriodTests.cs (1 hunks)
  • src/Chrononuensis.Core/CustomPeriod.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Chrononuensis.Core/CustomPeriod.cs

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (6)
src/Chrononuensis.SourceGenerator/StructGenerator.cs (1)

263-282: Consider enriching the template model with additional context.

The model passed to the template is minimal compared to other generator methods. Consider passing additional context that might be useful for generating period methods, such as:

  • parts for type information
  • period configuration
  • by_year, first, last values (as used in GeneratePeriod)

Apply this diff to enrich the model:

 var output = scribanTemplate.Render(new
 {
     struct_name = structDefinition.Name,
+    parts = structDefinition.Parts?.Select(p => new
+    {
+        name = p.Name,
+        type = p.Type
+    }).ToList(),
+    period = new
+    {
+        by_year = structDefinition.Period.ByYear,
+        first = structDefinition.Period.First,
+        last = structDefinition.Period.Last,
+        year = new
+        {
+            value = structDefinition.Period.Year,
+            duration = structDefinition.Period.YearDuration
+        }
+    }
 });
src/Chrononuensis.SourceGenerator/Templates/Period.Methods.scriban-cs (2)

24-25: Optimize the Meets method implementation.

The current implementation checks both boundaries. Since periods can only meet at one boundary, we can optimize by first determining which period comes first.

-    public bool Meets(IPeriod other) => 
-        LastDate.AddDays(1) == other.FirstDate || other.LastDate.AddDays(1) == FirstDate;
+    public bool Meets(IPeriod other) => 
+        FirstDate <= other.FirstDate 
+            ? LastDate.AddDays(1) == other.FirstDate
+            : other.LastDate.AddDays(1) == FirstDate;

58-61: Simplify the Gap method implementation.

The current implementation has multiple conditional branches. We can simplify it by using Math.Abs to handle both cases.

-    public int Gap(IPeriod other) =>
-        Overlaps(other) || Meets(other) ? 0 :
-        other.FirstDate > LastDate ? other.FirstDate.DayNumber - LastDate.DayNumber - 1 :
-        FirstDate.DayNumber - other.LastDate.DayNumber - 1;
+    public int Gap(IPeriod other) =>
+        Overlaps(other) || Meets(other) ? 0 :
+        Math.Abs(
+            FirstDate <= other.FirstDate
+                ? other.FirstDate.DayNumber - LastDate.DayNumber - 1
+                : FirstDate.DayNumber - other.LastDate.DayNumber - 1);
src/Chrononuensis.Core.Testing/PeriodMethodsTests.cs (3)

202-204: Fix incorrect XML comment.

The XML comment for SpanTestCases incorrectly states it's for "Intersect".

-    /// <summary>
-    /// Manually defined test cases for Intersect.
-    /// </summary>
+    /// <summary>
+    /// Manually defined test cases for Span.
+    /// </summary>

233-235: Fix incorrect XML comment.

The XML comment for GapTestCases incorrectly states it's for "Intersect".

-    /// <summary>
-    /// Manually defined test cases for Intersect.
-    /// </summary>
+    /// <summary>
+    /// Manually defined test cases for Gap.
+    /// </summary>

27-37: Add descriptive test case names.

The test cases would be more maintainable with descriptive names that explain the scenario being tested.

Example for ContainsTestCases:

-        yield return new TestCaseData(century, decade).Returns(false);
+        yield return new TestCaseData(century, decade)
+            .SetName("Century does not contain Decade outside its range")
+            .Returns(false);

Also applies to: 58-68, 89-99, 121-131, 153-165, 185-195, 216-226, 247-257

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb41959 and b07f378.

📒 Files selected for processing (5)
  • src/Chrononuensis.Core.Testing/CustomPeriodTests.cs (1 hunks)
  • src/Chrononuensis.Core.Testing/PeriodMethodsTests.cs (1 hunks)
  • src/Chrononuensis.SourceGenerator/Chrononuensis.SourceGenerator.csproj (1 hunks)
  • src/Chrononuensis.SourceGenerator/StructGenerator.cs (2 hunks)
  • src/Chrononuensis.SourceGenerator/Templates/Period.Methods.scriban-cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Chrononuensis.Core.Testing/CustomPeriodTests.cs
🔇 Additional comments (3)
src/Chrononuensis.SourceGenerator/StructGenerator.cs (1)

91-108: LGTM! Error handling follows consistent pattern.

The try-catch block follows the established pattern in the file, with proper error reporting and a unique error code.

src/Chrononuensis.SourceGenerator/Chrononuensis.SourceGenerator.csproj (1)

34-34: LGTM! Resource inclusion follows consistent pattern.

The new embedded resource follows the established pattern and is correctly placed alongside other period-related templates.

src/Chrononuensis.Core.Testing/PeriodMethodsTests.cs (1)

1-263: LGTM! Comprehensive test coverage.

The test suite provides excellent coverage of the period manipulation methods, including edge cases and various period combinations.

@codecov-commenter
Copy link

codecov-commenter commented Feb 23, 2025

Codecov Report

Attention: Patch coverage is 56.06061% with 29 lines in your changes missing coverage. Please review.

Project coverage is 85.75%. Comparing base (b717041) to head (4fd4c07).

Files with missing lines Patch % Lines
src/Chrononuensis.Core/CustomPeriod.cs 56.06% 20 Missing and 9 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #58      +/-   ##
==========================================
- Coverage   91.61%   85.75%   -5.87%     
==========================================
  Files          23       24       +1     
  Lines         334      400      +66     
  Branches       45       60      +15     
==========================================
+ Hits          306      343      +37     
- Misses         13       33      +20     
- Partials       15       24       +9     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/Chrononuensis.Core/CustomPeriod.cs (2)

10-13: Add XML documentation for public members.

Consider adding XML documentation for the public properties to improve IntelliSense support and API documentation.

+    /// <summary>
+    /// Gets the first date of the period.
+    /// </summary>
     public DateOnly FirstDate { get; init; }

+    /// <summary>
+    /// Gets the last date of the period.
+    /// </summary>
     public DateOnly LastDate { get; init; }

+    /// <summary>
+    /// Gets the total number of days in the period, inclusive of both first and last dates.
+    /// </summary>
     public int Days => LastDate.DayNumber - FirstDate.DayNumber + 1;

+    /// <summary>
+    /// Gets the lower bound of the period as a DateTime at midnight.
+    /// </summary>
     public DateTime LowerBound => FirstDate.ToDateTime(TimeOnly.MinValue);

+    /// <summary>
+    /// Gets the upper bound of the period as a DateTime at midnight of the next day.
+    /// </summary>
     public DateTime UpperBound => LastDate.AddDays(1).ToDateTime(TimeOnly.MinValue);

Also applies to: 21-25


73-86: Consider handling negative gaps.

The Gap method correctly handles overlapping periods and meets by returning 0. However, consider throwing an ArgumentException for negative gaps to catch potential bugs in the calling code.

     public int Gap(IPeriod other)
     {
         if (Overlaps(other) || Meets(other))
             return 0;

         (var earlierPeriod, var laterPeriod) = other.FirstDate > LastDate
             ? ((IPeriod)this, other)
             : (other, this);

-        return laterPeriod.FirstDate.DayNumber - earlierPeriod.LastDate.DayNumber - 1;
+        var gap = laterPeriod.FirstDate.DayNumber - earlierPeriod.LastDate.DayNumber - 1;
+        if (gap < 0)
+            throw new ArgumentException($"Invalid gap: {gap} days. This indicates a bug as periods should not overlap.", nameof(other));
+        return gap;
     }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c5add8 and 266704d.

📒 Files selected for processing (3)
  • src/Chrononuensis.Core.Testing/CustomPeriodTests.cs (1 hunks)
  • src/Chrononuensis.Core/CustomPeriod.cs (1 hunks)
  • src/Chrononuensis.SourceGenerator/Templates/Period.Methods.scriban-cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/Chrononuensis.Core.Testing/CustomPeriodTests.cs
  • src/Chrononuensis.SourceGenerator/Templates/Period.Methods.scriban-cs
🔇 Additional comments (4)
src/Chrononuensis.Core/CustomPeriod.cs (4)

8-13: LGTM! Well-designed immutable struct.

The struct is correctly marked as readonly with init-only properties, ensuring thread-safety and preventing mutation after construction.


27-54: LGTM! Well-implemented period comparison methods.

The period comparison methods are logically correct and well-documented:

  • Contains checks full containment
  • Overlaps checks any intersection
  • Meets checks adjacency
  • Precedes/Succeeds check strict ordering

55-72: LGTM! Well-implemented Intersect and Span methods.

The methods correctly handle period intersections and merging:

  • Intersect returns null for non-overlapping periods
  • Span creates the smallest enclosing period

88-109: LGTM! Well-implemented equality methods.

The equality methods follow best practices:

  • Null checks
  • Type checking
  • Consistent GetHashCode

Comment on lines 117 to 122
private static bool IsLessThanOrEqual(IPeriod left, IPeriod right)
=> left.FirstDate <= right.FirstDate && left.LastDate <= right.FirstDate;

private static bool IsGreaterThanOrEqual(IPeriod left, IPeriod right)
=> left.FirstDate >= right.FirstDate && left.LastDate >= right.FirstDate;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect comparison logic in IsLessThanOrEqual and IsGreaterThanOrEqual.

The comparison methods have bugs that could lead to incorrect results:

  • IsLessThanOrEqual compares LastDate with FirstDate instead of right.LastDate
  • IsGreaterThanOrEqual has the same issue
     private static bool IsLessThanOrEqual(IPeriod left, IPeriod right)
-        => left.FirstDate <= right.FirstDate && left.LastDate <= right.FirstDate;
+        => left.FirstDate <= right.FirstDate && left.LastDate <= right.LastDate;

     private static bool IsGreaterThanOrEqual(IPeriod left, IPeriod right)
-        => left.FirstDate >= right.FirstDate && left.LastDate >= right.FirstDate;
+        => left.FirstDate >= right.FirstDate && left.LastDate >= right.LastDate;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static bool IsLessThanOrEqual(IPeriod left, IPeriod right)
=> left.FirstDate <= right.FirstDate && left.LastDate <= right.FirstDate;
private static bool IsGreaterThanOrEqual(IPeriod left, IPeriod right)
=> left.FirstDate >= right.FirstDate && left.LastDate >= right.FirstDate;
private static bool IsLessThanOrEqual(IPeriod left, IPeriod right)
=> left.FirstDate <= right.FirstDate && left.LastDate <= right.LastDate;
private static bool IsGreaterThanOrEqual(IPeriod left, IPeriod right)
=> left.FirstDate >= right.FirstDate && left.LastDate >= right.LastDate;

Comment on lines 1 to 136
FirstDate <= other.LastDate && LastDate >= other.FirstDate;

/// <summary>
/// Determines if this period meets another period (i.e., is adjacent without overlap).
/// </summary>
public bool Meets(IPeriod other) =>
LastDate.AddDays(1) == other.FirstDate || other.LastDate.AddDays(1) == FirstDate;

/// <summary>
/// Determines if this period is strictly before another.
/// </summary>
public bool Precedes(IPeriod other) => LastDate < other.FirstDate;

/// <summary>
/// Determines if this period is strictly after another.
/// </summary>
public bool Succeeds(IPeriod other) => FirstDate > other.LastDate;

/// <summary>
/// Returns the intersection of two periods if they overlap.
/// </summary>
public IPeriod? Intersect(IPeriod other) =>
Overlaps(other)
? new CustomPeriod(
new[] { FirstDate, other.FirstDate }.Max(),
new[] { LastDate, other.LastDate }.Min())
: null;

/// <summary>
/// Returns the span of two periods, merging them into the smallest enclosing period.
/// </summary>
public IPeriod Span(IPeriod other) =>
new CustomPeriod(
new[] { FirstDate, other.FirstDate }.Min(),
new[] { LastDate, other.LastDate }.Max());

/// <summary>
/// Determines the gap (number of days) between two non-overlapping periods.
/// </summary>
public int Gap(IPeriod other)
{
if (Overlaps(other) || Meets(other))
return 0;

(var earlierPeriod, var laterPeriod) = other.FirstDate > LastDate
? ((IPeriod)this, other)
: (other, this);

return laterPeriod.FirstDate.DayNumber - earlierPeriod.LastDate.DayNumber - 1;
}

public override string ToString() => $"Custom period: {FirstDate} - {LastDate}";


public bool Equals(IPeriod? other)
=> other switch
{
null => false,
CustomPeriod period => this == period,
_ => FirstDate == other.FirstDate && LastDate == other.LastDate
};

public override bool Equals(object? obj)
=> obj switch
{
null => false,
CustomPeriod period => this == period,
IPeriod period => this == period,
_ => false
};

public override int GetHashCode() => HashCode.Combine(FirstDate, LastDate);

public static bool operator <(CustomPeriod left, CustomPeriod right) => left.Precedes(right);
public static bool operator >(CustomPeriod left, CustomPeriod right) => left.Succeeds(right);
public static bool operator <(CustomPeriod left, IPeriod right) => left.Precedes(right);
public static bool operator >(CustomPeriod left, IPeriod right) => left.Succeeds(right);
public static bool operator <(IPeriod left, CustomPeriod right) => left.Precedes(right);
public static bool operator >(IPeriod left, CustomPeriod right) => left.Succeeds(right);

private static bool IsLessThanOrEqual(IPeriod left, IPeriod right)
=> left.FirstDate <= right.FirstDate && left.LastDate <= right.FirstDate;

private static bool IsGreaterThanOrEqual(IPeriod left, IPeriod right)
=> left.FirstDate >= right.FirstDate && left.LastDate >= right.FirstDate;

public static bool operator <=(CustomPeriod left, CustomPeriod right) => IsLessThanOrEqual(left, right);
public static bool operator >=(CustomPeriod left, CustomPeriod right) => IsGreaterThanOrEqual(left, right);
public static bool operator <=(CustomPeriod left, IPeriod right) => IsLessThanOrEqual(left, right);
public static bool operator >=(CustomPeriod left, IPeriod right) => IsGreaterThanOrEqual(left, right);
public static bool operator <=(IPeriod left, CustomPeriod right) => IsLessThanOrEqual(left, right);
public static bool operator >=(IPeriod left, CustomPeriod right) => IsGreaterThanOrEqual(left, right);

public static bool operator ==(CustomPeriod left, CustomPeriod right) => left.FirstDate == right.FirstDate && left.LastDate == right.LastDate;
public static bool operator !=(CustomPeriod left, CustomPeriod right) => !(left == right);
public static bool operator ==(CustomPeriod left, IPeriod right) => left.FirstDate == right.FirstDate && left.LastDate == right.LastDate;
public static bool operator !=(CustomPeriod left, IPeriod right) => !(left == right);
public static bool operator ==(IPeriod left, CustomPeriod right) => left.FirstDate == right.FirstDate && left.LastDate == right.LastDate;
public static bool operator !=(IPeriod left, CustomPeriod right) => !(left == right);
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Improve test coverage.

The Codecov report indicates low patch coverage (38.98%) with 33 lines missing coverage. Consider adding test cases for:

  1. Edge cases in comparison methods (e.g., adjacent periods, single-day periods)
  2. Operator overloads (especially <=, >=)
  3. Error conditions (e.g., invalid periods, negative gaps)

🏁 Script executed:

#!/bin/bash
# Description: Find existing test files for CustomPeriod

echo "Searching for test files..."
fd -e cs "CustomPeriod.*Test" -x cat {}

Length of output: 5266


Enhance test coverage for period operations.

The existing test suite confirms correct behavior for constructors, basic properties, and a few operator overloads. However, many key period methods remain untested. In particular, please add tests that cover:

  • Comparison methods: Validate edge cases for Contains, Overlaps, Meets, Precedes, and Succeeds (e.g., adjacent periods and single-day periods).
  • Operator overloads: Extend coverage for all overloads, especially those that mix CustomPeriod and IPeriod.
  • Additional logic: Exercise methods like Intersect, Span, and especially Gap (including non-overlapping periods where the gap should be correctly computed).

These additions should help improve the overall patch coverage and safeguard against potential boundary issues.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/Chrononuensis.Core.Testing/CustomPeriodTests.cs (1)

74-77: ⚠️ Potential issue

Fix incorrect test name and assertion.

The test name suggests expecting false but asserts true, making it confusing. Additionally, the test case is testing overlapping periods which should be clarified in the name.

-    public void LessThanOrEqual_CustomPeriod_False()
-        => Assert.That(new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10))
-            <= new CustomPeriod(new DateOnly(2025, 1, 10), new DateOnly(2025, 1, 20)), Is.True);
+    public void LessThanOrEqual_OverlappingPeriods_True()
+        => Assert.That(new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10))
+            <= new CustomPeriod(new DateOnly(2025, 1, 10), new DateOnly(2025, 1, 20)), Is.True);
🧹 Nitpick comments (2)
src/Chrononuensis.Core.Testing/CustomPeriodTests.cs (2)

37-44: Enhance test coverage for Days property.

Consider adding more test cases to cover:

  • Longer periods (e.g., full year)
  • Periods crossing year boundaries
  • Maximum supported period length
 [TestCase("2025-01-01", "2025-01-01", 1, Description = "Single day")]
 [TestCase("2025-01-31", "2025-02-01", 2, Description = "Month boundary")]
 [TestCase("2024-02-28", "2024-03-01", 3, Description = "Month boundary in leap year")]
+[TestCase("2024-01-01", "2024-12-31", 366, Description = "Full leap year")]
+[TestCase("2024-12-31", "2025-01-01", 2, Description = "Year boundary")]
+[TestCase("2025-12-31", "2026-01-01", 2, Description = "Non-leap year boundary")]
 public void Days_ValidValues_Expected(string start, string end, int expectedDays)

54-60: Verify DateTime components in bound tests.

The tests for LowerBound and UpperBound should verify that the time component is set correctly (midnight for lower bound, midnight next day for upper bound).

-    public void LowerBound_ValidValues_Expected()
-        => Assert.That(new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)).LowerBound, Is.EqualTo(new DateTime(2025, 1, 1)));
+    public void LowerBound_ValidValues_Expected()
+        => Assert.Multiple(() => {
+            var bound = new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)).LowerBound;
+            Assert.That(bound.Date, Is.EqualTo(new DateTime(2025, 1, 1)));
+            Assert.That(bound.TimeOfDay, Is.EqualTo(TimeSpan.Zero));
+        });

-    public void UpperBound_ValidValues_Expected()
-        => Assert.That(new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)).UpperBound, Is.EqualTo(new DateTime(2025, 1, 11)));
+    public void UpperBound_ValidValues_Expected()
+        => Assert.Multiple(() => {
+            var bound = new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)).UpperBound;
+            Assert.That(bound.Date, Is.EqualTo(new DateTime(2025, 1, 11)));
+            Assert.That(bound.TimeOfDay, Is.EqualTo(TimeSpan.Zero));
+        });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 266704d and 4fd4c07.

📒 Files selected for processing (2)
  • src/Chrononuensis.Core.Testing/CustomPeriodTests.cs (1 hunks)
  • src/Chrononuensis.Core/CustomPeriod.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Chrononuensis.Core/CustomPeriod.cs

Comment on lines +62 to +97

[Test]
public void LessThanOrEqual_CustomPeriod_True()
=> Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 10))
<= new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10)), Is.True);

[Test]
public void LessThan_CustomPeriod_False()
=> Assert.That(new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10))
< new CustomPeriod(new DateOnly(2025, 1, 10), new DateOnly(2025, 1, 20)), Is.False);

[Test]
public void LessThanOrEqual_CustomPeriod_False()
=> Assert.That(new CustomPeriod(new DateOnly(2025, 1, 1), new DateOnly(2025, 1, 10))
<= new CustomPeriod(new DateOnly(2025, 1, 10), new DateOnly(2025, 1, 20)), Is.True);

[Test]
public void Equal_CustomPeriod_True()
=> Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31))
== new YearMonth(2024, 1), Is.True);

[Test]
public void Equal_CustomPeriod_False()
=> Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31))
== new YearMonth(2025, 1), Is.False);

[Test]
public void NotEqual_CustomPeriod_False()
=> Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31))
!= new YearMonth(2024, 1), Is.False);

[Test]
public void NotEqual_CustomPeriod_True()
=> Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31))
!= new YearMonth(2025, 1), Is.True);

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add missing comparison test cases.

The comparison operators need more test cases to cover:

  • Equal periods
  • Adjacent periods
  • Completely disjoint periods
  • Edge cases around period boundaries

Add these test cases:

[Test]
public void LessThan_AdjacentPeriods_True()
    => Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 10))
        < new CustomPeriod(new DateOnly(2024, 1, 11), new DateOnly(2024, 1, 20)), Is.True);

[Test]
public void LessThan_DisjointPeriods_True()
    => Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 10))
        < new CustomPeriod(new DateOnly(2024, 2, 1), new DateOnly(2024, 2, 10)), Is.True);

[Test]
public void LessThanOrEqual_EqualPeriods_True()
    => Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 10))
        <= new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 10)), Is.True);

Comment on lines +98 to +128
[Test]
public void Equal_Identical_True()
=> Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31)).Equals(
new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31))), Is.True);

[Test]
public void Equal_SameSpan_True()
=> Assert.That(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31)).Equals(
new YearMonth(2024, 1)), Is.True);

[Test]
public void Equal_SameRef_True()
{
var period = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31));
Assert.That(period.Equals(period), Is.True);
}

[Test]
public void Equal_Null_False()
{
var period = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31));
Assert.That(period.Equals(null), Is.False);
}

[Test]
public void Equal_DifferentType_False()
{
var period = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31));
Assert.That(period.Equals("not a period"), Is.False);
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add GetHashCode test for equality contract.

When implementing equality, it's important to test GetHashCode to ensure equal objects have equal hash codes.

Add this test:

[Test]
public void GetHashCode_EqualPeriods_SameHashCode()
{
    var period1 = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31));
    var period2 = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31));
    Assert.That(period1.GetHashCode(), Is.EqualTo(period2.GetHashCode()));
}

Comment on lines +129 to +192
[Test]
public void Contains_Distinct_Expected()
{
var left = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 20));
var right = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 10));
Assert.That(left.Contains(right), Is.True);
}

[Test]
public void Overlaps_Distinct_Expected()
{
var left = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 20));
var right = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 2, 10));
Assert.That(left.Overlaps(right), Is.True);
}

[Test]
public void Meets_Distinct_Expected()
{
var left = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 20));
var right = new CustomPeriod(new DateOnly(2024, 1, 21), new DateOnly(2024, 2, 10));
Assert.That(left.Meets(right), Is.True);
}

[Test]
public void Succeeds_Distinct_Expected()
{
var left = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 20));
var right = new CustomPeriod(new DateOnly(2024, 1, 25), new DateOnly(2024, 2, 10));
Assert.That(left.Succeeds(right), Is.False);
}

[Test]
public void Precedes_Distinct_Expected()
{
var left = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 20));
var right = new CustomPeriod(new DateOnly(2024, 1, 25), new DateOnly(2024, 2, 10));
Assert.That(left.Precedes(right), Is.True);
}

[Test]
public void Intersect_Distinct_Expected()
{
var left = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 20));
var right = new CustomPeriod(new DateOnly(2024, 1, 15), new DateOnly(2024, 2, 10));
Assert.That(left.Intersect(right), Is.EqualTo(new CustomPeriod(new DateOnly(2024, 1, 15), new DateOnly(2024, 1, 20))));
}

[Test]
public void Span_Distinct_Expected()
{
var left = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 20));
var right = new CustomPeriod(new DateOnly(2024, 1, 25), new DateOnly(2024, 2, 10));
Assert.That(left.Span(right), Is.EqualTo(new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 2, 10))));
}

[Test]
public void Gap_Distinct_Expected()
{
var left = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 20));
var right = new CustomPeriod(new DateOnly(2024, 1, 25), new DateOnly(2024, 2, 10));
Assert.That(left.Gap(right), Is.EqualTo(4));
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance period manipulation test coverage.

Each period operation needs more test cases to cover:

  • Edge cases (e.g., adjacent periods, single-day periods)
  • Negative cases (non-overlapping, non-meeting periods)
  • Invalid inputs (null periods)

Example additional test cases:

[Test]
public void Contains_SingleDay_Expected()
{
    var container = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31));
    var contained = new CustomPeriod(new DateOnly(2024, 1, 15), new DateOnly(2024, 1, 15));
    Assert.That(container.Contains(contained), Is.True);
}

[Test]
public void Overlaps_NoOverlap_Expected()
{
    var left = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 10));
    var right = new CustomPeriod(new DateOnly(2024, 1, 20), new DateOnly(2024, 1, 31));
    Assert.That(left.Overlaps(right), Is.False);
}

[Test]
public void Intersect_NullPeriod_ThrowsArgumentNullException()
{
    var period = new CustomPeriod(new DateOnly(2024, 1, 1), new DateOnly(2024, 1, 31));
    Assert.Throws<ArgumentNullException>(() => period.Intersect(null));
}

@Seddryck Seddryck merged commit 4a476a5 into main Feb 23, 2025
5 of 6 checks passed
@Seddryck Seddryck deleted the feat/iperiod-methods branch February 23, 2025 12:23
@Seddryck Seddryck changed the title feat: CustomPeriod to handle any flavour of period feat: CustomPeriod and additional methods to handle any flavour of period Feb 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants