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}