Skip to content

Classify nullable date-range overlap state

Classify the relationship between two open-ended-capable date ranges into one of ten specific overlap categories. This operation is triggered when a consumer calls NullableDateRange.GetOverlapState(rangeA, rangeB).

Field Value
Operation ID OP-03
Trigger A consumer calls NullableDateRange.GetOverlapState(rangeA, rangeB).
Inputs rangeA, rangeB (NullableDateRange, each with a required StartDate and nullable EndDate)
Outputs A DateRangeOverlapResult enum value.
Primary components NullableDateRange.GetOverlapState
Entry point src/SAIF.NetCore.Common/SAIF.NetCore.Common/SAIF.NetCore.Common/Types/NullableDateRange.cs

Steps

The method tests nine branches in source order and returns on the first match, falling back to NoOverlap if none match:

  1. If neither range has an end date (!rangeA.EndDate.HasValue && !rangeB.EndDate.HasValue): return NeitherEnds, regardless of the start dates — this branch runs before any start-date comparison, so two mutually open-ended ranges with identical, differing, or either-order start dates all return NeitherEnds.
  2. Else if range A has no end date, range B does, and A's start falls strictly between B's start and end: return RangeADoesNotEnd_RangeABetweenRangeBStartAndEnd.
  3. Else if range A has no end date, range B does, and A's start falls strictly before B's start: return RangeADoesNotEnd_RangeAStartBeforeRangeBStart.
  4. Else if range A has an end date, range B does not, and A's start falls strictly after B's start: return RangeBDoesNotEnd_RangeAStartAfterRangeBStart.
  5. Else if range A has an end date, range B does not, A's start falls strictly before B's start, and A's end falls strictly after B's start: return RangeBDoesNotEnd_RangeAStartBeforeRangeBStart_RangeAEndAfterRangeBStart.
  6. Else if (both ranges have end dates, implicitly) A's start is strictly after B's start and A's end is strictly after B's end: return RangeAStartAfterRangeBStart_RangeAEndAfterRangeBEnd.
  7. Else if A's start is strictly after B's start and A's end is strictly before B's end: return RangeAStartAfterRangeBStart_RangeAEndBeforeRangeBEnd.
  8. Else if A's start is strictly before B's start and A's end is strictly after B's end: return RangeAStartBeforeRangeBStart_RangeAEndAfterRangeBEnd.
  9. Else if A's start is strictly before B's start and A's end is strictly before B's end: return RangeAStartBeforeRangeBStart_RangeAEndBeforeRangeBEnd.
  10. Else: return NoOverlap (the enum's default value) — reached whenever none of the nine predicates in branches 1–9 match. Because every branch from 2–9 uses strict </> comparisons (never <=/>=), several distinct shapes fall through to this default; the following are illustrative examples, not an exhaustive list:

    • Equal start dates on two closed (or one-open/one-closed) ranges, since no branch 2–9 tests == on the start date — this does not apply when both ranges are open-ended, because branch 1 matches first and returns NeitherEnds before any start-date comparison runs.
    • Equal end dates with different closed start dates — for example A = [Jan 2, Jan 5], B = [Jan 1, Jan 5] has A.StartDate > B.StartDate but A.EndDate == B.EndDate, which satisfies neither branch 6 (A.EndDate > B.EndDate) nor branch 7 (A.EndDate < B.EndDate), so it returns NoOverlap even though the two ranges visibly overlap.
    • Open/closed boundary ties — when one range is open-ended and its start date exactly equals the closed range's start or end date, branches 2–5's strict comparisons also fail to match, producing the same fallthrough.
    • Neither range's start/end lines up with the other's at all — for example A = [Jan 10, null] (open-ended), B = [Jan 1, Jan 2] (closed) satisfies neither branch 2 (A's start is not strictly between B's start and end) nor branch 3 (A's start is not strictly before B's start), and falls through with no boundary tie present; the mirrored closed-before-open case falls through the same way.

    Conversely, not every mismatched or even fully disjoint pair falls through: two disjoint closed ranges such as A = [Jan 10, Jan 11] and B = [Jan 1, Jan 2] satisfy branch 6 (A.StartDate > B.StartDate and A.EndDate > B.EndDate) and return RangeAStartAfterRangeBStart_RangeAEndAfterRangeBEnd directly, with no fallthrough at all.

The ten enum member names are not a reliable interval-intersection test. NoOverlap can be returned for ranges that do overlap (the equal-end example above), and a compound name such as RangeAStartAfterRangeBStart_RangeAEndAfterRangeBEnd is returned for genuinely disjoint ranges (the Jan 10–11 / Jan 1–2 example above) just as readily as for ranges that actually overlap. A caller that needs a true overlap/intersection test must not infer it from the result name alone.

The real observed behavior therefore diverges from a naive "same start date is always ambiguous" reading of interval semantics: when both ranges are open-ended, branch 1 wins first and the result is always NeitherEnds, irrespective of the start dates; the fallthrough shapes above are representative examples, not an exhaustive enumeration of every case that reaches branch 10.

Process flow

The following diagram visualizes the real ordered branch predicates, in source order.

flowchart TD
    Start([Call GetOverlapState]) --> B1{Both EndDate null?}
    B1 -->|Yes| NeitherEnds([Return NeitherEnds])
    B1 -->|No| B2{A no-end, B ends,<br>B.Start < A.Start < B.End?}
    B2 -->|Yes| R2(["Return RangeADoesNotEnd_<br>RangeABetweenRangeBStartAndEnd"])
    B2 -->|No| B3{A no-end, B ends,<br>A.Start < B.Start?}
    B3 -->|Yes| R3(["Return RangeADoesNotEnd_<br>RangeAStartBeforeRangeBStart"])
    B3 -->|No| B4{A ends, B no-end,<br>A.Start > B.Start?}
    B4 -->|Yes| R4(["Return RangeBDoesNotEnd_<br>RangeAStartAfterRangeBStart"])
    B4 -->|No| B5{A ends, B no-end,<br>A.Start < B.Start < A.End?}
    B5 -->|Yes| R5(["Return RangeBDoesNotEnd_RangeAStartBeforeRangeBStart_<br>RangeAEndAfterRangeBStart"])
    B5 -->|No| B6{Both end,<br>A.Start > B.Start, A.End > B.End?}
    B6 -->|Yes| R6(["Return RangeAStartAfterRangeBStart_<br>RangeAEndAfterRangeBEnd"])
    B6 -->|No| B7{A.Start > B.Start, A.End < B.End?}
    B7 -->|Yes| R7(["Return RangeAStartAfterRangeBStart_<br>RangeAEndBeforeRangeBEnd"])
    B7 -->|No| B8{A.Start < B.Start, A.End > B.End?}
    B8 -->|Yes| R8(["Return RangeAStartBeforeRangeBStart_<br>RangeAEndAfterRangeBEnd"])
    B8 -->|No| B9{A.Start < B.Start, A.End < B.End?}
    B9 -->|Yes| R9(["Return RangeAStartBeforeRangeBStart_<br>RangeAEndBeforeRangeBEnd"])
    B9 -->|No| NoOverlap([Return NoOverlap - default])

Failure and edge-case behavior

  • Two mutually open-ended ranges (EndDate == null for both) always return NeitherEnds, even when their start dates are identical — this is resolved by the first branch, before any start-date logic runs, and is not the "ambiguous" case a reader might assume.
  • NoOverlap (the default value) is returned for a broader set of cases than "equal start dates" alone — reached whenever none of the nine predicates in branches 1–9 match. Illustrative (non-exhaustive) examples: equal start dates on two closed (or one-open/one-closed) ranges (not applicable when both ranges are open-ended, since branch 1 matches first), equal end dates with different closed start dates (for example A = [Jan 2, Jan 5], B = [Jan 1, Jan 5], which visibly overlaps but still returns NoOverlap), open/closed boundary ties where the open range's start matches the closed range's start or end, and non-overlapping open/closed pairs with no boundary tie at all (for example A = [Jan 10, null], B = [Jan 1, Jan 2]) — every branch 2–9 uses strict </> comparisons.*
  • The ten DateRangeOverlapResult member names do not constitute a reliable interval-intersection test: NoOverlap can be returned for overlapping ranges, and a compound name describing one range starting and ending after another (for example RangeAStartAfterRangeBStart_RangeAEndAfterRangeBEnd) is returned for fully disjoint closed ranges (A = [Jan 10, Jan 11], B = [Jan 1, Jan 2]) exactly as it would be for overlapping ones.*
  • GetOverlapState does not null-check rangeA/rangeB themselves; a null argument throws NullReferenceException on the first property access.*

Note: Statements marked with * were inferred from code structure, naming conventions, or contextual evidence where direct confirmation was not available in source artifacts. Statements marked with ** could not be confirmed; treat as unknown until verified.

Sources

  • src/SAIF.NetCore.Common/SAIF.NetCore.Common/SAIF.NetCore.Common/Types/NullableDateRange.cs