2323
2424import org .apache .lucene .util .RamUsageEstimator ;
2525
26+ import static org .elasticsearch .exponentialhistogram .ExponentialHistogram .MAX_INDEX ;
2627import static org .elasticsearch .exponentialhistogram .ExponentialHistogram .MAX_SCALE ;
28+ import static org .elasticsearch .exponentialhistogram .ExponentialHistogram .MIN_INDEX ;
2729import static org .elasticsearch .exponentialhistogram .ExponentialHistogram .MIN_SCALE ;
2830import static org .elasticsearch .exponentialhistogram .ExponentialScaleUtils .compareExponentiallyScaledValues ;
2931import static org .elasticsearch .exponentialhistogram .ExponentialScaleUtils .computeIndex ;
3032import static org .elasticsearch .exponentialhistogram .ExponentialScaleUtils .exponentiallyScaledToDoubleValue ;
3133
3234/**
3335 * Represents the bucket for values around zero in an exponential histogram.
34- * Range: [-zeroThreshold, +zeroThreshold].
36+ * The range of this bucket is {@code [-zeroThreshold, +zeroThreshold]} .
3537 *
3638 * Refactor (Task 1):
37- * - Added static factory methods (fromThreshold, fromIndexAndScale,
38- * minimalEmpty).
39- * - Replaced sentinel lazy state (Long.MAX_VALUE / Double.NaN) with explicit
40- * booleans
41- * (indexComputed, thresholdComputed).
42- * - Added equals, hashCode, toString for value semantics.
43- * - Added package-private isIndexComputed()/isThresholdComputed() for tests.
39+ * - Added static factories (fromThreshold, fromIndexAndScale) while keeping
40+ * original public constructor
41+ * - Introduced explicit lazy flags (indexComputed, thresholdComputed) instead
42+ * of sentinels
43+ * - Added value semantics (equals, hashCode, toString)
44+ * - Preserved original API: minimalEmpty, minimalWithCount, merge,
45+ * collapseOverlappingBuckets[ForAll], compareZeroThreshold
4446 */
4547public final class ZeroBucket {
4648
@@ -51,51 +53,102 @@ public final class ZeroBucket {
5153 private double realThreshold ;
5254 private final long count ;
5355
56+ // Explicit lazy flags
5457 private boolean indexComputed ;
5558 private boolean thresholdComputed ;
5659
57- // Singleton minimal empty
60+ // Original minimal empty singleton (index known, threshold lazy)
5861 private static final ZeroBucket MINIMAL_EMPTY = new ZeroBucket (
59- 0L ,
62+ MIN_INDEX ,
6063 MIN_SCALE ,
6164 0L ,
6265 true ,
63- true ,
64- 0.0d );
66+ false ,
67+ Double .NaN );
68+
69+ /*
70+ * ===================== Original Public Constructor (kept)
71+ * =====================
72+ */
73+
74+ /**
75+ * Original public constructor (threshold authoritative). Kept for source
76+ * compatibility.
77+ * Deprecated in favor of {@link #fromThreshold(double, long)}.
78+ */
79+ @ Deprecated
80+ public ZeroBucket (double zeroThreshold , long count ) {
81+ if (zeroThreshold < 0.0 ) {
82+ throw new IllegalArgumentException ("zeroThreshold must be >= 0 (was " + zeroThreshold + ")" );
83+ }
84+ this .scale = MAX_SCALE ;
85+ this .count = count ;
86+ this .realThreshold = zeroThreshold ;
87+ this .index = 0L ; // placeholder until computed
88+ this .indexComputed = false ;
89+ this .thresholdComputed = true ;
90+ }
6591
6692 /* ===================== Factory Methods ===================== */
6793
94+ /**
95+ * Create a ZeroBucket from an explicit threshold (index lazy).
96+ */
6897 public static ZeroBucket fromThreshold (double zeroThreshold , long count ) {
6998 if (zeroThreshold < 0.0 ) {
7099 throw new IllegalArgumentException ("zeroThreshold must be >= 0 (was " + zeroThreshold + ")" );
71100 }
72101 return new ZeroBucket (
73- 0L , // placeholder until index computed
102+ 0L ,
74103 MAX_SCALE ,
75104 count ,
76- false , // index not computed yet
77- true , // threshold known
105+ false ,
106+ true ,
78107 zeroThreshold );
79108 }
80109
110+ /**
111+ * Create a ZeroBucket from an index + scale (threshold lazy).
112+ */
81113 public static ZeroBucket fromIndexAndScale (long index , int scale , long count ) {
82114 if (scale < MIN_SCALE || scale > MAX_SCALE ) {
83115 throw new IllegalArgumentException ("scale out of range: " + scale );
84116 }
117+ if (index < MIN_INDEX || index > MAX_INDEX ) {
118+ throw new IllegalArgumentException ("index out of range: " + index );
119+ }
85120 return new ZeroBucket (
86121 index ,
87122 scale ,
88123 count ,
89- true , // index known
90- false , // threshold lazy
124+ true ,
125+ false ,
91126 Double .NaN );
92127 }
93128
129+ /**
130+ * @return singleton empty minimal bucket.
131+ */
94132 public static ZeroBucket minimalEmpty () {
95133 return MINIMAL_EMPTY ;
96134 }
97135
98- /* ===================== Private Constructor ===================== */
136+ /**
137+ * Creates a zero bucket with the smallest possible threshold and a given count.
138+ * If count == 0 returns the singleton.
139+ */
140+ public static ZeroBucket minimalWithCount (long count ) {
141+ if (count == 0 ) {
142+ return MINIMAL_EMPTY ;
143+ }
144+ // Resolve lazy threshold & index of singleton
145+ double threshold = MINIMAL_EMPTY .zeroThreshold ();
146+ long idx = MINIMAL_EMPTY .index ();
147+ return resolved (threshold , idx , MINIMAL_EMPTY .scale (), count );
148+ }
149+
150+ /* ===================== Private Constructors ===================== */
151+
99152 private ZeroBucket (
100153 long index ,
101154 int scale ,
@@ -111,44 +164,54 @@ private ZeroBucket(
111164 this .realThreshold = realThreshold ;
112165 }
113166
114- /* ===================== Public API ===================== */
167+ private static ZeroBucket resolved (double threshold , long index , int scale , long count ) {
168+ return new ZeroBucket (index , scale , count , true , true , threshold );
169+ }
170+
171+ /* ===================== Accessors ===================== */
115172
116173 public long count () {
117174 return count ;
118175 }
119176
177+ public int scale () {
178+ return scale ;
179+ }
180+
181+ /**
182+ * Returns index; if threshold authoritative, compute with +1 rule (matches
183+ * original code).
184+ */
120185 public long index () {
121186 computeIndexIfNeeded ();
122187 return index ;
123188 }
124189
190+ /**
191+ * Returns threshold; if index authoritative compute it lazily.
192+ */
125193 public double zeroThreshold () {
126194 computeThresholdIfNeeded ();
127195 return realThreshold ;
128196 }
129197
130- public int scale () {
131- return scale ;
132- }
133-
134- /* ===================== Lazy Computation ===================== */
198+ /* ===================== Lazy Computation Helpers ===================== */
135199
136200 private void computeIndexIfNeeded () {
137201 if (indexComputed == false ) {
138- index = computeIndex (realThreshold , scale );
202+ index = computeIndex (realThreshold , scale ) + 1 ;
139203 indexComputed = true ;
140204 }
141205 }
142206
143207 private void computeThresholdIfNeeded () {
144208 if (thresholdComputed == false ) {
145- realThreshold = exponentiallyScaledToDoubleValue (index , scale );
209+ realThreshold = exponentiallyScaledToDoubleValue (index () , scale );
146210 thresholdComputed = true ;
147211 }
148212 }
149213
150- /* ===================== Package-Private for Tests ===================== */
151-
214+ /* Package-private for tests */
152215 boolean isIndexComputed () {
153216 return indexComputed ;
154217 }
@@ -157,10 +220,59 @@ boolean isThresholdComputed() {
157220 return thresholdComputed ;
158221 }
159222
160- /* ===================== Comparison Helper ===================== */
223+ /* ===================== Original Functional API ===================== */
161224
162- int compareTo (long otherIndex , int otherScale ) {
163- return compareExponentiallyScaledValues (index (), scale , otherIndex , otherScale );
225+ public int compareZeroThreshold (ZeroBucket other ) {
226+ return compareExponentiallyScaledValues (index (), scale (), other .index (), other .scale ());
227+ }
228+
229+ public ZeroBucket merge (ZeroBucket other ) {
230+ if (other .count == 0 ) {
231+ return this ;
232+ } else if (this .count == 0 ) {
233+ return other ;
234+ } else {
235+ long total = this .count + other .count ;
236+ if (this .compareZeroThreshold (other ) >= 0 ) {
237+ return resolved (this .zeroThreshold (), this .index (), this .scale (), total );
238+ } else {
239+ return resolved (other .zeroThreshold (), other .index (), other .scale (), total );
240+ }
241+ }
242+ }
243+
244+ public ZeroBucket collapseOverlappingBucketsForAll (BucketIterator ... bucketIterators ) {
245+ ZeroBucket current = this ;
246+ ZeroBucket previous ;
247+ do {
248+ previous = current ;
249+ for (BucketIterator b : bucketIterators ) {
250+ current = current .collapseOverlappingBuckets (b );
251+ }
252+ } while (previous .compareZeroThreshold (current ) != 0 );
253+ return current ;
254+ }
255+
256+ public ZeroBucket collapseOverlappingBuckets (BucketIterator buckets ) {
257+ long collapsedCount = 0 ;
258+ long highestCollapsedIndex = 0 ;
259+ while (buckets .hasNext ()
260+ && compareExponentiallyScaledValues (buckets .peekIndex (), buckets .scale (), index (), scale ()) < 0 ) {
261+ highestCollapsedIndex = buckets .peekIndex ();
262+ collapsedCount += buckets .peekCount ();
263+ buckets .advance ();
264+ }
265+ if (collapsedCount == 0 ) {
266+ return this ;
267+ } else {
268+ long newZeroCount = count + collapsedCount ;
269+ long collapsedUpperBoundIndex = highestCollapsedIndex + 1 ;
270+ if (compareExponentiallyScaledValues (index (), scale (), collapsedUpperBoundIndex , buckets .scale ()) >= 0 ) {
271+ return resolved (this .zeroThreshold (), this .index (), this .scale (), newZeroCount );
272+ } else {
273+ return fromIndexAndScale (collapsedUpperBoundIndex , buckets .scale (), newZeroCount );
274+ }
275+ }
164276 }
165277
166278 /* ===================== Value Semantics ===================== */
0 commit comments