2323
2424import org .apache .lucene .util .RamUsageEstimator ;
2525
26- import static org .elasticsearch .exponentialhistogram .ExponentialHistogram .MAX_INDEX ;
2726import static org .elasticsearch .exponentialhistogram .ExponentialHistogram .MAX_SCALE ;
28- import static org .elasticsearch .exponentialhistogram .ExponentialHistogram .MIN_INDEX ;
2927import static org .elasticsearch .exponentialhistogram .ExponentialHistogram .MIN_SCALE ;
3028import static org .elasticsearch .exponentialhistogram .ExponentialScaleUtils .compareExponentiallyScaledValues ;
3129import static org .elasticsearch .exponentialhistogram .ExponentialScaleUtils .computeIndex ;
3230import static org .elasticsearch .exponentialhistogram .ExponentialScaleUtils .exponentiallyScaledToDoubleValue ;
3331
3432/**
3533 * Represents the bucket for values around zero in an exponential histogram.
36- * The range of this bucket is {@code [-zeroThreshold, +zeroThreshold]}.
37- * To allow efficient comparison with bucket boundaries, this class internally
38- * represents the zero threshold as a exponential histogram bucket index with a scale,
39- * computed via {@link ExponentialScaleUtils#computeIndex(double, int)}.
34+ * Range: [-zeroThreshold, +zeroThreshold].
35+ *
36+ * 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.
4044 */
4145public final class ZeroBucket {
4246
4347 public static final long SHALLOW_SIZE = RamUsageEstimator .shallowSizeOfInstance (ZeroBucket .class );
4448
45- /**
46- * The exponential histogram scale used for {@link #index}
47- */
4849 private final int scale ;
49-
50- /**
51- * The exponential histogram bucket index whose upper boundary corresponds to the zero threshold.
52- * Might be computed lazily from {@link #realThreshold}, uses {@link Long#MAX_VALUE} as placeholder in this case.
53- */
5450 private long index ;
55-
56- /**
57- * Might be computed lazily from {@link #realThreshold}, uses {@link Double#NaN} as placeholder in this case.
58- */
5951 private double realThreshold ;
60-
6152 private final long count ;
62- // A singleton for an empty zero bucket with the smallest possible threshold.
63- private static final ZeroBucket MINIMAL_EMPTY = new ZeroBucket (MIN_INDEX , MIN_SCALE , 0 );
64-
65- /**
66- * Creates a new zero bucket with a specific threshold and count.
67- *
68- * @param zeroThreshold The threshold defining the bucket's range [-zeroThreshold, +zeroThreshold].
69- * @param count The number of values in the bucket.
70- */
71- public ZeroBucket (double zeroThreshold , long count ) {
72- assert zeroThreshold >= 0.0 : "zeroThreshold must not be negative" ;
73- this .index = Long .MAX_VALUE ; // compute lazily when needed
74- this .scale = MAX_SCALE ;
75- this .realThreshold = zeroThreshold ;
76- this .count = count ;
77- }
7853
79- private ZeroBucket (long index , int scale , long count ) {
80- assert index >= MIN_INDEX && index <= MAX_INDEX : "index must be in range [" + MIN_INDEX + ", " + MAX_INDEX + "]" ;
81- assert scale >= MIN_SCALE && scale <= MAX_SCALE : "scale must be in range [" + MIN_SCALE + ", " + MAX_SCALE + "]" ;
82- this .index = index ;
83- this .scale = scale ;
84- this .realThreshold = Double .NaN ; // compute lazily when needed
85- this .count = count ;
54+ private boolean indexComputed ;
55+ private boolean thresholdComputed ;
56+
57+ // Singleton minimal empty
58+ private static final ZeroBucket MINIMAL_EMPTY = new ZeroBucket (
59+ 0L ,
60+ MIN_SCALE ,
61+ 0L ,
62+ true ,
63+ true ,
64+ 0.0d );
65+
66+ /* ===================== Factory Methods ===================== */
67+
68+ public static ZeroBucket fromThreshold (double zeroThreshold , long count ) {
69+ if (zeroThreshold < 0.0 ) {
70+ throw new IllegalArgumentException ("zeroThreshold must be >= 0 (was " + zeroThreshold + ")" );
71+ }
72+ return new ZeroBucket (
73+ 0L , // placeholder until index computed
74+ MAX_SCALE ,
75+ count ,
76+ false , // index not computed yet
77+ true , // threshold known
78+ zeroThreshold );
8679 }
8780
88- private ZeroBucket (double realThreshold , long index , int scale , long count ) {
89- this .realThreshold = realThreshold ;
90- this .index = index ;
91- this .scale = scale ;
92- this .count = count ;
81+ public static ZeroBucket fromIndexAndScale (long index , int scale , long count ) {
82+ if (scale < MIN_SCALE || scale > MAX_SCALE ) {
83+ throw new IllegalArgumentException ("scale out of range: " + scale );
84+ }
85+ return new ZeroBucket (
86+ index ,
87+ scale ,
88+ count ,
89+ true , // index known
90+ false , // threshold lazy
91+ Double .NaN );
9392 }
9493
95- /**
96- * @return A singleton instance of an empty zero bucket with the smallest possible threshold.
97- */
9894 public static ZeroBucket minimalEmpty () {
9995 return MINIMAL_EMPTY ;
10096 }
10197
102- /**
103- * Creates a zero bucket with the smallest possible threshold and a given count.
104- *
105- * @param count The number of values in the bucket.
106- * @return A new {@link ZeroBucket}.
107- */
108- public static ZeroBucket minimalWithCount (long count ) {
109- if (count == 0 ) {
110- return MINIMAL_EMPTY ;
111- } else {
112- return new ZeroBucket (MINIMAL_EMPTY .zeroThreshold (), MINIMAL_EMPTY .index (), MINIMAL_EMPTY .scale (), count );
113- }
98+ /* ===================== Private Constructor ===================== */
99+ private ZeroBucket (
100+ long index ,
101+ int scale ,
102+ long count ,
103+ boolean indexComputed ,
104+ boolean thresholdComputed ,
105+ double realThreshold ) {
106+ this .index = index ;
107+ this .scale = scale ;
108+ this .count = count ;
109+ this .indexComputed = indexComputed ;
110+ this .thresholdComputed = thresholdComputed ;
111+ this .realThreshold = realThreshold ;
114112 }
115113
116- /**
117- * @return The value of the zero threshold.
118- */
119- public double zeroThreshold () {
120- if (Double .isNaN (realThreshold )) {
121- realThreshold = exponentiallyScaledToDoubleValue (index (), scale ());
122- }
123- return realThreshold ;
114+ /* ===================== Public API ===================== */
115+
116+ public long count () {
117+ return count ;
124118 }
125119
126120 public long index () {
127- if (index == Long .MAX_VALUE ) {
128- index = computeIndex (zeroThreshold (), scale ()) + 1 ;
129- }
121+ computeIndexIfNeeded ();
130122 return index ;
131123 }
132124
125+ public double zeroThreshold () {
126+ computeThresholdIfNeeded ();
127+ return realThreshold ;
128+ }
129+
133130 public int scale () {
134131 return scale ;
135132 }
136133
137- public long count () {
138- return count ;
139- }
134+ /* ===================== Lazy Computation ===================== */
140135
141- /**
142- * Merges this zero bucket with another one.
143- * <ul>
144- * <li>If the other zero bucket or both are empty, this instance is returned unchanged.</li>
145- * <li>If the this zero bucket is empty and the other one is populated, the other instance is returned unchanged.</li>
146- * <li>Otherwise, the zero threshold is increased if necessary (by taking the maximum of the two), and the counts are summed.</li>
147- * </ul>
148- *
149- * @param other The other zero bucket to merge with.
150- * @return A new {@link ZeroBucket} representing the merged result.
151- */
152- public ZeroBucket merge (ZeroBucket other ) {
153- if (other .count == 0 ) {
154- return this ;
155- } else if (count == 0 ) {
156- return other ;
157- } else {
158- long totalCount = count + other .count ;
159- // Both are populated, so we need to use the higher zero-threshold.
160- if (this .compareZeroThreshold (other ) >= 0 ) {
161- return new ZeroBucket (realThreshold , index , scale , totalCount );
162- } else {
163- return new ZeroBucket (other .realThreshold , other .index , other .scale , totalCount );
164- }
136+ private void computeIndexIfNeeded () {
137+ if (indexComputed == false ) {
138+ index = computeIndex (realThreshold , scale );
139+ indexComputed = true ;
165140 }
166141 }
167142
168- /**
169- * Collapses all buckets from the given iterators whose lower boundaries are smaller than the zero threshold.
170- * The iterators are advanced to point at the first, non-collapsed bucket.
171- *
172- * @param bucketIterators The iterators whose buckets may be collapsed.
173- * @return A potentially updated {@link ZeroBucket} with the collapsed buckets' counts and an adjusted threshold.
174- */
175- public ZeroBucket collapseOverlappingBucketsForAll (BucketIterator ... bucketIterators ) {
176- ZeroBucket current = this ;
177- ZeroBucket previous ;
178- do {
179- previous = current ;
180- for (BucketIterator buckets : bucketIterators ) {
181- current = current .collapseOverlappingBuckets (buckets );
182- }
183- } while (previous .compareZeroThreshold (current ) != 0 );
184- return current ;
185- }
186-
187- /**
188- * Compares the zero threshold of this bucket with another one.
189- *
190- * @param other The other zero bucket to compare against.
191- * @return A negative integer, zero, or a positive integer if this bucket's threshold is less than,
192- * equal to, or greater than the other's.
193- */
194- public int compareZeroThreshold (ZeroBucket other ) {
195- return compareExponentiallyScaledValues (index (), scale (), other .index (), other .scale ());
196- }
197-
198- /**
199- * Collapses all buckets from the given iterator whose lower boundaries are smaller than the zero threshold.
200- * The iterator is advanced to point at the first, non-collapsed bucket.
201- *
202- * @param buckets The iterator whose buckets may be collapsed.
203- * @return A potentially updated {@link ZeroBucket} with the collapsed buckets' counts and an adjusted threshold.
204- */
205- public ZeroBucket collapseOverlappingBuckets (BucketIterator buckets ) {
206-
207- long collapsedCount = 0 ;
208- long highestCollapsedIndex = 0 ;
209- while (buckets .hasNext () && compareExponentiallyScaledValues (buckets .peekIndex (), buckets .scale (), index (), scale ()) < 0 ) {
210- highestCollapsedIndex = buckets .peekIndex ();
211- collapsedCount += buckets .peekCount ();
212- buckets .advance ();
143+ private void computeThresholdIfNeeded () {
144+ if (thresholdComputed == false ) {
145+ realThreshold = exponentiallyScaledToDoubleValue (index , scale );
146+ thresholdComputed = true ;
213147 }
214- if (collapsedCount == 0 ) {
215- return this ;
216- } else {
217- long newZeroCount = count + collapsedCount ;
218- // +1 because we need to adjust the zero threshold to the upper boundary of the collapsed bucket
219- long collapsedUpperBoundIndex = highestCollapsedIndex + 1 ;
220- if (compareExponentiallyScaledValues (index (), scale (), collapsedUpperBoundIndex , buckets .scale ()) >= 0 ) {
221- // Our current zero-threshold is larger than the upper boundary of the largest collapsed bucket, so we keep it.
222- return new ZeroBucket (realThreshold , index , scale , newZeroCount );
223- } else {
224- return new ZeroBucket (collapsedUpperBoundIndex , buckets .scale (), newZeroCount );
225- }
148+ }
149+
150+ /* ===================== Package-Private for Tests ===================== */
151+
152+ boolean isIndexComputed () {
153+ return indexComputed ;
154+ }
155+
156+ boolean isThresholdComputed () {
157+ return thresholdComputed ;
158+ }
159+
160+ /* ===================== Comparison Helper ===================== */
161+
162+ int compareTo (long otherIndex , int otherScale ) {
163+ return compareExponentiallyScaledValues (index (), scale , otherIndex , otherScale );
164+ }
165+
166+ /* ===================== Value Semantics ===================== */
167+
168+ @ Override
169+ public boolean equals (Object o ) {
170+ if (this == o )
171+ return true ;
172+ if (o instanceof ZeroBucket zb ) {
173+ long i1 = index ();
174+ long i2 = zb .index ();
175+ double t1 = zeroThreshold ();
176+ double t2 = zb .zeroThreshold ();
177+ return scale == zb .scale && count == zb .count && i1 == i2 && Double .compare (t1 , t2 ) == 0 ;
226178 }
179+ return false ;
180+ }
181+
182+ @ Override
183+ public int hashCode () {
184+ int h = Integer .hashCode (scale );
185+ h = 31 * h + Long .hashCode (index ());
186+ h = 31 * h + Double .hashCode (zeroThreshold ());
187+ h = 31 * h + Long .hashCode (count );
188+ return h ;
189+ }
190+
191+ @ Override
192+ public String toString () {
193+ return "ZeroBucket{scale=" + scale
194+ + ", index=" + index ()
195+ + ", threshold=" + zeroThreshold ()
196+ + ", count=" + count
197+ + ", indexComputed=" + indexComputed
198+ + ", thresholdComputed=" + thresholdComputed
199+ + "}" ;
227200 }
228- }
201+ }
0 commit comments