lib/duckdb/converter.rb



# frozen_string_literal: true

require 'date'
require_relative 'interval'
require_relative 'converter/int_to_sym'

module DuckDB
  QueryProgress = Struct.new(:percentage, :rows_processed, :total_rows_to_process)

  module Converter # :nodoc: all
    HALF_HUGEINT_BIT = 64
    HALF_HUGEINT = 1 << HALF_HUGEINT_BIT
    FLIP_HUGEINT = 1 << 63
    EPOCH = Time.local(1970, 1, 1)
    EPOCH_UTC = Time.new(1970, 1, 1, 0, 0, 0, 0)

    module_function

    def _to_infinity(value)
      if value.positive?
        DuckDB::Infinity::POSITIVE
      else
        DuckDB::Infinity::NEGATIVE
      end
    end

    def _to_date(year, month, day)
      Date.new(year, month, day)
    end

    def _to_time(year, month, day, hour, minute, second, microsecond)
      Time.local(year, month, day, hour, minute, second, microsecond)
    end

    def _to_time_from_duckdb_time(hour, minute, second, microsecond)
      Time.parse(
        format(
          '%<hour>02d:%<minute>02d:%<second>02d.%<microsecond>06d',
          hour: hour,
          minute: minute,
          second: second,
          microsecond: microsecond
        )
      )
    end

    def _to_time_from_duckdb_timestamp_s(time)
      EPOCH + time
    end

    def _to_time_from_duckdb_timestamp_ms(time)
      tm = EPOCH + (time / 1000)
      Time.local(tm.year, tm.month, tm.day, tm.hour, tm.min, tm.sec, time % 1000 * 1000)
    end

    def _to_time_from_duckdb_timestamp_ns(time)
      tm = EPOCH + (time / 1_000_000_000)
      Time.local(tm.year, tm.month, tm.day, tm.hour, tm.min, tm.sec, time % 1_000_000_000 / 1000)
    end

    def _to_time_from_duckdb_time_tz(hour, min, sec, micro, timezone)
      sign = '+'
      if timezone.negative?
        timezone = -timezone
        sign = '-'
      end

      tzhour = timezone / 3600
      tzmin = (timezone % 3600) / 60

      Time.parse(
        format(
          '%<hour>02d:%<min>02d:%<sec>02d.%<micro>06d%<sign>s%<tzhour>02d:%<tzmin>02d',
          hour: hour,
          min: min,
          sec: sec,
          micro: micro,
          sign: sign,
          tzhour: tzhour,
          tzmin: tzmin
        )
      )
    end

    def _to_time_from_duckdb_timestamp_tz(bits)
      micro = bits % 1_000_000
      sec = (bits / 1_000_000)
      time = EPOCH_UTC + sec

      Time.parse(
        format(
          '%<year>04d-%<mon>02d-%<day>02d %<hour>02d:%<min>02d:%<sec>02d.%<micro>06d +0000',
          year: time.year,
          mon: time.month,
          day: time.day,
          hour: time.hour,
          min: time.min,
          sec: time.sec,
          micro: micro
        )
      )
    end

    def _to_hugeint_from_vector(lower, upper)
      (upper << HALF_HUGEINT_BIT) + lower
    end

    def _to_decimal_from_hugeint(width, scale, upper, lower = nil)
      v = lower.nil? ? upper : _to_hugeint_from_vector(lower, upper)
      _to_decimal_from_value(width, scale, v)
    end

    def _to_decimal_from_value(_width, scale, value)
      v = value.to_s
      v = v.rjust(scale + 1, '0') if v.length < scale
      v[-scale, 0] = '.' if scale.positive?
      BigDecimal(v)
    end

    def _to_interval_from_vector(months, days, micros)
      Interval.new(interval_months: months, interval_days: days, interval_micros: micros)
    end

    def _to_uuid_from_vector(lower, upper)
      upper ^= FLIP_HUGEINT
      upper += HALF_HUGEINT if upper.negative?

      str = _to_hugeint_from_vector(lower, upper).to_s(16).rjust(32, '0')
      "#{str[0, 8]}-#{str[8, 4]}-#{str[12, 4]}-#{str[16, 4]}-#{str[20, 12]}"
    end

    def _parse_date(value)
      case value
      when Date, Time
        value
      else
        begin
          Date.parse(value)
        rescue StandardError => e
          raise(ArgumentError, "Cannot parse `#{value.inspect}` to Date object. #{e.message}")
        end
      end
    end

    def _parse_time(value)
      case value
      when Time
        value
      else
        begin
          Time.parse(value)
        rescue StandardError => e
          raise(ArgumentError, "Cannot parse `#{value.inspect}` to Time object. #{e.message}")
        end
      end
    end

    def _parse_deciaml(value)
      case value
      when BigDecimal
        value
      else
        begin
          BigDecimal(value.to_s)
        rescue StandardError => e
          raise(ArgumentError, "Cannot parse `#{value.inspect}` to BigDecimal object. #{e.message}")
        end
      end
    end

    def _to_query_progress(percentage, rows_processed, total_rows_to_process)
      DuckDB::QueryProgress.new(percentage, rows_processed, total_rows_to_process).freeze
    end

    private

    def integer_to_hugeint(value)
      case value
      when Integer
        upper = value >> HALF_HUGEINT_BIT
        lower = value - (upper << HALF_HUGEINT_BIT)
        [lower, upper]
      else
        raise(ArgumentError, "The argument `#{value.inspect}` must be Integer.")
      end
    end

    def decimal_to_hugeint(value)
      integer_value = (value * (10 ** value.scale)).to_i
      integer_to_hugeint(integer_value)
    rescue FloatDomainError => e
      raise(ArgumentError, "The argument `#{value.inspect}` must be converted to Integer. #{e.message}")
    end
  end
end