@@ -98,10 +98,12 @@ public short getK() {
9898 * Update this TDigest with the given value.
9999 * NaN and infinity are ignored.
100100 * @param value to update the TDigest with
101+ * @throws ArithmeticException if the total weight would exceed Long.MAX_VALUE
101102 */
102103 public void update (final double value ) {
103104 if (!Double .isFinite (value )) { return ; }
104- if (numBuffered_ == (centroidsCapacity_ * BUFFER_MULTIPLIER )) { compress (); }
105+ Math .addExact (getTotalWeight (), 1 );
106+ if (numBuffered_ >= (centroidsCapacity_ * BUFFER_MULTIPLIER )) { compress (); }
105107 bufferValues_ [numBuffered_ ] = value ;
106108 numBuffered_ ++;
107109 minValue_ = Math .min (minValue_ , value );
@@ -111,9 +113,11 @@ public void update(final double value) {
111113 /**
112114 * Merge the given TDigest into this one
113115 * @param other TDigest to merge
116+ * @throws ArithmeticException if the combined total weight would exceed Long.MAX_VALUE
114117 */
115118 public void merge (final TDigestDouble other ) {
116119 if (other .isEmpty ()) { return ; }
120+ Math .addExact (getTotalWeight (), other .getTotalWeight ());
117121 final int num = numCentroids_ + numBuffered_ + other .numCentroids_ + other .numBuffered_ ;
118122 final double [] values = new double [num ];
119123 final long [] weights = new long [num ];
@@ -256,6 +260,7 @@ public double getQuantile(final double rank) {
256260 }
257261 final double lastWeight = centroidWeights_ [numCentroids_ - 1 ];
258262 if ((lastWeight > 1 ) && ((centroidsWeight_ - weight ) <= (lastWeight / 2.0 ))) {
263+ if (lastWeight == 2 ) { return maxValue_ ; }
259264 return maxValue_ - (((centroidsWeight_ - weight - 1.0 ) / ((lastWeight / 2.0 ) - 1.0 ))
260265 * (maxValue_ - centroidMeans_ [numCentroids_ - 1 ]));
261266 }
@@ -395,6 +400,7 @@ public static TDigestDouble heapify(final MemorySegment seg) {
395400 * @return an instance of TDigest
396401 */
397402 public static TDigestDouble heapify (final MemorySegment seg , final boolean isFloat ) {
403+ checkSerializedSize (seg , 8 );
398404 final PositionalSegment posSeg = PositionalSegment .wrap (seg );
399405 final byte preambleLongs = posSeg .getByte ();
400406 final byte serialVersion = posSeg .getByte ();
@@ -411,6 +417,10 @@ public static TDigestDouble heapify(final MemorySegment seg, final boolean isFlo
411417 final byte flagsByte = posSeg .getByte ();
412418 final boolean isEmpty = (flagsByte & (1 << Flags .IS_EMPTY .ordinal ())) > 0 ;
413419 final boolean isSingleValue = (flagsByte & (1 << Flags .IS_SINGLE_VALUE .ordinal ())) > 0 ;
420+ final int knownFlags = (1 << Flags .values ().length ) - 1 ;
421+ if (((flagsByte & ~knownFlags ) != 0 ) || (isEmpty && isSingleValue )) {
422+ throw new SketchesArgumentException ("Invalid TDigest flags: " + flagsByte );
423+ }
414424 final byte expectedPreambleLongs = isEmpty || isSingleValue ? PREAMBLE_LONGS_EMPTY_OR_SINGLE : PREAMBLE_LONGS_MULTIPLE ;
415425 if (preambleLongs != expectedPreambleLongs ) {
416426 throw new SketchesArgumentException ("Preamble longs mismatch: expected " + expectedPreambleLongs + ", actual " + preambleLongs );
@@ -419,6 +429,7 @@ public static TDigestDouble heapify(final MemorySegment seg, final boolean isFlo
419429 if (isEmpty ) { return new TDigestDouble (k ); }
420430 final boolean reverseMerge = (flagsByte & (1 << Flags .REVERSE_MERGE .ordinal ())) > 0 ;
421431 if (isSingleValue ) {
432+ checkSerializedSize (seg , 8 + (isFloat ? Float .BYTES : Double .BYTES ));
422433 final double value ;
423434 if (isFloat ) {
424435 value = posSeg .getFloat ();
@@ -428,8 +439,12 @@ public static TDigestDouble heapify(final MemorySegment seg, final boolean isFlo
428439 checkDeserializedValue (value , "value" );
429440 return new TDigestDouble (reverseMerge , k , value , value , new double [] {value }, new long [] {1 }, 1 , null );
430441 }
442+ final int valueBytes = isFloat ? Float .BYTES : Double .BYTES ;
443+ checkSerializedSize (seg , 16 + (2L * valueBytes ));
431444 final int numCentroids = posSeg .getInt ();
432445 final int numBuffered = posSeg .getInt ();
446+ checkDeserializedCounts (k , numCentroids , numBuffered );
447+ checkSerializedSize (seg , 16 + (2L * valueBytes ) + (2L * valueBytes * numCentroids ) + ((long ) valueBytes * numBuffered ));
433448 final double min ;
434449 final double max ;
435450 if (isFloat ) {
@@ -439,22 +454,22 @@ public static TDigestDouble heapify(final MemorySegment seg, final boolean isFlo
439454 min = posSeg .getDouble ();
440455 max = posSeg .getDouble ();
441456 }
442- checkDeserializedValue (min , "min" );
443- checkDeserializedValue (max , "max" );
457+ checkDeserializedExtrema (min , max );
444458 final double [] means = new double [numCentroids ];
445459 final long [] weights = new long [numCentroids ];
446460 long totalWeight = 0 ;
447461 for (int i = 0 ; i < numCentroids ; i ++) {
448462 means [i ] = isFloat ? posSeg .getFloat () : posSeg .getDouble ();
449463 weights [i ] = isFloat ? posSeg .getInt () : posSeg .getLong ();
450- checkDeserializedValue (means [i ], "centroid mean" );
464+ checkDeserializedRange (means [i ], i == 0 ? min : means [ i - 1 ], max , "centroid mean" );
451465 checkDeserializedWeight (weights [i ]);
452- totalWeight += weights [i ];
466+ totalWeight = addDeserializedWeight ( totalWeight , weights [i ]) ;
453467 }
468+ addDeserializedWeight (totalWeight , numBuffered );
454469 final double [] buffered = new double [numBuffered ];
455470 for (int i = 0 ; i < numBuffered ; i ++) {
456471 buffered [i ] = isFloat ? posSeg .getFloat () : posSeg .getDouble ();
457- checkDeserializedValue (buffered [i ], "buffered value" );
472+ checkDeserializedRange (buffered [i ], min , max , "buffered value" );
458473 }
459474 return new TDigestDouble (reverseMerge , k , min , max , means , weights , totalWeight , buffered );
460475 }
@@ -468,43 +483,47 @@ private static TDigestDouble heapifyCompat(final MemorySegment seg) {
468483 throw new SketchesArgumentException ("unexpected compatibility type " + type );
469484 }
470485 if (type == COMPAT_DOUBLE ) { // compatibility with asBytes()
486+ checkSerializedSize (seg , 32 );
471487 final double min = seg .get (JAVA_DOUBLE_UNALIGNED_BIG_ENDIAN , offset ); offset += Double .BYTES ;
472488 final double max = seg .get (JAVA_DOUBLE_UNALIGNED_BIG_ENDIAN , offset ); offset += Double .BYTES ;
473489 final short k = (short ) seg .get (JAVA_DOUBLE_UNALIGNED_BIG_ENDIAN , offset ); offset += Double .BYTES ;
474490 final int numCentroids = seg .get (JAVA_INT_UNALIGNED_BIG_ENDIAN , offset ); offset += Integer .BYTES ;
475- checkDeserializedValue (min , "min" );
476- checkDeserializedValue (max , "max" );
491+ checkDeserializedCounts (k , numCentroids , 0 );
492+ checkSerializedSize (seg , offset + (16L * numCentroids ));
493+ checkDeserializedExtrema (min , max );
477494 final double [] means = new double [numCentroids ];
478495 final long [] weights = new long [numCentroids ];
479496 long totalWeight = 0 ;
480497 for (int i = 0 ; i < numCentroids ; i ++) {
481- weights [i ] = ( long ) seg .get (JAVA_DOUBLE_UNALIGNED_BIG_ENDIAN , offset ); offset += Double .BYTES ;
498+ weights [i ] = readCompatibilityWeight ( seg .get (JAVA_DOUBLE_UNALIGNED_BIG_ENDIAN , offset ) ); offset += Double .BYTES ;
482499 means [i ] = seg .get (JAVA_DOUBLE_UNALIGNED_BIG_ENDIAN , offset ); offset += Double .BYTES ;
483- checkDeserializedValue (means [i ], "centroid mean" );
484- checkDeserializedWeight (weights [i ]);
485- totalWeight += weights [i ];
500+ checkDeserializedRange (means [i ], i == 0 ? min : means [i - 1 ], max , "centroid mean" );
501+ totalWeight = addDeserializedWeight (totalWeight , weights [i ]);
486502 }
487503 return new TDigestDouble (false , k , min , max , means , weights , totalWeight , null );
488504 }
489505 // COMPAT_FLOAT: compatibility with asSmallBytes(), reference implementation uses doubles for min and max
506+ checkSerializedSize (seg , 30 );
490507 final double min = seg .get (JAVA_DOUBLE_UNALIGNED_BIG_ENDIAN , offset ); offset += Double .BYTES ;
491508 final double max = seg .get (JAVA_DOUBLE_UNALIGNED_BIG_ENDIAN , offset );offset += Double .BYTES ;
492509 final short k = (short ) seg .get (JAVA_FLOAT_UNALIGNED_BIG_ENDIAN , offset ); offset += Float .BYTES ;
493510 // reference implementation stores capacities of the array of centroids and the buffer as shorts
494511 // they can be derived from k in the constructor
495512 seg .get (JAVA_INT_UNALIGNED_BIG_ENDIAN , offset ); offset += Integer .BYTES ;
496513 final int numCentroids = seg .get (JAVA_SHORT_UNALIGNED_BIG_ENDIAN , offset ); offset += Short .BYTES ;
497- checkDeserializedValue (min , "min" );
498- checkDeserializedValue (max , "max" );
514+ checkDeserializedCounts (k , numCentroids , 0 );
515+ checkSerializedSize (seg , offset + (8L * numCentroids ));
516+ checkDeserializedExtrema (min , max );
499517 final double [] means = new double [numCentroids ];
500518 final long [] weights = new long [numCentroids ];
501519 long totalWeight = 0 ;
502520 for (int i = 0 ; i < numCentroids ; i ++) {
503- weights [i ] = ( long ) seg .get (JAVA_FLOAT_UNALIGNED_BIG_ENDIAN , offset ); offset += Float .BYTES ;
521+ weights [i ] = readCompatibilityWeight ( seg .get (JAVA_FLOAT_UNALIGNED_BIG_ENDIAN , offset ) ); offset += Float .BYTES ;
504522 means [i ] = seg .get (JAVA_FLOAT_UNALIGNED_BIG_ENDIAN , offset ); offset += Float .BYTES ;
505- checkDeserializedValue (means [i ], "centroid mean" );
506- checkDeserializedWeight (weights [i ]);
507- totalWeight += weights [i ];
523+ // Float means can round just outside the double extrema in asSmallBytes().
524+ checkDeserializedRange (means [i ], (float ) (i == 0 ? min : means [i - 1 ]), (float ) max , "centroid mean" );
525+ means [i ] = Math .max (min , Math .min (max , means [i ]));
526+ totalWeight = addDeserializedWeight (totalWeight , weights [i ]);
508527 }
509528 return new TDigestDouble (false , k , min , max , means , weights , totalWeight , null );
510529 }
@@ -521,6 +540,50 @@ private static void checkDeserializedWeight(final long weight) {
521540 }
522541 }
523542
543+ private static long readCompatibilityWeight (final double weight ) {
544+ // Validate before narrowing: Java truncates fractions and saturates overflowing casts.
545+ if (!Double .isFinite (weight ) || (weight < 1 ) || (weight >= 0x1p63 ) || (weight != Math .rint (weight ))) {
546+ throw new SketchesArgumentException ("Deserialized centroid weight must be a positive long, actual: " + weight );
547+ }
548+ return (long ) weight ;
549+ }
550+
551+ private static long addDeserializedWeight (final long total , final long weight ) {
552+ if (weight > (Long .MAX_VALUE - total )) {
553+ throw new SketchesArgumentException ("Deserialized total weight exceeds Long.MAX_VALUE" );
554+ }
555+ return total + weight ;
556+ }
557+
558+ private static void checkDeserializedExtrema (final double min , final double max ) {
559+ checkDeserializedValue (min , "min" );
560+ checkDeserializedValue (max , "max" );
561+ if (min > max ) { throw new SketchesArgumentException ("Deserialized min must not exceed max" ); }
562+ }
563+
564+ private static void checkDeserializedRange (final double value , final double lower , final double upper ,
565+ final String description ) {
566+ checkDeserializedValue (value , description );
567+ if ((value < lower ) || (value > upper )) {
568+ throw new SketchesArgumentException ("Deserialized " + description + " must be within [" + lower + ", " + upper + "]" );
569+ }
570+ }
571+
572+ private static void checkDeserializedCounts (final short k , final int numCentroids , final int numBuffered ) {
573+ checkK (k );
574+ if ((numCentroids < 0 ) || (numBuffered < 0 ) || (numCentroids > (Integer .MAX_VALUE - numBuffered ))
575+ || ((numCentroids + numBuffered ) == 0 )) {
576+ throw new SketchesArgumentException ("Invalid TDigest counts: centroids=" + numCentroids + ", buffered=" + numBuffered );
577+ }
578+ }
579+
580+ private static void checkSerializedSize (final MemorySegment seg , final long requiredBytes ) {
581+ if (seg .byteSize () < requiredBytes ) {
582+ throw new SketchesArgumentException ("Insufficient TDigest data: expected at least " + requiredBytes
583+ + " bytes, actual: " + seg .byteSize ());
584+ }
585+ }
586+
524587 /**
525588 * Human-readable summary of this TDigest as a string
526589 * @return summary of this TDigest
@@ -542,8 +605,8 @@ public String toString(final boolean printCentroids) {
542605 .append (" Compression: " ).append (k_ ).append (LS )
543606 .append (" Centroids: " ).append (numCentroids_ ).append (LS )
544607 .append (" Buffered: " ).append (numBuffered_ ).append (LS )
545- .append (" Centroids Capacity: " ).append (centroidsCapacity_ ).append (LS )
546- .append (" Buffer Capacity: " ).append (centroidsCapacity_ * BUFFER_MULTIPLIER ).append (LS )
608+ .append (" Centroids Capacity: " ).append (centroidMeans_ . length ).append (LS )
609+ .append (" Buffer Capacity: " ).append (bufferValues_ . length ).append (LS )
547610 .append ("Centroids Weight: " ).append (centroidsWeight_ ).append (LS )
548611 .append (" Total Weight: " ).append (getTotalWeight ()).append (LS )
549612 .append (" Reverse Merge: " ).append (reverseMerge_ ).append (LS );
@@ -574,12 +637,15 @@ private TDigestDouble(final boolean reverseMerge, final short k, final double mi
574637 k_ = k ;
575638 minValue_ = min ;
576639 maxValue_ = max ;
577- if ( k < 10 ) { throw new SketchesArgumentException ( "k must be at least 10" ); }
640+ checkK ( k );
578641 final int fudge = k < 30 ? 30 : 10 ;
579- centroidsCapacity_ = (k_ * 2 ) + fudge ;
580- centroidMeans_ = new double [centroidsCapacity_ ];
581- centroidWeights_ = new long [centroidsCapacity_ ];
582- bufferValues_ = new double [centroidsCapacity_ * BUFFER_MULTIPLIER ];
642+ centroidsCapacity_ = (k * 2 ) + fudge ;
643+ // Compression thresholds are local sizing choices, not serialization limits.
644+ final int centroidSlots = Math .max (centroidsCapacity_ , means == null ? 0 : means .length );
645+ final int bufferSlots = Math .max (centroidsCapacity_ * BUFFER_MULTIPLIER , buffer == null ? 0 : buffer .length );
646+ centroidMeans_ = new double [centroidSlots ];
647+ centroidWeights_ = new long [centroidSlots ];
648+ bufferValues_ = new double [bufferSlots ];
583649 numCentroids_ = 0 ;
584650 numBuffered_ = 0 ;
585651 centroidsWeight_ = weight ;
@@ -594,6 +660,10 @@ private TDigestDouble(final boolean reverseMerge, final short k, final double mi
594660 }
595661 }
596662
663+ private static void checkK (final short k ) {
664+ if (k < 10 ) { throw new SketchesArgumentException ("k must be at least 10" ); }
665+ }
666+
597667 // assumes that there is enough room in the input arrays to add centroids from this TDigest
598668 private void merge (final double [] values , final long [] weights , final long weight , int num ) {
599669 System .arraycopy (centroidMeans_ , 0 , values , num , numCentroids_ );
@@ -670,25 +740,26 @@ static double z(final double compression, final double n) {
670740 }
671741
672742 /*
673- * The weights are normalized before multiplying so that each term is bounded by the magnitude
674- * of its input, otherwise the products can overflow to infinity for values of large magnitude .
743+ * Opposite-sign inputs need normalized weights to avoid overflowing their difference.
744+ * Same-sign inputs need interpolation to avoid overflowing the sum of rounded products .
675745 */
676746 private static double weightedAverage (final double x1 , final double w1 , final double x2 , final double w2 ) {
677- final double weight = w1 + w2 ;
678- return (x1 * (w1 / weight )) + (x2 * (w2 / weight ));
747+ final double ratio = w2 / (w1 + w2 );
748+ if (Math .copySign (1 , x1 ) != Math .copySign (1 , x2 )) {
749+ return (x1 * (1 - ratio )) + (x2 * ratio );
750+ }
751+ return Math .fma (x2 - x1 , ratio , x1 );
679752 }
680753
681754 /*
682755 * Computes the mean of a centroid after merging in the given value with weight w,
683756 * where weight is the total weight of the centroid including w.
684- * The intermediate (value - mean) or its product with w can overflow to infinity
685- * even when both inputs are finite (e.g. means near opposite ends of the double range),
686- * which eventually turns the stored mean into NaN. In that case fall back to
687- * the overflow-safe weighted average, which stays finite.
757+ * Normalize w before multiplying to avoid overflowing the product. If the difference
758+ * itself overflows, the inputs have opposite signs and need the scaled weighted average.
688759 */
689760 private static double mergedMean (final double mean , final double value , final long w , final long weight ) {
690- final double newMean = mean + ((( value - mean ) * w ) / weight ) ;
691- if (Double .isFinite (newMean )) { return newMean ; }
761+ final double delta = value - mean ;
762+ if (Double .isFinite (delta )) { return Math . fma ( delta , ( double ) w / weight , mean ) ; }
692763 return weightedAverage (mean , weight - w , value , w );
693764 }
694765}
0 commit comments