mongodb/bson-ruby

View on GitHub
lib/bson/time.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true
# rubocop:todo all
# Copyright (C) 2009-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.

module BSON

  # Injects behaviour for encoding and decoding time values to
  # and from raw bytes as specified by the BSON spec.
  #
  # @note
  #   Ruby time can have nanosecond precision:
  #   +Time.utc(2020, 1, 1, 0, 0, 0, 999_999_999/1000r)+
  #   +Time#usec+ returns the number of microseconds in the time, and
  #   if the time has nanosecond precision the sub-microsecond part is
  #   truncated (the value is floored to the nearest millisecond).
  #   MongoDB only supports millisecond precision; we truncate the
  #   sub-millisecond part of microseconds (floor to the nearest millisecond).
  #   Note that if a time is constructed from a floating point value,
  #   the microsecond value may round to the starting floating point value
  #   but due to flooring, the time after serialization may end up to
  #   be different than the starting floating point value.
  #   It is recommended that time calculations use integer math only.
  #
  # @see http://bsonspec.org/#/specification
  #
  # @since 2.0.0
  module Time

    # A time is type 0x09 in the BSON spec.
    #
    # @since 2.0.0
    BSON_TYPE = ::String.new(9.chr, encoding: BINARY).freeze

    # Get the time as encoded BSON.
    #
    # @note The time is floored to the nearest millisecond.
    #
    # @example Get the time as encoded BSON.
    #   Time.new(2012, 1, 1, 0, 0, 0).to_bson
    #
    # @return [ BSON::ByteBuffer ] The buffer with the encoded object.
    #
    # @see http://bsonspec.org/#/specification
    #
    # @since 2.0.0
    def to_bson(buffer = ByteBuffer.new)
      value = _bson_to_i * 1000 + usec.divmod(1000).first
      buffer.put_int64(value)
    end

    # Converts this object to a representation directly serializable to
    # Extended JSON (https://github.com/mongodb/specifications/blob/master/source/extended-json.rst).
    #
    # @note The time is floored to the nearest millisecond.
    #
    # @option opts [ nil | :relaxed | :legacy ] :mode Serialization mode
    #   (default is canonical extended JSON)
    #
    # @return [ Hash ] The extended json representation.
    def as_extended_json(**options)
      utc_time = utc
      if options[:mode] == :relaxed && (1970..9999).include?(utc_time.year)
        if utc_time.usec != 0
          if utc_time.respond_to?(:floor)
            # Ruby 2.7+
            utc_time = utc_time.floor(3)
          else
            utc_time -= utc_time.usec.divmod(1000).last.to_r / 1000000
          end
          {'$date' => utc_time.strftime('%Y-%m-%dT%H:%M:%S.%LZ')}
        else
          {'$date' => utc_time.strftime('%Y-%m-%dT%H:%M:%SZ')}
        end
      else
        sec = utc_time._bson_to_i
        msec = utc_time.usec.divmod(1000).first
        {'$date' => {'$numberLong' => (sec * 1000 + msec).to_s}}
      end
    end

    def _bson_to_i
      # Workaround for JRuby's #to_i rounding negative timestamps up
      # rather than down (https://github.com/jruby/jruby/issues/6104)
      if BSON::Environment.jruby?
        (self - usec.to_r/1000000).to_i
      else
        to_i
      end
    end

    module ClassMethods

      # Deserialize UTC datetime from BSON.
      #
      # @param [ ByteBuffer ] buffer The byte buffer.
      #
      # @option options [ nil | :bson ] :mode Decoding mode to use.
      #
      # @return [ Time ] The decoded UTC datetime.
      #
      # @see http://bsonspec.org/#/specification
      #
      # @since 2.0.0
      def from_bson(buffer, **options)
        seconds, fragment = Int64.from_bson(buffer, mode: nil).divmod(1000)
        at(seconds, fragment * 1000).utc
      end
    end

    # Register this type when the module is loaded.
    #
    # @since 2.0.0
    Registry.register(BSON_TYPE, ::Time)
  end

  # Enrich the core Time class with this module.
  #
  # @since 2.0.0
  ::Time.send(:include, Time)
  ::Time.send(:extend, Time::ClassMethods)
end