001/* 002 * Copyright 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.ThreadSafe; 023import java.math.BigDecimal; 024 025import static java.util.Objects.requireNonNull; 026 027/** 028 * An immutable, exactly represented JSON number. 029 * <p>Equality and hashing compare numeric values regardless of decimal scale. 030 * For example, {@code 1}, {@code 1.0} and {@code 1E+0} are equal. The supplied 031 * {@link BigDecimal}, including its scale, remains available from 032 * {@link #getValue()}. 033 * 034 * @author <a href="https://www.revetkn.com">Mark Allen</a> 035 */ 036@ThreadSafe 037public final class McpJsonNumber implements McpJsonValue { 038 @NonNull 039 private final BigDecimal value; 040 041 /** 042 * Creates an immutable, exactly represented JSON number from its value. 043 * 044 * @param value the non-null numeric value 045 * @return immutable JSON number 046 */ 047 @NonNull 048 public static McpJsonNumber fromValue(@NonNull BigDecimal value) { 049 return new McpJsonNumber(value); 050 } 051 052 private McpJsonNumber(@NonNull BigDecimal value) { 053 this.value = requireNonNull(value); 054 } 055 056 /** @return exactly represented numeric value, retaining the supplied scale */ 057 @NonNull 058 public BigDecimal getValue() { 059 return this.value; 060 } 061 062 /** @return whether this object contains the same numeric value regardless of scale */ 063 @Override 064 public boolean equals(@Nullable Object other) { 065 if (this == other) 066 return true; 067 if (!(other instanceof McpJsonNumber number)) 068 return false; 069 return this.value.compareTo(number.value) == 0; 070 } 071 072 /** @return numeric hash code independent of decimal scale */ 073 @Override 074 public int hashCode() { 075 if (this.value.signum() == 0) 076 return 0; 077 // Trim coefficient text instead of repeatedly dividing a large integer 078 // by ten. Keep the adjusted scale in a long so extreme public decimals 079 // neither underflow nor require exponent expansion. 080 String coefficient = this.value.unscaledValue().toString(); 081 int end = coefficient.length(); 082 while (coefficient.charAt(end - 1) == '0') 083 --end; 084 int coefficientHash = 0; 085 for (int index = 0; index < end; ++index) 086 coefficientHash = 31 * coefficientHash + coefficient.charAt(index); 087 long normalizedScale = (long) this.value.scale() - (coefficient.length() - end); 088 return 31 * coefficientHash + Long.hashCode(normalizedScale); 089 } 090 091 /** @return redacted diagnostic rendering */ 092 @Override 093 @NonNull 094 public String toString() { 095 return "McpJsonNumber{value=<redacted>}"; 096 } 097}