Skip to main content

ixa/entity/entity_set/
entity_set.rs

1//! A lazy, composable set type built from set-algebraic expressions.
2//!
3//! An [`EntitySet`] represents a set of [`EntityId`] values as a tree of union,
4//! intersection, and difference operations over leaf [`SourceSet`] nodes. The tree
5//! is constructed eagerly but evaluated lazily: membership tests ([`contains`]) walk
6//! the tree on demand, and iteration is deferred to [`EntitySetIterator`].
7//!
8//! Construction methods reorder operands by estimated size to improve
9//! short-circuit performance and apply only minimal structural simplification.
10//!
11//! [`contains`]: EntitySet::contains
12
13use std::borrow::Borrow;
14use std::ops::Range;
15
16use log::warn;
17use rand::Rng;
18
19use super::{EntitySetIterator, SourceSet};
20use crate::entity::{Entity, EntityId};
21use crate::random::{
22    count_and_sample_single_l_reservoir, sample_multiple_from_known_length,
23    sample_multiple_l_reservoir, sample_single_excluding_l_reservoir, sample_single_l_reservoir,
24};
25
26/// Opaque public wrapper around the internal set-expression tree.
27pub struct EntitySet<'a, E: Entity>(EntitySetInner<'a, E>);
28
29/// Internal set-expression tree used to represent composed query sources.
30pub(super) enum EntitySetInner<'a, E: Entity> {
31    Source(SourceSet<'a, E>),
32    Union(Box<EntitySet<'a, E>>, Box<EntitySet<'a, E>>),
33    Intersection(Vec<EntitySet<'a, E>>),
34    Difference(Box<EntitySet<'a, E>>, Box<EntitySet<'a, E>>),
35}
36
37impl<'a, E: Entity> Clone for EntitySet<'a, E> {
38    fn clone(&self) -> Self {
39        Self(self.0.clone())
40    }
41}
42
43impl<'a, E: Entity> Clone for EntitySetInner<'a, E> {
44    fn clone(&self) -> Self {
45        match self {
46            Self::Source(source) => Self::Source(source.clone()),
47            Self::Union(left, right) => Self::Union(left.clone(), right.clone()),
48            Self::Intersection(sets) => Self::Intersection(sets.clone()),
49            Self::Difference(left, right) => Self::Difference(left.clone(), right.clone()),
50        }
51    }
52}
53
54impl<'a, E: Entity> Default for EntitySet<'a, E> {
55    fn default() -> Self {
56        Self::empty()
57    }
58}
59
60impl<'a, E: Entity> EntitySet<'a, E> {
61    pub(super) fn into_inner(self) -> EntitySetInner<'a, E> {
62        self.0
63    }
64
65    pub(super) fn is_source_leaf(&self) -> bool {
66        matches!(self.0, EntitySetInner::Source(_))
67    }
68
69    pub(super) fn into_source_leaf(self) -> Option<SourceSet<'a, E>> {
70        match self.0 {
71            EntitySetInner::Source(source) => Some(source),
72            _ => None,
73        }
74    }
75
76    /// Create an empty entity set.
77    #[must_use]
78    pub fn empty() -> Self {
79        EntitySet(EntitySetInner::Source(SourceSet::empty()))
80    }
81
82    /// Create an entity set from a single source set.
83    pub(crate) fn from_source(source: SourceSet<'a, E>) -> Self {
84        EntitySet(EntitySetInner::Source(source))
85    }
86
87    pub(crate) fn from_intersection_sources(mut sources: Vec<SourceSet<'a, E>>) -> Self {
88        match sources.len() {
89            0 => return Self::empty(),
90            1 => return Self::from_source(sources.pop().unwrap()),
91            _ => {}
92        }
93
94        // Keep intersections sorted smallest-to-largest so iterators can take the
95        // first source as the driver and membership checks short-circuit quickly.
96        sources.sort_unstable_by_key(SourceSet::sort_key);
97
98        let sets = sources.into_iter().map(Self::from_source).collect();
99
100        EntitySet(EntitySetInner::Intersection(sets))
101    }
102
103    #[must_use]
104    pub fn union(self, other: Self) -> Self {
105        // Idempotence: A ∪ A = A  (same structure over same sources)
106        if self.structurally_eq(&other) {
107            return self;
108        }
109
110        // Adjacent or overlapping intervals
111        if let (Some(a), Some(b)) = (self.as_range(), other.as_range()) {
112            if a.start <= b.end && b.start <= a.end {
113                return Self::from_source(SourceSet::population_range(
114                    a.start.min(b.start)..a.end.max(b.end),
115                ));
116            }
117        }
118
119        // Union with empty set is identity: A ∪ ∅ = ∅ ∪ A = A
120        match (self.is_empty(), other.is_empty()) {
121            (true, _) => return other,
122            (_, true) => return self,
123            _ => { /* pass */ }
124        }
125
126        // Larger set on LHS: more likely to short-circuit `||`.
127        let (left, right) = if self.sort_key() >= other.sort_key() {
128            (self, other)
129        } else {
130            (other, self)
131        };
132        EntitySet(EntitySetInner::Union(Box::new(left), Box::new(right)))
133    }
134
135    #[must_use]
136    pub fn intersection(self, other: Self) -> Self {
137        // Idempotence: A ∩ A = A
138        if self.structurally_eq(&other) {
139            return self;
140        }
141
142        // Intersection of overlapping intervals
143        if let (Some(a), Some(b)) = (self.as_range(), other.as_range()) {
144            let overlap = a.start.max(b.start)..a.end.min(b.end);
145            return if overlap.is_empty() {
146                Self::empty()
147            } else {
148                Self::from_source(SourceSet::population_range(overlap))
149            };
150        }
151
152        // Intersection an empty set is empty: A ∩ ∅ = ∅ ∩ A = ∅
153        match (self.is_empty(), other.is_empty()) {
154            (true, _) => return self,
155            (_, true) => return other,
156            _ => { /* pass */ }
157        }
158
159        let mut sets = match self {
160            EntitySet(EntitySetInner::Intersection(sets)) => sets,
161            _ => vec![self],
162        };
163
164        sets.push(other);
165        // Keep intersections sorted smallest-to-largest so iterators can take the
166        // first source as the driver and membership checks short-circuit quickly.
167        sets.sort_unstable_by_key(EntitySet::sort_key);
168        EntitySet(EntitySetInner::Intersection(sets))
169    }
170
171    #[must_use]
172    pub fn difference(self, other: Self) -> Self {
173        // Self-subtraction: A \ A = ∅
174        if self.structurally_eq(&other) {
175            return Self::empty();
176        }
177
178        if let (Some(a), Some(b)) = (self.as_range(), other.as_range()) {
179            let overlap = a.start.max(b.start)..a.end.min(b.end);
180            // Disjoint ranges leave the left operand unchanged.
181            if overlap.is_empty() {
182                return Self::from_source(SourceSet::population_range(a));
183            }
184            // A covering subtraction removes the entire left range.
185            if overlap.start == a.start && overlap.end == a.end {
186                return Self::empty();
187            }
188            // Trimming the left edge still leaves one contiguous suffix.
189            if overlap.start == a.start {
190                return Self::from_source(SourceSet::population_range(overlap.end..a.end));
191            }
192            // Trimming the right edge still leaves one contiguous prefix.
193            if overlap.end == a.end {
194                return Self::from_source(SourceSet::population_range(a.start..overlap.start));
195            }
196            // An interior subtraction would split the range, so keep the generic difference node.
197        }
198
199        // Subtraction involving an empty set is identity: A \ ∅ = A, ∅ \ A = ∅
200        if self.is_empty() || other.is_empty() {
201            return self;
202        }
203
204        EntitySet(EntitySetInner::Difference(Box::new(self), Box::new(other)))
205    }
206
207    /// Test whether `entity_id` is a member of this set.
208    #[must_use]
209    pub fn contains(&self, entity_id: EntityId<E>) -> bool {
210        match self {
211            EntitySet(EntitySetInner::Source(source)) => source.contains(entity_id),
212            EntitySet(EntitySetInner::Union(a, b)) => {
213                a.contains(entity_id) || b.contains(entity_id)
214            }
215            EntitySet(EntitySetInner::Intersection(sets)) => {
216                sets.iter().all(|set| set.contains(entity_id))
217            }
218            EntitySet(EntitySetInner::Difference(a, b)) => {
219                a.contains(entity_id) && !b.contains(entity_id)
220            }
221        }
222    }
223
224    /// Collect this set's contents into an owned vector of `EntityId<E>`.
225    #[must_use]
226    pub fn to_owned_vec(self) -> Vec<EntityId<E>> {
227        self.into_iter().collect()
228    }
229
230    /// Sample a single entity uniformly from this set, excluding any entity
231    /// equal to `excluded`. Returns `None` if the set is empty or contains
232    /// only the excluded entity.
233    ///
234    /// For source-leaf sets with random-access backing (`PopulationRange`,
235    /// `IndexSet`), runs in O(1) with at most two index lookups and no
236    /// iterator construction. Falls back to O(n) reservoir sampling for
237    /// composite sets and `PropertySet` sources.
238    #[must_use]
239    pub fn sample_entity_excluding<R, X>(&self, rng: &mut R, excluded: X) -> Option<EntityId<E>>
240    where
241        R: Rng,
242        X: Borrow<EntityId<E>>,
243    {
244        let excluded = *excluded.borrow();
245        if let Some(n) = self.try_len() {
246            if n == 0 {
247                return None;
248            }
249            let p = rng.random_range(0..n as u32) as usize;
250            let candidate = self.try_nth(p)?;
251            if candidate != excluded {
252                return Some(candidate);
253            }
254            // `excluded` is at position `p`. Resample from the n-1 remaining
255            // positions: pick `k` uniform in `[0, n-1)`, then map it around
256            // the hole at `p`.
257            if n == 1 {
258                return None;
259            }
260            let k = rng.random_range(0..(n - 1) as u32) as usize;
261            let target = if k < p { k } else { k + 1 };
262            return self.try_nth(target);
263        }
264        sample_single_excluding_l_reservoir(rng, self.clone(), excluded)
265    }
266
267    /// Sample a single entity uniformly from this set. Returns `None` if the
268    /// set is empty.
269    #[must_use]
270    pub fn sample_entity<R>(&self, rng: &mut R) -> Option<EntityId<E>>
271    where
272        R: Rng,
273    {
274        if let Some(n) = self.try_len() {
275            if n == 0 {
276                warn!("Requested a sample entity from an empty population");
277                return None;
278            }
279            // The `u32` cast makes this function 30% faster than `usize`.
280            let index = rng.random_range(0..n as u32) as usize;
281            return self.try_nth(index);
282        }
283        sample_single_l_reservoir(rng, self.clone())
284    }
285
286    /// Count the entities in this set and sample one uniformly from them.
287    ///
288    /// Returns `(count, sample)` where `sample` is `None` iff `count == 0`.
289    #[must_use]
290    pub fn count_and_sample_entity<R>(&self, rng: &mut R) -> (usize, Option<EntityId<E>>)
291    where
292        R: Rng,
293    {
294        if let Some(n) = self.try_len() {
295            if n == 0 {
296                return (0, None);
297            }
298            let index = rng.random_range(0..n as u32) as usize;
299            return (n, self.try_nth(index));
300        }
301        count_and_sample_single_l_reservoir(rng, self.clone())
302    }
303
304    /// Sample up to `requested` entities uniformly from this set. If the set
305    /// has fewer than `requested` entities, the entire set is returned.
306    #[must_use]
307    pub fn sample_entities<R>(&self, rng: &mut R, requested: usize) -> Vec<EntityId<E>>
308    where
309        R: Rng,
310    {
311        match self.try_len() {
312            Some(0) => {
313                warn!("Requested a sample of entities from an empty population");
314                vec![]
315            }
316            Some(_) => sample_multiple_from_known_length(rng, self.clone(), requested),
317            None => sample_multiple_l_reservoir(rng, self.clone(), requested),
318        }
319    }
320
321    /// Returns `Some(length)` only when the set length is trivially known.
322    ///
323    /// This is true only for direct `SourceSet` leaves except `PropertySet`.
324    /// Composite expressions return `None`.
325    #[must_use]
326    pub fn try_len(&self) -> Option<usize> {
327        match self {
328            EntitySet(EntitySetInner::Source(source)) => source.try_len(),
329            _ => None,
330        }
331    }
332
333    /// Random-access lookup. Defined for the same variants as `try_len`.
334    fn try_nth(&self, idx: usize) -> Option<EntityId<E>> {
335        match self {
336            EntitySet(EntitySetInner::Source(source)) => source.try_nth(idx),
337            _ => None,
338        }
339    }
340
341    fn as_range(&self) -> Option<Range<usize>> {
342        match self {
343            EntitySet(EntitySetInner::Source(SourceSet::PopulationRange(range))) => {
344                Some(range.clone())
345            }
346            _ => None,
347        }
348    }
349
350    fn is_empty(&self) -> bool {
351        match self {
352            EntitySet(EntitySetInner::Source(SourceSet::PopulationRange(range))) => {
353                range.is_empty()
354            }
355            _ => false,
356        }
357    }
358
359    fn sort_key(&self) -> (usize, u8) {
360        match self {
361            EntitySet(EntitySetInner::Source(source)) => source.sort_key(),
362            EntitySet(EntitySetInner::Union(left, right)) => {
363                // Union upper bound is additive; cost hint tracks the cheaper side.
364                let (left_upper, left_hint) = left.sort_key();
365                let (right_upper, right_hint) = right.sort_key();
366                (
367                    left_upper.saturating_add(right_upper),
368                    left_hint.min(right_hint),
369                )
370            }
371            EntitySet(EntitySetInner::Intersection(sets)) => {
372                let mut upper = usize::MAX;
373                let mut hint = 0u8;
374                for set in sets {
375                    let (set_upper, set_hint) = set.sort_key();
376                    upper = upper.min(set_upper);
377                    hint = hint.saturating_add(set_hint);
378                }
379                if upper == usize::MAX {
380                    upper = 0;
381                }
382                (upper, hint)
383            }
384            EntitySet(EntitySetInner::Difference(left, right)) => {
385                let (left_upper, left_hint) = left.sort_key();
386                let (_, right_hint) = right.sort_key();
387                (left_upper, left_hint.saturating_add(right_hint))
388            }
389        }
390    }
391
392    /// Structural equality check: same tree shape with same sources at leaves.
393    fn structurally_eq(&self, other: &Self) -> bool {
394        match (self, other) {
395            (EntitySet(EntitySetInner::Source(a)), EntitySet(EntitySetInner::Source(b))) => a == b,
396            (
397                EntitySet(EntitySetInner::Union(a1, a2)),
398                EntitySet(EntitySetInner::Union(b1, b2)),
399            )
400            | (
401                EntitySet(EntitySetInner::Difference(a1, a2)),
402                EntitySet(EntitySetInner::Difference(b1, b2)),
403            ) => a1.structurally_eq(b1) && a2.structurally_eq(b2),
404            (
405                EntitySet(EntitySetInner::Intersection(a_sets)),
406                EntitySet(EntitySetInner::Intersection(b_sets)),
407            ) => {
408                a_sets.len() == b_sets.len()
409                    && a_sets
410                        .iter()
411                        .zip(b_sets.iter())
412                        .all(|(a_set, b_set)| a_set.structurally_eq(b_set))
413            }
414            _ => false,
415        }
416    }
417}
418
419impl<'a, E: Entity> IntoIterator for EntitySet<'a, E> {
420    type Item = EntityId<E>;
421    type IntoIter = EntitySetIterator<'a, E>;
422
423    fn into_iter(self) -> Self::IntoIter {
424        EntitySetIterator::new(self)
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use rand::rngs::StdRng;
431    use rand::SeedableRng;
432
433    use super::*;
434    use crate::entity::ContextEntitiesExt;
435    use crate::hashing::IndexSet;
436    use crate::{
437        define_derived_property, define_entity, define_multi_property, define_property, with,
438        Context,
439    };
440
441    define_entity!(Person);
442    define_property!(struct Age(u8), Person);
443    define_property!(struct County(u32), Person, default_const = County(0));
444    define_derived_property!(struct Senior(bool), Person, [Age], |age| Senior(age.0 >= 65));
445    define_multi_property!(Person, (Age, County));
446    define_multi_property!(Person, (County, Age));
447
448    fn finite_set(ids: &[usize]) -> IndexSet<EntityId<Person>> {
449        ids.iter()
450            .copied()
451            .map(EntityId::<Person>::new)
452            .collect::<IndexSet<_>>()
453    }
454
455    fn as_entity_set(set: &IndexSet<EntityId<Person>>) -> EntitySet<Person> {
456        EntitySet::from_source(SourceSet::IndexSet(set))
457    }
458
459    #[test]
460    fn from_source_empty_is_empty() {
461        let es = EntitySet::<Person>::empty();
462        assert_eq!(es.sort_key().0, 0);
463        for value in 0..10 {
464            assert!(!es.contains(EntityId::<Person>::new(value)));
465        }
466    }
467
468    #[test]
469    fn from_source_population_ranges() {
470        let population = EntitySet::from_source(SourceSet::<Person>::population_range(0..3));
471        assert!(population.contains(EntityId::<Person>::new(0)));
472        assert!(population.contains(EntityId::<Person>::new(2)));
473        assert!(!population.contains(EntityId::<Person>::new(3)));
474        assert_eq!(population.sort_key().0, 3);
475
476        let singleton = EntitySet::from_source(SourceSet::<Person>::singleton(EntityId::new(5)));
477        assert!(singleton.contains(EntityId::<Person>::new(5)));
478        assert!(!singleton.contains(EntityId::<Person>::new(4)));
479        assert_eq!(singleton.sort_key().0, 1);
480
481        let range = EntitySet::from_source(SourceSet::<Person>::population_range(2..5));
482        assert!(range.contains(EntityId::<Person>::new(2)));
483        assert!(range.contains(EntityId::<Person>::new(4)));
484        assert!(!range.contains(EntityId::<Person>::new(1)));
485        assert!(!range.contains(EntityId::<Person>::new(5)));
486        assert_eq!(range.try_len(), Some(3));
487    }
488
489    #[test]
490    fn union_basic_behavior_without_legacy_reductions() {
491        let a = finite_set(&[1, 2, 3]);
492        let e = EntitySet::<Person>::empty();
493        let u = EntitySet::from_source(SourceSet::<Person>::population_range(0..10));
494
495        let a_union_empty = as_entity_set(&a).union(e);
496        assert!(a_union_empty.contains(EntityId::<Person>::new(1)));
497        assert!(!a_union_empty.contains(EntityId::<Person>::new(4)));
498
499        let u_union_a = u.union(as_entity_set(&a));
500        assert!(u_union_a.contains(EntityId::<Person>::new(0)));
501        assert!(u_union_a.contains(EntityId::<Person>::new(9)));
502        assert!(!u_union_a.contains(EntityId::<Person>::new(10)));
503    }
504
505    #[test]
506    fn union_range_optimizations() {
507        let adjacent = EntitySet::from_source(SourceSet::<Person>::population_range(0..3)).union(
508            EntitySet::from_source(SourceSet::<Person>::population_range(3..6)),
509        );
510        assert!(matches!(
511            adjacent,
512            EntitySet(EntitySetInner::Source(SourceSet::PopulationRange(ref range))) if range == &(0..6)
513        ));
514
515        let overlapping = EntitySet::from_source(SourceSet::<Person>::population_range(2..6))
516            .union(EntitySet::from_source(
517                SourceSet::<Person>::population_range(4..8),
518            ));
519        assert!(matches!(
520            overlapping,
521            EntitySet(EntitySetInner::Source(SourceSet::PopulationRange(ref range))) if range == &(2..8)
522        ));
523
524        let disjoint = EntitySet::from_source(SourceSet::<Person>::singleton(EntityId::new(1)))
525            .union(EntitySet::from_source(SourceSet::<Person>::singleton(
526                EntityId::new(4),
527            )));
528        assert!(matches!(disjoint, EntitySet(EntitySetInner::Union(_, _))));
529    }
530
531    #[test]
532    fn intersection_range_optimizations() {
533        let overlap = EntitySet::from_source(SourceSet::<Person>::population_range(2..6))
534            .intersection(EntitySet::from_source(
535                SourceSet::<Person>::population_range(4..8),
536            ));
537        assert!(matches!(
538            overlap,
539            EntitySet(EntitySetInner::Source(SourceSet::PopulationRange(ref range))) if range == &(4..6)
540        ));
541
542        let empty = EntitySet::from_source(SourceSet::<Person>::population_range(1..3))
543            .intersection(EntitySet::from_source(
544                SourceSet::<Person>::population_range(5..7),
545            ));
546        assert!(matches!(
547            empty,
548            EntitySet(EntitySetInner::Source(SourceSet::PopulationRange(ref range))) if range == &(0..0)
549        ));
550
551        let indexed_ids = finite_set(&[1, 2, 3]);
552        let mixed = EntitySet::from_source(SourceSet::<Person>::singleton(EntityId::new(2)))
553            .intersection(as_entity_set(&indexed_ids));
554        assert!(mixed.contains(EntityId::<Person>::new(2)));
555        assert!(!mixed.contains(EntityId::<Person>::new(1)));
556    }
557
558    #[test]
559    fn difference_range_optimizations() {
560        let unchanged = EntitySet::from_source(SourceSet::<Person>::population_range(2..6))
561            .difference(EntitySet::from_source(
562                SourceSet::<Person>::population_range(8..10),
563            ));
564        assert!(matches!(
565            unchanged,
566            EntitySet(EntitySetInner::Source(SourceSet::PopulationRange(ref range))) if range == &(2..6)
567        ));
568
569        let empty = EntitySet::from_source(SourceSet::<Person>::population_range(2..6)).difference(
570            EntitySet::from_source(SourceSet::<Person>::population_range(1..7)),
571        );
572        assert!(matches!(
573            empty,
574            EntitySet(EntitySetInner::Source(SourceSet::PopulationRange(ref range))) if range == &(0..0)
575        ));
576
577        let trim_left = EntitySet::from_source(SourceSet::<Person>::population_range(2..6))
578            .difference(EntitySet::from_source(
579                SourceSet::<Person>::population_range(1..4),
580            ));
581        assert!(matches!(
582            trim_left,
583            EntitySet(EntitySetInner::Source(SourceSet::PopulationRange(ref range))) if range == &(4..6)
584        ));
585
586        let trim_right = EntitySet::from_source(SourceSet::<Person>::population_range(2..6))
587            .difference(EntitySet::from_source(
588                SourceSet::<Person>::population_range(4..8),
589            ));
590        assert!(matches!(
591            trim_right,
592            EntitySet(EntitySetInner::Source(SourceSet::PopulationRange(ref range))) if range == &(2..4)
593        ));
594
595        let split = EntitySet::from_source(SourceSet::<Person>::population_range(2..8)).difference(
596            EntitySet::from_source(SourceSet::<Person>::population_range(4..6)),
597        );
598        assert!(matches!(split, EntitySet(EntitySetInner::Difference(_, _))));
599        assert!(split.contains(EntityId::<Person>::new(2)));
600        assert!(split.contains(EntityId::<Person>::new(7)));
601        assert!(!split.contains(EntityId::<Person>::new(4)));
602        assert!(!split.contains(EntityId::<Person>::new(5)));
603    }
604
605    #[test]
606    fn difference_is_not_commutative() {
607        let a = finite_set(&[1, 2, 3]);
608        let b = finite_set(&[2, 3, 4]);
609
610        let d1 = as_entity_set(&a).difference(as_entity_set(&b));
611        let c = finite_set(&[2, 3, 4]);
612        let d = finite_set(&[1, 2, 3]);
613        let d2 = as_entity_set(&c).difference(as_entity_set(&d));
614
615        assert!(d1.contains(EntityId::<Person>::new(1)));
616        assert!(!d1.contains(EntityId::<Person>::new(4)));
617        assert!(d2.contains(EntityId::<Person>::new(4)));
618        assert!(!d2.contains(EntityId::<Person>::new(1)));
619    }
620
621    #[test]
622    fn sort_key_rules() {
623        let a = finite_set(&[1, 2]);
624        let b = finite_set(&[2, 3, 4]);
625
626        let union = as_entity_set(&a).union(as_entity_set(&b));
627        assert_eq!(union.sort_key(), (a.len() + b.len(), 3));
628
629        let intersection = as_entity_set(&a).intersection(as_entity_set(&b));
630        assert_eq!(intersection.sort_key(), (a.len().min(b.len()), 6));
631
632        let difference = as_entity_set(&a).difference(as_entity_set(&b));
633        assert_eq!(difference.sort_key(), (a.len(), 6));
634    }
635
636    #[test]
637    fn compound_expressions_membership() {
638        let a = finite_set(&[1, 2, 3, 4]);
639        let b = finite_set(&[3, 4, 5]);
640        let c = finite_set(&[10, 20]);
641        let d = finite_set(&[20]);
642
643        let union_of_intersections = as_entity_set(&a)
644            .intersection(as_entity_set(&b))
645            .union(as_entity_set(&c).intersection(as_entity_set(&d)));
646        assert!(union_of_intersections.contains(EntityId::<Person>::new(3)));
647        assert!(union_of_intersections.contains(EntityId::<Person>::new(4)));
648        assert!(union_of_intersections.contains(EntityId::<Person>::new(20)));
649        assert!(!union_of_intersections.contains(EntityId::<Person>::new(5)));
650
651        let a2 = finite_set(&[1, 2, 3]);
652        let b2 = finite_set(&[3, 4, 5]);
653        let a3 = finite_set(&[1, 2, 3]);
654        let law = as_entity_set(&a3).intersection(as_entity_set(&a2).union(as_entity_set(&b2)));
655        assert!(law.contains(EntityId::<Person>::new(1)));
656        assert!(law.contains(EntityId::<Person>::new(2)));
657        assert!(law.contains(EntityId::<Person>::new(3)));
658        assert!(!law.contains(EntityId::<Person>::new(4)));
659    }
660
661    #[test]
662    fn clone_preserves_composite_expression_behavior() {
663        let a = finite_set(&[1, 2, 3, 4]);
664        let b = finite_set(&[3, 4, 5]);
665        let c = finite_set(&[2]);
666
667        let set = as_entity_set(&a)
668            .difference(as_entity_set(&c))
669            .union(as_entity_set(&b));
670        let cloned = set.clone();
671
672        for value in 0..7 {
673            let entity_id = EntityId::<Person>::new(value);
674            assert_eq!(set.contains(entity_id), cloned.contains(entity_id));
675        }
676
677        assert_eq!(
678            set.into_iter().collect::<Vec<_>>(),
679            cloned.into_iter().collect::<Vec<_>>()
680        );
681    }
682
683    #[test]
684    fn population_zero_is_empty() {
685        let es = EntitySet::from_source(SourceSet::<Person>::empty());
686        assert_eq!(es.sort_key().0, 0);
687        assert!(!es.contains(EntityId::<Person>::new(0)));
688    }
689
690    #[test]
691    fn try_len_known_only_for_non_property_sources() {
692        let empty = EntitySet::<Person>::from_source(SourceSet::empty());
693        assert_eq!(empty.try_len(), Some(0));
694
695        let singleton = EntitySet::<Person>::from_source(SourceSet::singleton(EntityId::new(42)));
696        assert_eq!(singleton.try_len(), Some(1));
697
698        let population = EntitySet::<Person>::from_source(SourceSet::population_range(0..5));
699        assert_eq!(population.try_len(), Some(5));
700
701        let range = EntitySet::<Person>::from_source(SourceSet::population_range(4..9));
702        assert_eq!(range.try_len(), Some(5));
703
704        let index_data = [EntityId::new(1), EntityId::new(2), EntityId::new(3)]
705            .into_iter()
706            .collect::<IndexSet<_>>();
707        let indexed = EntitySet::<Person>::from_source(SourceSet::IndexSet(&index_data));
708        assert_eq!(indexed.try_len(), Some(3));
709
710        let mut context = Context::new();
711        context.add_entity(with!(Person, Age(10))).unwrap();
712        let property_source = SourceSet::<Person>::new(Age(10), &context).unwrap();
713        assert!(matches!(property_source, SourceSet::PropertySet(_)));
714        let property_set = EntitySet::<Person>::from_source(property_source);
715        assert_eq!(property_set.try_len(), None);
716
717        let composed = EntitySet::<Person>::from_source(SourceSet::population_range(0..3))
718            .difference(EntitySet::from_source(SourceSet::singleton(EntityId::new(
719                1,
720            ))));
721        assert_eq!(composed.try_len(), None);
722    }
723
724    #[test]
725    fn range_leaf_works_inside_composite_expressions() {
726        let indexed_ids = finite_set(&[1, 3, 5, 8]);
727        let indexed = as_entity_set(&indexed_ids);
728        let range = EntitySet::from_source(SourceSet::<Person>::population_range(2..8));
729
730        let intersection = range.intersection(indexed);
731        assert!(!intersection.contains(EntityId::new(1)));
732        assert!(intersection.contains(EntityId::new(3)));
733        assert!(intersection.contains(EntityId::new(5)));
734        assert!(!intersection.contains(EntityId::new(8)));
735    }
736
737    #[test]
738    fn clone_preserves_unindexed_concrete_property_query_results() {
739        let mut context = Context::new();
740        let p1 = context.add_entity(with!(Person, Age(10))).unwrap();
741        let p2 = context.add_entity(with!(Person, Age(10))).unwrap();
742        let _p3 = context.add_entity(with!(Person, Age(11))).unwrap();
743
744        let set = context.query::<Person, _>(with!(Person, Age(10)));
745        assert_eq!(set.try_len(), None);
746        let cloned = set.clone();
747
748        let mut iter = set.into_iter();
749        assert_eq!(iter.next(), Some(p1));
750        assert_eq!(iter.collect::<Vec<_>>(), vec![p2]);
751
752        assert!(cloned.contains(p1));
753        assert!(cloned.contains(p2));
754        assert_eq!(cloned.into_iter().collect::<Vec<_>>(), vec![p1, p2]);
755    }
756
757    #[test]
758    fn clone_preserves_unindexed_derived_property_query_results() {
759        let mut context = Context::new();
760        let _p1 = context.add_entity(with!(Person, Age(64))).unwrap();
761        let p2 = context.add_entity(with!(Person, Age(65))).unwrap();
762        let p3 = context.add_entity(with!(Person, Age(90))).unwrap();
763
764        let set = context.query::<Person, _>(with!(Person, Senior(true)));
765        assert_eq!(set.try_len(), None);
766        let cloned = set.clone();
767
768        assert!(set.contains(p2));
769        assert!(set.contains(p3));
770        assert_eq!(set.into_iter().collect::<Vec<_>>(), vec![p2, p3]);
771        assert_eq!(cloned.into_iter().collect::<Vec<_>>(), vec![p2, p3]);
772    }
773
774    #[test]
775    fn union_of_same_unindexed_property_query_deduplicates_to_one_source() {
776        let mut context = Context::new();
777        let p1 = context.add_entity(with!(Person, Age(10))).unwrap();
778        let p2 = context.add_entity(with!(Person, Age(10))).unwrap();
779        let _p3 = context.add_entity(with!(Person, Age(11))).unwrap();
780
781        let query = context.query::<Person, _>(with!(Person, Age(10)));
782        let union = query.clone().union(query);
783
784        assert!(matches!(
785            union,
786            EntitySet(EntitySetInner::Source(SourceSet::PropertySet(_)))
787        ));
788        assert_eq!(union.into_iter().collect::<Vec<_>>(), vec![p1, p2]);
789    }
790
791    #[test]
792    fn union_of_equivalent_unindexed_multi_property_queries_deduplicates_to_one_source() {
793        let mut context = Context::new();
794        let matching = context
795            .add_entity(with!(Person, Age(28), County(7)))
796            .unwrap();
797        let _wrong_county = context
798            .add_entity(with!(Person, Age(28), County(8)))
799            .unwrap();
800        let _wrong_age = context
801            .add_entity(with!(Person, Age(29), County(7)))
802            .unwrap();
803
804        let age_county = context.query::<Person, _>(with!(Person, (Age(28), County(7))));
805        let county_age = context.query::<Person, _>(with!(Person, (County(7), Age(28))));
806        let union = age_county.union(county_age);
807
808        assert!(matches!(
809            union,
810            EntitySet(EntitySetInner::Source(SourceSet::PropertySet(_)))
811        ));
812        assert_eq!(union.into_iter().collect::<Vec<_>>(), vec![matching]);
813    }
814
815    #[test]
816    fn sample_entity_excluding_skips_excluded() {
817        let set = EntitySet::from_source(SourceSet::<Person>::PopulationRange(0..5));
818        let excluded = EntityId::<Person>::new(2);
819        let mut rng = StdRng::seed_from_u64(42);
820        for _ in 0..200 {
821            let sampled = set.sample_entity_excluding(&mut rng, excluded).unwrap();
822            assert_ne!(sampled, excluded);
823            assert!(sampled.0 < 5);
824        }
825    }
826
827    #[test]
828    fn sample_entity_excluding_returns_none_when_only_excluded_present() {
829        let only = EntityId::<Person>::new(7);
830        let single = finite_set(&[7]);
831        let mut rng = StdRng::seed_from_u64(42);
832        assert_eq!(
833            as_entity_set(&single).sample_entity_excluding(&mut rng, only),
834            None
835        );
836    }
837
838    #[test]
839    fn sample_entity_excluding_returns_none_on_empty() {
840        let mut rng = StdRng::seed_from_u64(42);
841        assert_eq!(
842            EntitySet::<Person>::empty()
843                .sample_entity_excluding(&mut rng, EntityId::<Person>::new(0)),
844            None
845        );
846    }
847
848    #[test]
849    fn sample_entity_excluding_excluded_not_in_set_uses_first_pick() {
850        // When `excluded` is outside the set, every sample is the first
851        // uniform pick — exercises the no-resample path.
852        let set = EntitySet::from_source(SourceSet::<Person>::PopulationRange(0..10));
853        let mut rng = StdRng::seed_from_u64(42);
854        for _ in 0..200 {
855            let sampled = set
856                .sample_entity_excluding(&mut rng, EntityId::<Person>::new(999))
857                .unwrap();
858            assert!(sampled.0 < 10);
859        }
860    }
861
862    #[test]
863    fn sample_entity_excluding_uniform_over_known_length() {
864        // Chi-square test on PopulationRange (known-length, fast path).
865        let excluded = EntityId::<Person>::new(7);
866        let set = EntitySet::from_source(SourceSet::<Person>::PopulationRange(0..20));
867        let num_runs = 50_000;
868        let mut counts = [0usize; 20];
869        let mut rng = StdRng::seed_from_u64(42);
870        for _ in 0..num_runs {
871            let id = set.sample_entity_excluding(&mut rng, excluded).unwrap();
872            counts[id.0] += 1;
873        }
874        assert_eq!(counts[excluded.0], 0);
875
876        let expected = num_runs as f64 / 19.0;
877        let chi_square: f64 = counts
878            .iter()
879            .enumerate()
880            .filter(|(i, _)| *i != excluded.0)
881            .map(|(_, &obs)| {
882                let diff = obs as f64 - expected;
883                diff * diff / expected
884            })
885            .sum();
886        // df = 18, χ²_{0.999} ≈ 42.31
887        assert!(chi_square < 42.31, "χ² = {chi_square}");
888    }
889}