lib/bson/decimal128.rb



# frozen_string_literal: true
# rubocop:todo all
# Copyright (C) 2016-2020 MongoDB Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'bigdecimal'
require 'bson/decimal128/builder'

module BSON

  class Decimal128
    include JSON
    include Comparable

    # A Decimal128 is type 0x13 in the BSON spec.
    #
    # @since 4.2.0
    BSON_TYPE = ::String.new(19.chr, encoding: BINARY).freeze

    # Exponent offset.
    #
    # @since 4.2.0
    EXPONENT_OFFSET = 6176

    # Minimum exponent.
    #
    # @since 4.2.0
    MIN_EXPONENT = -6176

    # Maximum exponent.
    #
    # @since 4.2.0
    MAX_EXPONENT = 6111

    # Maximum digits of precision.
    #
    # @since 4.2.0
    MAX_DIGITS_OF_PRECISION = 34

    # Key for this type when converted to extended json.
    #
    # @since 4.2.0
    EXTENDED_JSON_KEY = "$numberDecimal"

    # The native type to which this object can be converted.
    #
    # @since 4.2.0
    NATIVE_TYPE = BigDecimal

    # Return a string representation of the Decimal128 use in standard
    # application-level JSON serialization. Returns nil for non-real
    # numbers such as NaN and Infinity to be compatible with ActiveSupport.
    # This method is intentionally different from #as_extended_json.
    #
    # @example Get the Decimal128 as a JSON-serializable object.
    #   decimal.as_json
    #
    # @return [ String | nil ] The decimal128 as a String or nil for non-representable numbers.
    def as_json(*args)
      value = to_s
      value unless %w[NaN Infinity -Infinity].include?(value)
    end

    # Converts this object to a representation directly serializable to
    # Extended JSON (https://github.com/mongodb/specifications/blob/master/source/extended-json/extended-json.md).
    #
    # @option opts [ nil | :relaxed | :legacy ] :mode Serialization mode
    #   (default is canonical extended JSON)
    #
    # @return [ Hash ] The extended json representation.
    def as_extended_json(**_options)
      { EXTENDED_JSON_KEY => to_s }
    end

    # Check equality of the decimal128 object with another object.
    #
    # @example Check if the decimal128 object is equal to the other.
    #   decimal == other
    #
    # @param [ Object ] other The object to check against.
    #
    # @return [ true, false ] If the objects are equal.
    #
    # @since 4.2.0
    def ==(other)
      return false unless other.is_a?(Decimal128)
      @high == other.instance_variable_get(:@high) &&
          @low == other.instance_variable_get(:@low)
    end
    alias :eql? :==

    def <=>(other)
      to_d <=> case other
        when Decimal128
          other.to_d
        else
          other
        end
    end

    # Create a new Decimal128 from a string or a BigDecimal instance.
    #
    # @example Create a Decimal128 from a BigDecimal.
    #   Decimal128.new(big_decimal)
    #
    # @param [ String, BigDecimal ] object The BigDecimal or String to use for
    #   instantiating a Decimal128.
    #
    # @raise [ BSON::Error::InvalidDecimal128Argument ] When argument is not a String or BigDecimal.
    #
    # @since 4.2.0
    def initialize(object)
      if object.is_a?(String)
        set_bits(*Builder::FromString.new(object).bits)
      elsif object.is_a?(BigDecimal)
        set_bits(*Builder::FromBigDecimal.new(object).bits)
      else
        raise Error::InvalidDecimal128Argument.new
      end
    end

    # Get the BSON type for Decimal128.
    def bson_type
      BSON_TYPE
    end

    # Get the decimal128 as its raw BSON data.
    #
    # @example Get the raw bson bytes in a buffer.
    #   decimal.to_bson
    #
    # @return [ BSON::ByteBuffer ] The buffer with the encoded object.
    #
    # @see http://bsonspec.org/#/specification
    #
    # @since 4.2.0
    def to_bson(buffer = ByteBuffer.new)
      buffer.put_decimal128(@low, @high)
    end

    # Get the hash value for the decimal128.
    #
    # @example Get the hash value.
    #   decimal.hash
    #
    # @return [ Integer ] The hash value.
    #
    # @since 4.2.0
    def hash
      num = @high << 64
      num |= @low
      num.hash
    end

    # Get a nice string for use with object inspection.
    #
    # @example Inspect the decimal128 object.
    #   decimal128.inspect
    #
    # @return [ String ] The decimal as a string.
    #
    # @since 4.2.0
    def inspect
      "BSON::Decimal128('#{to_s}')"
    end

    # Get the string representation of the decimal128.
    #
    # @example Get the decimal128 as a string.
    #   decimal128.to_s
    #
    # @return [ String ] The decimal128 as a string.
    #
    # @since 4.2.0
    def to_s
      @string ||= Builder::ToString.new(self).string
    end
    alias :to_str :to_s

    # Get a Ruby BigDecimal object corresponding to this Decimal128.
    # Note that, when converting to a Ruby BigDecimal, non-zero significant digits
    # are preserved but trailing zeroes may be lost.
    # See the following example:
    #
    # @example
    #  decimal128 = BSON::Decimal128.new("0.200")
    #    => BSON::Decimal128('0.200')
    #  big_decimal = decimal128.to_d
    #    => #<BigDecimal:7fc619c95388,'0.2E0',9(18)>
    #  big_decimal.to_s
    #    => "0.2E0"
    #
    # Note that the the BSON::Decimal128 object can represent -NaN, sNaN,
    # and -sNaN while Ruby's BigDecimal cannot.
    #
    # @return [ BigDecimal ] The decimal as a BigDecimal.
    def to_d
      @big_decimal ||= BigDecimal(to_s)
    end
    alias :to_big_decimal :to_d

    private

    def set_bits(low, high)
      @low = low
      @high = high
    end

    class << self

      # Deserialize the decimal128 from raw BSON bytes.
      #
      # @example Get the decimal128 from BSON.
      #   Decimal128.from_bson(bson)
      #
      # @param [ ByteBuffer ] buffer The byte buffer.
      #
      # @option options [ nil | :bson ] :mode Decoding mode to use.
      #
      # @return [ BSON::Decimal128 ] The decimal object.
      #
      # @since 4.2.0
      def from_bson(buffer, **options)
        from_bits(*buffer.get_decimal128_bytes.unpack('Q<*'))
      end

      # Instantiate a Decimal128 from a string.
      #
      # @example Create a Decimal128 from a string.
      #   BSON::Decimal128.from_string("1.05E+3")
      #
      # @param [ String ] string The string to parse.
      #
      # @raise [ BSON::Error:InvalidDecimal128String ] If the provided string is invalid.
      #
      # @return [ BSON::Decimal128 ] The new decimal128.
      #
      # @since 4.2.0
      def from_string(string)
        from_bits(*Builder::FromString.new(string).bits)
      end

      # Instantiate a Decimal128 from high and low bits.
      #
      # @example Create a Decimal128 from high and low bits.
      #   BSON::Decimal128.from_bits(high, low)
      #
      # @param [ Integer ] high The high order bits.
      # @param [ Integer ] low The low order bits.
      #
      # @return [ BSON::Decimal128 ] The new decimal128.
      #
      # @since 4.2.0
      def from_bits(low, high)
        decimal = allocate
        decimal.send(:set_bits, low, high)
        decimal
      end
    end
  end
end