1+ // Copyright (c) Microsoft Corporation.
2+ // Licensed under the MIT license.
3+
4+ namespace GarnetRoaringBitmap . Containers
5+ {
6+ /// <summary>
7+ /// Sorted-array container: stores up to <see cref="ArrayThreshold"/> 16-bit values in
8+ /// ascending order. Uses ~2 bytes per element; preferred representation when the
9+ /// container's cardinality is below the threshold because membership is O(log n)
10+ /// and the memory footprint scales with cardinality. Above the threshold the parent
11+ /// promotes the container to a <see cref="BitmapContainer"/> (constant 8 KiB).
12+ ///
13+ /// Serialization layout (body only — caller writes kind+cardinality):
14+ /// ushort[cardinality] values (little-endian per BinaryWriter contract)
15+ /// </summary>
16+ internal sealed class ArrayContainer : IContainer
17+ {
18+ /// <summary>
19+ /// Crossover point at which an array-encoded container should be promoted to a bitmap.
20+ /// 4096 is the canonical Roaring threshold: at 4096 elements an array uses 8 KiB
21+ /// (same as a bitmap container) but membership is still O(log n); past 4096 the
22+ /// bitmap becomes strictly cheaper per element.
23+ /// </summary>
24+ public const int ArrayThreshold = 4096 ;
25+
26+ // Initial capacity is intentionally small to keep tiny containers cheap;
27+ // capacity grows geometrically up to ArrayThreshold + 1 (the +1 lets us detect
28+ // overflow before promoting in Add).
29+ private const int InitialCapacity = 4 ;
30+
31+ private ushort [ ] values ;
32+ private int count ;
33+
34+ public ArrayContainer ( )
35+ {
36+ values = new ushort [ InitialCapacity ] ;
37+ count = 0 ;
38+ }
39+
40+ public ArrayContainer ( ushort [ ] sortedUnique , int count )
41+ {
42+ if ( sortedUnique == null ) throw new ArgumentNullException ( nameof ( sortedUnique ) ) ;
43+ if ( ( uint ) count > ( uint ) sortedUnique . Length ) throw new ArgumentOutOfRangeException ( nameof ( count ) ) ;
44+ this . values = sortedUnique ;
45+ this . count = count ;
46+ }
47+
48+ public ContainerKind Kind => ContainerKind . Array ;
49+ public int Cardinality => count ;
50+ public long ByteSize => 16 + 2L * values . Length ; // array header ~16B + 2B per slot
51+
52+ public bool Contains ( ushort value )
53+ {
54+ return BinarySearch ( value ) >= 0 ;
55+ }
56+
57+ /// <summary>Returns the index of <paramref name="value"/>, or the bitwise complement of the insertion position.</summary>
58+ private int BinarySearch ( ushort value )
59+ {
60+ int lo = 0 ;
61+ int hi = count - 1 ;
62+ while ( lo <= hi )
63+ {
64+ int mid = ( lo + hi ) >>> 1 ;
65+ int cmp = values [ mid ] - value ;
66+ if ( cmp == 0 ) return mid ;
67+ if ( cmp < 0 ) lo = mid + 1 ; else hi = mid - 1 ;
68+ }
69+ return ~ lo ;
70+ }
71+
72+ public IContainer Add ( ushort value , out bool added )
73+ {
74+ int idx = BinarySearch ( value ) ;
75+ if ( idx >= 0 ) { added = false ; return this ; }
76+
77+ int insert = ~ idx ;
78+ // Promote to bitmap before exceeding the threshold to avoid one wasteful array grow.
79+ if ( count >= ArrayThreshold )
80+ {
81+ var bitmap = ToBitmap ( ) ;
82+ bitmap . SetUnchecked ( value ) ;
83+ added = true ;
84+ return bitmap ;
85+ }
86+
87+ EnsureCapacity ( count + 1 ) ;
88+ if ( insert < count )
89+ {
90+ Array . Copy ( values , insert , values , insert + 1 , count - insert ) ;
91+ }
92+ values [ insert ] = value ;
93+ count ++ ;
94+ added = true ;
95+ return this ;
96+ }
97+
98+ public IContainer Remove ( ushort value , out bool removed )
99+ {
100+ int idx = BinarySearch ( value ) ;
101+ if ( idx < 0 ) { removed = false ; return this ; }
102+
103+ if ( idx < count - 1 )
104+ {
105+ Array . Copy ( values , idx + 1 , values , idx , count - idx - 1 ) ;
106+ }
107+ count -- ;
108+ removed = true ;
109+ return count == 0 ? null : this ;
110+ }
111+
112+ public ushort First ( )
113+ {
114+ if ( count == 0 ) throw new InvalidOperationException ( "empty container" ) ;
115+ return values [ 0 ] ;
116+ }
117+
118+ public ushort Last ( )
119+ {
120+ if ( count == 0 ) throw new InvalidOperationException ( "empty container" ) ;
121+ return values [ count - 1 ] ;
122+ }
123+
124+ public int NextSetBit ( int from )
125+ {
126+ // Find first values[i] >= from
127+ int lo = 0 , hi = count ;
128+ while ( lo < hi )
129+ {
130+ int mid = ( lo + hi ) >>> 1 ;
131+ if ( values [ mid ] < from ) lo = mid + 1 ; else hi = mid ;
132+ }
133+ return lo == count ? - 1 : values [ lo ] ;
134+ }
135+
136+ public int NextUnsetBit ( int from )
137+ {
138+ if ( from < 0 || from > 65535 ) return - 1 ;
139+ // Find first values[i] >= from. Then walk forward looking for a gap.
140+ int lo = 0 , hi = count ;
141+ while ( lo < hi )
142+ {
143+ int mid = ( lo + hi ) >>> 1 ;
144+ if ( values [ mid ] < from ) lo = mid + 1 ; else hi = mid ;
145+ }
146+ // Walk from `from`. If values[lo] != from, then `from` itself is unset.
147+ int candidate = from ;
148+ while ( lo < count && values [ lo ] == candidate )
149+ {
150+ if ( candidate == 65535 ) return - 1 ;
151+ candidate ++ ;
152+ lo ++ ;
153+ }
154+ return candidate <= 65535 ? candidate : - 1 ;
155+ }
156+
157+ public IContainer Clone ( )
158+ {
159+ var copy = new ushort [ count ] ; // tight copy — clones don't preallocate growth
160+ Array . Copy ( values , copy , count ) ;
161+ return new ArrayContainer ( copy , count ) ;
162+ }
163+
164+ public void SerializeBody ( BinaryWriter writer )
165+ {
166+ for ( int i = 0 ; i < count ; i ++ )
167+ writer . Write ( values [ i ] ) ;
168+ }
169+
170+ public static ArrayContainer DeserializeBody ( BinaryReader reader , int cardinality )
171+ {
172+ if ( cardinality < 1 || cardinality > ArrayThreshold )
173+ throw new InvalidDataException ( $ "ArrayContainer cardinality out of range: { cardinality } ") ;
174+ var arr = new ushort [ cardinality ] ;
175+ ushort prev = 0 ;
176+ for ( int i = 0 ; i < cardinality ; i ++ )
177+ {
178+ ushort v = reader . ReadUInt16 ( ) ;
179+ if ( i > 0 && v <= prev )
180+ throw new InvalidDataException ( "ArrayContainer values must be strictly ascending" ) ;
181+ arr [ i ] = v ;
182+ prev = v ;
183+ }
184+ return new ArrayContainer ( arr , cardinality ) ;
185+ }
186+
187+ private void EnsureCapacity ( int required )
188+ {
189+ if ( required <= values . Length ) return ;
190+ int newCap = values . Length ;
191+ while ( newCap < required ) newCap = checked ( newCap * 2 ) ;
192+ if ( newCap > ArrayThreshold + 1 ) newCap = ArrayThreshold + 1 ;
193+ Array . Resize ( ref values , newCap ) ;
194+ }
195+
196+ public BitmapContainer ToBitmap ( )
197+ {
198+ var bitmap = new BitmapContainer ( ) ;
199+ for ( int i = 0 ; i < count ; i ++ )
200+ bitmap . SetUnchecked ( values [ i ] ) ;
201+ return bitmap ;
202+ }
203+
204+ // Accessor for tests and RunContainer conversion.
205+ internal ushort GetAt ( int index ) => values [ index ] ;
206+ }
207+ }
0 commit comments