@@ -530,4 +530,146 @@ defmodule Ash.Type.RangeTest do
530530 refute Range . empty? ( % Range { lower: nil , upper: nil } )
531531 end
532532 end
533+
534+ describe "contains?/2" do
535+ test "a bounded range holds values between its bounds" do
536+ range = % Ash.Range { lower: 1 , upper: 5 , bounds: :"[)" }
537+
538+ refute Ash.Range . contains? ( range , 0 )
539+ assert Ash.Range . contains? ( range , 1 )
540+ assert Ash.Range . contains? ( range , 4 )
541+ refute Ash.Range . contains? ( range , 5 )
542+ end
543+
544+ test "a bound holds its own value only when inclusive" do
545+ assert Ash.Range . contains? ( % Ash.Range { lower: 1 , upper: 5 , bounds: :"[]" } , 5 )
546+ refute Ash.Range . contains? ( % Ash.Range { lower: 1 , upper: 5 , bounds: :"()" } , 1 )
547+ assert Ash.Range . contains? ( % Ash.Range { lower: 1 , upper: 5 , bounds: :"(]" } , 5 )
548+ refute Ash.Range . contains? ( % Ash.Range { lower: 1 , upper: 5 , bounds: :"(]" } , 1 )
549+ end
550+
551+ test "an unbounded end holds everything beyond it" do
552+ assert Ash.Range . contains? ( % Ash.Range { lower: nil , upper: 5 } , - 1_000 )
553+ assert Ash.Range . contains? ( % Ash.Range { lower: 1 , upper: nil } , 1_000 )
554+ assert Ash.Range . contains? ( % Ash.Range { lower: nil , upper: nil } , 0 )
555+ end
556+
557+ test "an empty range holds nothing" do
558+ refute Ash.Range . contains? ( % Ash.Range { lower: 5 , upper: 5 , bounds: :"[)" } , 5 )
559+ refute Ash.Range . contains? ( % Ash.Range { lower: 9 , upper: 5 } , 7 )
560+ end
561+
562+ test "holds datetimes by Comp, not by term order" do
563+ range = % Ash.Range {
564+ lower: ~U[ 2026-01-31 00:00:00Z] ,
565+ upper: ~U[ 2026-03-01 00:00:00Z] ,
566+ bounds: :"[)"
567+ }
568+
569+ assert Ash.Range . contains? ( range , ~U[ 2026-02-01 00:00:00Z] )
570+ refute Ash.Range . contains? ( range , ~U[ 2026-03-02 00:00:00Z] )
571+ end
572+ end
573+
574+ describe "contains?/2 with a range" do
575+ # Cross-checked against Postgres 19: `@>` on int4range agrees case for case.
576+ test "a range holds one that lies within it, sharing an endpoint or equal included" do
577+ outer = % Range { lower: 1 , upper: 10 , bounds: :"[)" }
578+
579+ assert Range . contains? ( outer , % Range { lower: 3 , upper: 5 , bounds: :"[)" } )
580+ assert Range . contains? ( outer , % Range { lower: 1 , upper: 5 , bounds: :"[)" } )
581+ assert Range . contains? ( outer , % Range { lower: 5 , upper: 10 , bounds: :"[)" } )
582+ assert Range . contains? ( outer , outer )
583+ refute Range . contains? ( outer , % Range { lower: 5 , upper: 20 , bounds: :"[)" } )
584+ end
585+
586+ test "every range holds the empty range, and the empty range holds nothing else" do
587+ empty = Range . empty ( )
588+ range = % Range { lower: 1 , upper: 10 , bounds: :"[)" }
589+
590+ assert Range . contains? ( range , empty )
591+ assert Range . contains? ( empty , empty )
592+ refute Range . contains? ( empty , range )
593+ refute Range . contains? ( empty , 5 )
594+ end
595+
596+ test "an unbounded end holds everything beyond it" do
597+ assert Range . contains? (
598+ % Range { lower: nil , upper: nil , bounds: :"[)" } ,
599+ % Range { lower: 1 , upper: 10 , bounds: :"[)" }
600+ )
601+
602+ refute Range . contains? (
603+ % Range { lower: 1 , upper: 10 , bounds: :"[)" } ,
604+ % Range { lower: nil , upper: 10 , bounds: :"[)" }
605+ )
606+ end
607+ end
608+
609+ describe "intersects?/2" do
610+ test "two ranges sharing points intersect, in either order" do
611+ left = % Range { lower: 1 , upper: 5 , bounds: :"[)" }
612+ right = % Range { lower: 4 , upper: 9 , bounds: :"[)" }
613+
614+ assert Range . intersects? ( left , right )
615+ assert Range . intersects? ( right , left )
616+ end
617+
618+ test "a range contained in another intersects it" do
619+ assert Range . intersects? (
620+ % Range { lower: 1 , upper: 9 , bounds: :"[)" } ,
621+ % Range { lower: 3 , upper: 4 , bounds: :"[)" }
622+ )
623+ end
624+
625+ test "adjacent ranges do not intersect, which is what lets them tile" do
626+ refute Range . intersects? (
627+ % Range { lower: 1 , upper: 3 , bounds: :"[)" } ,
628+ % Range { lower: 3 , upper: 5 , bounds: :"[)" }
629+ )
630+ end
631+
632+ test "a shared boundary is a shared point only when both sides include it" do
633+ assert Range . intersects? (
634+ % Range { lower: 1 , upper: 3 , bounds: :"[]" } ,
635+ % Range { lower: 3 , upper: 5 , bounds: :"[)" }
636+ )
637+
638+ refute Range . intersects? (
639+ % Range { lower: 1 , upper: 3 , bounds: :"[]" } ,
640+ % Range { lower: 3 , upper: 5 , bounds: :"()" }
641+ )
642+ end
643+
644+ test "an unbounded end intersects everything beyond it" do
645+ assert Range . intersects? (
646+ % Range { lower: 1 , upper: nil , bounds: :"[)" } ,
647+ % Range { lower: 1_000 , upper: nil , bounds: :"[)" }
648+ )
649+
650+ assert Range . intersects? (
651+ % Range { lower: nil , upper: nil , bounds: :"[)" } ,
652+ % Range { lower: 3 , upper: 4 , bounds: :"[)" }
653+ )
654+ end
655+
656+ test "an empty range intersects nothing, not even itself" do
657+ empty = % Range { lower: 5 , upper: 5 , bounds: :"[)" }
658+
659+ refute Range . intersects? ( empty , empty )
660+ refute Range . intersects? ( empty , % Range { lower: nil , upper: nil , bounds: :"[)" } )
661+ end
662+
663+ test "compares datetimes by Comp, not by term order" do
664+ assert Range . intersects? (
665+ % Range { lower: ~U[ 2026-01-31 00:00:00Z] , upper: nil , bounds: :"[)" } ,
666+ % Range { lower: ~U[ 2026-02-01 00:00:00Z] , upper: nil , bounds: :"[)" }
667+ )
668+
669+ refute Range . intersects? (
670+ % Range { lower: ~U[ 2026-01-31 00:00:00Z] , upper: ~U[ 2026-02-01 00:00:00Z] } ,
671+ % Range { lower: ~U[ 2026-02-01 00:00:00Z] , upper: ~U[ 2026-03-01 00:00:00Z] }
672+ )
673+ end
674+ end
533675end
0 commit comments