# frozen_string_literal: true

require "yaml"

RSpec.describe Unit.base_units do
  it { is_expected.to be_a Array }

  it "has 14 elements" do
    expect(subject.size).to eq(14)
  end

  %w[kilogram meter second ampere degK tempK mole candela each dollar steradian radian decibel byte].each do |u|
    it { is_expected.to include(RubyUnits::Unit.new(u)) }
  end
end

RSpec.describe "Create some simple units" do
  # zero string
  describe RubyUnits::Unit.new("0") do
    it { is_expected.to be_a Numeric }
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to eq(0) }
      it { is_expected.to be_an Integer }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to be_empty }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:unitless) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.to be_unitless }
    it { is_expected.to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to eq(subject) }
    end
  end

  # non-zero string
  describe RubyUnits::Unit.new("1") do
    it { is_expected.to be_a Numeric }
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to eq(1) }
      it { is_expected.to be_an Integer }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to be_empty }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:unitless) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to eq(subject) }
    end
  end

  # numeric
  describe RubyUnits::Unit.new(1) do
    it { is_expected.to be_a Numeric }
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to eq(1) }
      it { is_expected.to be_an Integer }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to be_empty }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:unitless) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to eq(subject) }
    end
  end

  # rational
  describe RubyUnits::Unit.new(Rational(1, 2)) do
    it { is_expected.to be_a Numeric }
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to eq(1/2r) }
      it { is_expected.to be_a Rational }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to be_empty }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:unitless) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to eq(subject) }
    end
  end

  # float
  describe RubyUnits::Unit.new(0.5) do
    it { is_expected.to be_a Numeric }
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to eq(0.5) }
      it { is_expected.to be_a Float }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to be_empty }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:unitless) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to eq(subject) }
    end
  end

  # complex
  describe RubyUnits::Unit.new(Complex(1, 1)) do
    it { is_expected.to be_a Numeric }
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to eq(Complex(1, 1)) }
      it { is_expected.to be_a Complex }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to be_empty }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:unitless) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to eq(subject) }
    end
  end

  describe RubyUnits::Unit.new("1+1i m") do
    it { is_expected.to be_a Numeric }
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to eq(Complex(1, 1)) }
      it { is_expected.to be_a Complex }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("m") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:length) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to eq(subject) }
    end
  end

  # scalar and unit
  describe RubyUnits::Unit.new("1 mm") do
    it { is_expected.to be_a Numeric }
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to eq(1) }
      it { is_expected.to be_an Integer }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("mm") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:length) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to eq(RubyUnits::Unit.new("0.001 m")) }
    end
  end

  # with a zero power
  describe RubyUnits::Unit.new("1 m^0") do
    it { is_expected.to be_a Numeric }
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to eq(1) }
      it { is_expected.to be_an Integer }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:unitless) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to eq(RubyUnits::Unit.new("1")) }
    end
  end

  # unit only
  describe RubyUnits::Unit.new("mm") do
    it { is_expected.to be_a Numeric }
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to eq(1) }
      it { is_expected.to be_an Integer }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("mm") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:length) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to eq(RubyUnits::Unit.new("0.001 m")) }
    end
  end

  # Compound unit
  describe RubyUnits::Unit.new("1 N*m") do
    it { is_expected.to be_a Numeric }
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to eq(1) }
      it { is_expected.to be_an Integer }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("N*m") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:energy) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to eq(RubyUnits::Unit.new("1 kg*m^2/s^2")) }
    end
  end

  # scalar and unit with powers
  describe RubyUnits::Unit.new("10 m/s^2") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to eq(10) }
      it { is_expected.to be_an Integer }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("m/s^2") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:acceleration) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to eq(RubyUnits::Unit.new("10 m/s^2")) }
    end
  end

  # feet/in form
  ["5 feet 6 inches", "5 feet 6 inch", "5ft 6in", "5 ft 6 in", %(5'6"), %(5' 6")].each do |unit_string|
    describe unit_string do
      subject(:unit) { RubyUnits::Unit.new(unit_string) }

      it { is_expected.to be_an_instance_of Unit }
      it { is_expected.to have_attributes(scalar: 5.5, units: "ft", kind: :length) }
      it { is_expected.not_to be_temperature }
      it { is_expected.not_to be_degree }
      it { is_expected.not_to be_base }
      it { is_expected.not_to be_unitless }
      it { is_expected.not_to be_zero }
      it { expect(unit.base).to be_within(RubyUnits::Unit.new("0.01 m")).of RubyUnits::Unit.new("1.6764 m") }
      it { expect(unit.to_s(:ft)).to eq(%(5'6")) }
    end
  end

  describe RubyUnits::Unit.new(%(-5' 3/4")) do
    it { is_expected.to be_an_instance_of Unit }
    it { is_expected.to have_attributes(scalar: -5.0625, units: "ft", kind: :length) }
  end

  describe RubyUnits::Unit.new(%(-5' 1 3/4")) do
    it { is_expected.to be_an_instance_of Unit }
    it { is_expected.to have_attributes(scalar: (-247/48r), units: "ft", kind: :length) }
  end

  # pound/ounces form
  describe "6lbs 5oz" do
    subject(:unit) { RubyUnits::Unit.new("6lbs 5oz") }

    it { is_expected.to be_an_instance_of Unit }
    it { is_expected.to have_attributes(scalar: 6.3125, units: "lbs", kind: :mass) }
    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }
    it { expect(unit.base).to be_within(RubyUnits::Unit.new("0.01 kg")).of RubyUnits::Unit.new("2.8633 kg") }
    it { expect(unit.to_s(:lbs)).to eq("6 lbs 5 oz") }
  end

  describe RubyUnits::Unit.new("-6lbs 5oz") do
    it { is_expected.to be_an_instance_of Unit }
    it { is_expected.to have_attributes(scalar: -6.3125, units: "lbs", kind: :mass) }
  end

  describe RubyUnits::Unit.new("-6lbs 5 3/4oz") do
    it { is_expected.to be_an_instance_of Unit }
    it { is_expected.to have_attributes(scalar: -6.359375, units: "lbs", kind: :mass) }
  end

  # temperature
  describe RubyUnits::Unit.new("100 tempC") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_within(0.001).of 100 }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("tempC") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:temperature) }
    end

    it { is_expected.to be_temperature }
    it { is_expected.to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_within(RubyUnits::Unit.new("0.01 degK")).of RubyUnits::Unit.new("373.15 tempK") }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to eq("degC") }
    end
  end

  # Time
  describe RubyUnits::Unit.new("1:00") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_a(Numeric) }
      it { is_expected.to eq(1) }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("h") }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }
  end

  describe RubyUnits::Unit.new("1:23") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_a(Numeric) }
      it { is_expected.to eq(83/60r) }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("h") }
    end
  end

  describe RubyUnits::Unit.new("1:23:45") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_a(Numeric) }
      it { is_expected.to eq(67/48r) }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("h") }
    end
  end

  describe RubyUnits::Unit.new("1:23:45.67") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_a(Numeric) }
      it { is_expected.to eq(5_025_067/3600000r) }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("h") }
    end
  end

  describe RubyUnits::Unit.new(Time.now) do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_a(Numeric) }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("s") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:time) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a(Numeric) }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end
  end

  # degrees
  describe RubyUnits::Unit.new("100 degC") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_within(0.001).of 100 }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("degC") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:temperature) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_within(RubyUnits::Unit.new("0.01 degK")).of RubyUnits::Unit.new("100 degK") }
    end
  end

  # percent
  describe RubyUnits::Unit.new("75%") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_an Integer }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("%") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:unitless) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a(Numeric) }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end
  end

  # angle
  describe RubyUnits::Unit.new("180 deg") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_a Numeric }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("deg") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:angle) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a(Numeric) }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end
  end

  # radians
  describe RubyUnits::Unit.new("1 radian") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_a Numeric }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("rad") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:angle) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a Numeric }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end
  end

  # counting
  describe RubyUnits::Unit.new("12 dozen") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_an Integer }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("doz") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:unitless) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a Numeric }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end
  end

  # rational scalar with unit
  describe RubyUnits::Unit.new("1/2 kg") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_an Rational }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("kg") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:mass) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a Numeric }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end
  end

  describe RubyUnits::Unit.new("6/1 lbs") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#to_s" do
      subject { super().to_s }

      it { is_expected.to eq "6 lbs" }
    end
  end

  # rational scalar with compound unit
  describe RubyUnits::Unit.new("1/2 kg/m") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_an Rational }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("kg/m") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to be_nil }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a Numeric }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end
  end

  # rational scalar with numeric modified unit
  describe Unit.new("12.0mg/6.0mL") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_an Numeric }
      it { is_expected.to eq(12.0 / 6.0) }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq "mg/ml" }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq :density }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a Numeric }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end
  end

  # time string
  describe RubyUnits::Unit.new("1:23:45.200") do
    it { is_expected.to be_an_instance_of Unit }
    it { is_expected.to eq(RubyUnits::Unit.new("1 h") + RubyUnits::Unit.new("23 min") + RubyUnits::Unit.new("45 seconds") + RubyUnits::Unit.new("200 ms")) }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_an Rational }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("h") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:time) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a Numeric }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end
  end

  # also  '1 hours as minutes'
  #       '1 hour to minutes'
  describe Unit.parse("1 hour in minutes") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to eq 60 }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("min") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:time) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a Numeric }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end
  end

  # funky unit
  describe RubyUnits::Unit.new("1 attoparsec/microfortnight") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_an Integer }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("apc/ufortnight") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:speed) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a Numeric }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end

    it { expect(subject.convert_to("in/s")).to be_within(RubyUnits::Unit.new("0.0001 in/s")).of(RubyUnits::Unit.new("1.0043269330917 in/s")) }
  end

  # Farads
  describe RubyUnits::Unit.new("1 F") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_an Integer }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("F") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:capacitance) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a Numeric }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end
  end

  describe RubyUnits::Unit.new("1 m^2 s^-2") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_an Integer }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("m^2/s^2") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:radiation) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a Numeric }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end
  end

  describe RubyUnits::Unit.new(1, "m^2", "s^2") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_an Integer }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("m^2/s^2") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:radiation) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a Numeric }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end
  end

  # scientific notation
  describe RubyUnits::Unit.new("1e6 cells") do
    it { is_expected.to be_an_instance_of Unit }

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to be_an Integer }
    end

    describe "#scalar" do
      subject { super().scalar }

      it { is_expected.to eq(1e6) }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("cells") }
    end

    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:unitless) }
    end

    it { is_expected.not_to be_temperature }
    it { is_expected.not_to be_degree }
    it { is_expected.not_to be_base }
    it { is_expected.not_to be_unitless }
    it { is_expected.not_to be_zero }

    describe "#base" do
      subject { super().base }

      it { is_expected.to be_a Numeric }
    end

    describe "#temperature_scale" do
      subject { super().temperature_scale }

      it { is_expected.to be_nil }
    end
  end

  # could be m*m
  describe RubyUnits::Unit.new("1 mm") do
    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:length) }
    end
  end

  # could be centi-day
  describe RubyUnits::Unit.new("1 cd") do
    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:luminous_power) }
    end
  end

  # could be milli-inch
  describe RubyUnits::Unit.new("1 min") do
    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:time) }
    end
  end

  # could be femto-tons
  describe RubyUnits::Unit.new("1 ft") do
    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:length) }
    end
  end

  # could be deci-ounce
  describe RubyUnits::Unit.new("1 doz") do
    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:unitless) }
    end
  end

  # explicit create
  describe RubyUnits::Unit.new("1 <meter>/<second>") do
    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:speed) }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("m/s") }
    end
  end

  describe RubyUnits::Unit.new("1 <kilogram><meter>/<second><second><second>") do
    describe "#kind" do
      subject { super().kind }

      it { is_expected.to eq(:yank) }
    end

    describe "#units" do
      subject { super().units }

      it { is_expected.to eq("kg*m/s^3") }
    end
  end

  # without spaces
  describe RubyUnits::Unit.new("1g") do
    specify { expect(subject).to eq(RubyUnits::Unit.new("1 g")) }
  end

  describe RubyUnits::Unit.new("-1g") do
    specify { expect(subject).to eq(RubyUnits::Unit.new("-1 g")) }
  end

  describe RubyUnits::Unit.new("11/s") do
    specify { expect(subject).to eq(RubyUnits::Unit.new("11 1/s")) }
  end

  describe Unit.new("63.5029318kg") do
    specify { expect(subject).to eq(Unit.new("63.5029318 kg")) }
  end

  # mixed fraction
  describe Unit.new("6 1/2 cups") do
    specify { expect(subject).to eq(Unit.new("13/2 cu")) }
  end

  # mixed fraction
  describe Unit.new("-6-1/2 cups") do
    specify { expect(subject).to eq(Unit.new("-13/2 cu")) }
  end

  describe Unit.new("100 mcg") do
    specify { expect(subject).to eq(Unit.new("100 ug")) }
  end

  describe Unit.new("100 mcL") do
    specify { expect(subject).to eq(Unit.new("100 uL")) }
  end
end

describe "Unit handles attempts to create bad units" do
  specify "no empty strings" do
    expect { RubyUnits::Unit.new("") }.to raise_error(ArgumentError, "No Unit Specified")
  end

  specify "no blank strings" do
    expect { RubyUnits::Unit.new("   ") }.to raise_error(ArgumentError, "No Unit Specified")
  end

  specify "no strings with tabs" do
    expect { RubyUnits::Unit.new("\t") }.to raise_error(ArgumentError, "No Unit Specified")
  end

  specify "no strings with newlines" do
    expect { RubyUnits::Unit.new("\n") }.to raise_error(ArgumentError, "No Unit Specified")
  end

  specify "no double slashes" do
    expect { RubyUnits::Unit.new("3 s/s/ft") }.to raise_error(ArgumentError, /Unit not recognized/)
  end

  specify "no pipes or commas" do
    expect { RubyUnits::Unit.new("3 s**2|,s**2") }.to raise_error(ArgumentError, /Unit not recognized/)
  end

  specify "no multiple spaces" do
    expect { RubyUnits::Unit.new("3 s**2 4s s**2") }.to raise_error(ArgumentError, /Unit not recognized/)
  end

  specify "no exponentiation of numbers" do
    expect { RubyUnits::Unit.new("3 s 5^6") }.to raise_error(ArgumentError, /Unit not recognized/)
  end

  specify "no strings that don't specify a valid unit" do
    expect { RubyUnits::Unit.new("random string") }.to raise_error(ArgumentError, "'random string' Unit not recognized")
  end

  specify "no unhandled classes" do
    expect { RubyUnits::Unit.new(STDIN) }.to raise_error(ArgumentError, "Invalid Unit Format")
  end

  specify "no undefined units" do
    expect { RubyUnits::Unit.new("1 mFoo") }.to raise_error(ArgumentError, "'1 mFoo' Unit not recognized")
    expect { RubyUnits::Unit.new("1 second/mFoo") }.to raise_error(ArgumentError, "'1 second/mFoo' Unit not recognized")
  end

  specify "no units with powers greater than 19" do
    expect { RubyUnits::Unit.new("1 m^20") }.to raise_error(ArgumentError, "Power out of range (-20 < net power of a unit < 20)")
  end

  specify "no units with powers less than 19" do
    expect { RubyUnits::Unit.new("1 m^-20") }.to raise_error(ArgumentError, "Power out of range (-20 < net power of a unit < 20)")
  end

  specify "no temperatures less than absolute zero" do
    expect { RubyUnits::Unit.new("-100 tempK") }.to raise_error(ArgumentError, "Temperatures must not be less than absolute zero")
    expect { RubyUnits::Unit.new("-100 tempR") }.to raise_error(ArgumentError, "Temperatures must not be less than absolute zero")
    expect { RubyUnits::Unit.new("-500/9 tempR") }.to raise_error(ArgumentError, "Temperatures must not be less than absolute zero")
  end

  specify "no nil scalar" do
    expect { RubyUnits::Unit.new(nil, "feet") }.to raise_error(ArgumentError, "Invalid Unit Format")
    expect { RubyUnits::Unit.new(nil, "feet", "min") }.to raise_error(ArgumentError, "Invalid Unit Format")
  end

  specify "no double prefixes" do
    expect { Unit.new("1 mmm") }.to raise_error(ArgumentError, /Unit not recognized/)
  end
end

describe Unit do
  it "is a subclass of Numeric" do
    expect(described_class).to be < Numeric
  end

  it "is Comparable" do
    expect(described_class).to be < Comparable
  end

  describe "#defined?" do
    it "returns true when asked about a defined unit" do
      expect(Unit.defined?("meter")).to be_truthy
    end

    it "returns true when asked about an alias for a unit" do
      expect(Unit.defined?("m")).to be_truthy
    end

    it "returns false when asked about a unit that is not defined" do
      expect(Unit.defined?("doohickey")).to be_falsey
    end
  end

  describe "#to_yaml" do
    subject { RubyUnits::Unit.new("1 mm") }

    describe "#to_yaml" do
      subject { super().to_yaml }

      it { is_expected.to match(%r{--- !ruby/object:RubyUnits::Unit}) }
    end
  end

  describe "#definition" do
    context "The requested unit is defined" do
      before do
        @definition = Unit.definition("mph")
      end

      it "returns a Unit::Definition" do
        expect(@definition).to be_instance_of(Unit::Definition)
      end

      specify { expect(@definition.name).to eq("<mph>") }
      specify { expect(@definition.aliases).to eq(%w[mph]) }
      specify { expect(@definition.numerator).to eq(["<meter>"]) }
      specify { expect(@definition.denominator).to eq(["<second>"]) }
      specify { expect(@definition.kind).to eq(:speed) }
      specify { expect(@definition.scalar).to eq(0.44704) }
    end

    context "The requested unit is not defined" do
      it "returns nil" do
        expect(Unit.definition("doohickey")).to be_nil
      end
    end
  end

  describe "#define" do
    describe "a new unit" do
      before do
        # do this because the unit is not defined at the time this file is parsed, so it fails
        @jiffy = Unit.define("jiffy") do |jiffy|
          jiffy.scalar = Rational(1, 100)
          jiffy.aliases = %w[jif]
          jiffy.numerator = ["<second>"]
          jiffy.kind = :time
        end
      end

      after do
        Unit.undefine!("jiffy")
      end

      describe "RubyUnits::Unit.new('1e6 jiffy')" do
        subject { RubyUnits::Unit.new("1e6 jiffy") }

        it { is_expected.to be_a Numeric }
        it { is_expected.to be_an_instance_of Unit }

        describe "#scalar" do
          subject { super().scalar }

          it { is_expected.to eq(1e6) }
        end

        describe "#scalar" do
          subject { super().scalar }

          it { is_expected.to be_an Integer }
        end

        describe "#units" do
          subject { super().units }

          it { is_expected.to eq("jif") }
        end

        describe "#kind" do
          subject { super().kind }

          it { is_expected.to eq(:time) }
        end

        it { is_expected.not_to be_temperature }
        it { is_expected.not_to be_degree }
        it { is_expected.not_to be_base }
        it { is_expected.not_to be_unitless }
        it { is_expected.not_to be_zero }

        describe "#base" do
          subject { super().base }

          it { is_expected.to eq(RubyUnits::Unit.new("10000 s")) }
        end
      end

      it "registers the new unit" do
        expect(Unit.defined?("jiffy")).to be_truthy
      end
    end

    describe "an existing unit again" do
      before do
        @cups = Unit.definition("cup")
        @original_display_name = @cups.display_name
        @cups.display_name = "cupz"
        Unit.define(@cups)
      end

      after do
        Unit.redefine!("cup") do |cup|
          cup.display_name = @original_display_name
        end
      end

      describe "RubyUnits::Unit.new('1 cup')" do
        # do this because the unit is going to be redefined
        subject { RubyUnits::Unit.new("1 cup") }

        it { is_expected.to be_a Numeric }
        it { is_expected.to be_an_instance_of Unit }

        describe "#scalar" do
          subject { super().scalar }

          it { is_expected.to eq(1) }
        end

        describe "#scalar" do
          subject { super().scalar }

          it { is_expected.to be_an Integer }
        end

        describe "#units" do
          subject { super().units }

          it { is_expected.to eq("cupz") }
        end

        describe "#kind" do
          subject { super().kind }

          it { is_expected.to eq(:volume) }
        end

        it { is_expected.not_to be_temperature }
        it { is_expected.not_to be_degree }
        it { is_expected.not_to be_base }
        it { is_expected.not_to be_unitless }
        it { is_expected.not_to be_zero }
      end
    end
  end

  describe "#redefine!" do
    before do
      @jiffy = Unit.define("jiffy") do |jiffy|
        jiffy.scalar = (1 / 100)
        jiffy.aliases = %w[jif]
        jiffy.numerator = ["<second>"]
        jiffy.kind = :time
      end

      Unit.redefine!("jiffy") do |jiffy|
        jiffy.scalar = (1 / 1000)
      end
    end

    after do
      Unit.undefine!("jiffy")
    end

    specify { expect(RubyUnits::Unit.new("1 jiffy").to_base.scalar).to eq(1 / 1000) }
  end

  describe "#undefine!" do
    before do
      @jiffy = Unit.define("jiffy") do |jiffy|
        jiffy.scalar = (1 / 100)
        jiffy.aliases = %w[jif]
        jiffy.numerator = ["<second>"]
        jiffy.kind = :time
      end
      Unit.undefine!("jiffy")
    end

    specify "the unit should be undefined" do
      expect(Unit.defined?("jiffy")).to be_falsey
    end

    specify "attempting to use an undefined unit fails" do
      expect { RubyUnits::Unit.new("1 jiffy") }.to raise_exception(ArgumentError)
    end

    it "returns true when undefining an unknown unit" do
      expect(Unit.defined?("unknown")).to be_falsey
      expect(Unit.undefine!("unknown")).to be_truthy
    end
  end

  describe "#clone" do
    subject { RubyUnits::Unit.new("1 mm") }

    describe "#clone" do
      subject { super().clone }

      it { is_expected.to be_same_as(subject) }
    end
  end

  describe "#eliminate_terms" do
    subject(:unit) { RubyUnits::Unit.new("1 kg*m/kg*s").eliminate_terms }

    it "returns a new Unit" do
      expect(unit).to be_a(RubyUnits::Unit)
    end

    it "the new unit has common terms removed" do
      expect(unit).to have_attributes(
        to_s: "1 m/s",
        denominator: ["<second>"],
        kind: :speed,
        numerator: ["<meter>"]
      )
    end
  end
end

describe "Unit Comparisons" do
  context "Unit should detect if two units are 'compatible' (i.e., can be converted into each other)" do
    specify { expect(RubyUnits::Unit.new("1 ft") =~ RubyUnits::Unit.new("1 m")).to be true }
    specify { expect(RubyUnits::Unit.new("1 ft") =~ "m").to be true }
    specify { expect(RubyUnits::Unit.new("1 ft")).to be_compatible_with RubyUnits::Unit.new("1 m") }
    specify { expect(RubyUnits::Unit.new("1 ft")).to be_compatible_with "m" }
    specify { expect(RubyUnits::Unit.new("1 m")).to be_compatible_with RubyUnits::Unit.new("1 kg*m/kg") }
    specify { expect(RubyUnits::Unit.new("1 ft") =~ RubyUnits::Unit.new("1 kg")).to be false }
    specify { expect(RubyUnits::Unit.new("1 ft")).not_to be_compatible_with RubyUnits::Unit.new("1 kg") }
    specify { expect(RubyUnits::Unit.new("1 ft")).not_to be_compatible_with nil }
  end

  context "Equality" do
    context "with uncoercable objects" do
      specify { expect(RubyUnits::Unit.new("1 mm")).not_to eq(nil) }
    end

    context "units of same kind" do
      specify { expect(RubyUnits::Unit.new("1000 m")).to eq(RubyUnits::Unit.new("1 km")) }
      specify { expect(RubyUnits::Unit.new("100 m")).not_to eq(RubyUnits::Unit.new("1 km")) }
      specify { expect(RubyUnits::Unit.new("1 m")).to eq(RubyUnits::Unit.new("100 cm")) }
    end

    context "units of incompatible types" do
      specify { expect(RubyUnits::Unit.new("1 m")).not_to eq(RubyUnits::Unit.new("1 kg")) }
    end

    context "units with a zero scalar are equal" do
      specify { expect(RubyUnits::Unit.new("0 m")).to eq(RubyUnits::Unit.new("0 s")) }
      specify { expect(RubyUnits::Unit.new("0 m")).to eq(RubyUnits::Unit.new("0 kg")) }

      context "except for temperature units" do
        specify { expect(RubyUnits::Unit.new("0 tempK")).to eq(RubyUnits::Unit.new("0 m")) }
        specify { expect(RubyUnits::Unit.new("0 tempR")).to eq(RubyUnits::Unit.new("0 m")) }
        specify { expect(RubyUnits::Unit.new("0 tempC")).not_to eq(RubyUnits::Unit.new("0 m")) }
        specify { expect(RubyUnits::Unit.new("0 tempF")).not_to eq(RubyUnits::Unit.new("0 m")) }
      end
    end
  end

  context "Equivalence" do
    context "units and scalars are exactly the same" do
      specify { expect(RubyUnits::Unit.new("1 m") === RubyUnits::Unit.new("1 m")).to be true } # rubocop:disable Style/CaseEquality
      specify { expect(RubyUnits::Unit.new("1 m")).to be_same RubyUnits::Unit.new("1 m") }
      specify { expect(RubyUnits::Unit.new("1 m")).to be_same_as RubyUnits::Unit.new("1 m") }
    end

    context "units are compatible but not identical" do
      specify { expect(RubyUnits::Unit.new("1000 m") === RubyUnits::Unit.new("1 km")).to be false } # rubocop:disable Style/CaseEquality
      specify { expect(RubyUnits::Unit.new("1000 m")).not_to be_same RubyUnits::Unit.new("1 km") }
      specify { expect(RubyUnits::Unit.new("1000 m")).not_to be_same_as RubyUnits::Unit.new("1 km") }
    end

    context "units are not compatible" do
      specify { expect(RubyUnits::Unit.new("1000 m") === RubyUnits::Unit.new("1 hour")).to be false } # rubocop:disable Style/CaseEquality
      specify { expect(RubyUnits::Unit.new("1000 m")).not_to be_same RubyUnits::Unit.new("1 hour") }
      specify { expect(RubyUnits::Unit.new("1000 m")).not_to be_same_as RubyUnits::Unit.new("1 hour") }
    end

    context "scalars are different" do
      specify { expect(RubyUnits::Unit.new("1 m") === RubyUnits::Unit.new("2 m")).to be false } # rubocop:disable Style/CaseEquality
      specify { expect(RubyUnits::Unit.new("1 m")).not_to be_same RubyUnits::Unit.new("2 m") }
      specify { expect(RubyUnits::Unit.new("1 m")).not_to be_same_as RubyUnits::Unit.new("2 m") }
    end

    specify { expect(RubyUnits::Unit.new("1 m")).not_to be_nil }
  end

  context "Comparisons" do
    context "compatible units can be compared" do
      specify { expect(RubyUnits::Unit.new("1 m")).to be < RubyUnits::Unit.new("2 m") }
      specify { expect(RubyUnits::Unit.new("2 m")).to be > RubyUnits::Unit.new("1 m") }
      specify { expect(RubyUnits::Unit.new("1 m")).to be < RubyUnits::Unit.new("1 mi") }
      specify { expect(RubyUnits::Unit.new("2 m")).to be > RubyUnits::Unit.new("1 ft") }
      specify { expect(RubyUnits::Unit.new("70 tempF")).to be > RubyUnits::Unit.new("10 degC") }
      specify { expect(RubyUnits::Unit.new("1 m")).to be > 0 }
      specify { expect { RubyUnits::Unit.new("1 m") > nil }.to raise_error(ArgumentError, /comparison of RubyUnits::Unit with (nil failed|NilClass)/) }
    end

    context "incompatible units cannot be compared" do
      specify { expect { RubyUnits::Unit.new("1 m") < RubyUnits::Unit.new("1 liter") }.to raise_error(ArgumentError, "Incompatible Units ('m' not compatible with 'l')") }
      specify { expect { RubyUnits::Unit.new("1 kg") > RubyUnits::Unit.new("60 mph") }.to raise_error(ArgumentError, "Incompatible Units ('kg' not compatible with 'mph')") }
    end

    context "with coercions should be valid" do
      specify { expect(RubyUnits::Unit.new("1GB") > "500MB").to eq(true) }
      specify { expect(RubyUnits::Unit.new("0.5GB") < "900MB").to eq(true) }
    end
  end
end

describe "Unit Conversions" do
  context "between compatible units" do
    specify { expect(RubyUnits::Unit.new("1 s").convert_to("ns")).to eq(RubyUnits::Unit.new("1e9 ns")) }
    specify { expect(RubyUnits::Unit.new("1 s").convert_to("ns")).to eq(RubyUnits::Unit.new("1e9 ns")) }
    specify { expect(RubyUnits::Unit.new("1 s") >> "ns").to eq(RubyUnits::Unit.new("1e9 ns")) }

    specify { expect(RubyUnits::Unit.new("1 m").convert_to(RubyUnits::Unit.new("ft"))).to be_within(RubyUnits::Unit.new("0.001 ft")).of(RubyUnits::Unit.new("3.28084 ft")) }
  end

  context "between incompatible units" do
    specify { expect { RubyUnits::Unit.new("1 s").convert_to("m") }.to raise_error(ArgumentError, "Incompatible Units ('1 s' not compatible with 'm')") }
  end

  context "given bad input" do
    specify { expect { RubyUnits::Unit.new("1 m").convert_to("random string") }.to raise_error(ArgumentError, "'random string' Unit not recognized") }
    specify { expect { RubyUnits::Unit.new("1 m").convert_to(STDOUT) }.to raise_error(ArgumentError, "Unknown target units") }
  end

  context "between temperature scales" do
    # NOTE: that 'temp' units are for temperature readings on a scale, while 'deg' units are used to represent
    # differences between temperatures, offsets, or other differential temperatures.

    specify { expect(RubyUnits::Unit.new("100 tempC")).to be_within(RubyUnits::Unit.new("0.001 degK")).of(RubyUnits::Unit.new("373.15 tempK")) }
    specify { expect(RubyUnits::Unit.new("0 tempC")).to be_within(RubyUnits::Unit.new("0.001 degK")).of(RubyUnits::Unit.new("273.15 tempK")) }
    specify { expect(RubyUnits::Unit.new("37 tempC")).to be_within(RubyUnits::Unit.new("0.01 degK")).of(RubyUnits::Unit.new("310.15 tempK")) }
    specify { expect(RubyUnits::Unit.new("-273.15 tempC")).to eq(RubyUnits::Unit.new("0 tempK")) }

    specify { expect(RubyUnits::Unit.new("212 tempF")).to be_within(RubyUnits::Unit.new("0.001 degK")).of(RubyUnits::Unit.new("373.15 tempK")) }
    specify { expect(RubyUnits::Unit.new("32 tempF")).to be_within(RubyUnits::Unit.new("0.001 degK")).of(RubyUnits::Unit.new("273.15 tempK")) }
    specify { expect(RubyUnits::Unit.new("98.6 tempF")).to be_within(RubyUnits::Unit.new("0.01 degK")).of(RubyUnits::Unit.new("310.15 tempK")) }
    specify { expect(RubyUnits::Unit.new("-459.67 tempF")).to eq(RubyUnits::Unit.new("0 tempK")) }

    specify { expect(RubyUnits::Unit.new("671.67 tempR")).to be_within(RubyUnits::Unit.new("0.001 degK")).of(RubyUnits::Unit.new("373.15 tempK")) }
    specify { expect(RubyUnits::Unit.new("491.67 tempR")).to be_within(RubyUnits::Unit.new("0.001 degK")).of(RubyUnits::Unit.new("273.15 tempK")) }
    specify { expect(RubyUnits::Unit.new("558.27 tempR")).to be_within(RubyUnits::Unit.new("0.01 degK")).of(RubyUnits::Unit.new("310.15 tempK")) }
    specify { expect(RubyUnits::Unit.new("0 tempR")).to eq(RubyUnits::Unit.new("0 tempK")) }

    specify { expect(RubyUnits::Unit.new("100 tempK").convert_to("tempC")).to be_within(RubyUnits::Unit.new("0.01 degC")).of(RubyUnits::Unit.new("-173.15 tempC")) }
    specify { expect(RubyUnits::Unit.new("100 tempK").convert_to("tempF")).to be_within(RubyUnits::Unit.new("0.01 degF")).of(RubyUnits::Unit.new("-279.67 tempF")) }
    specify { expect(RubyUnits::Unit.new("100 tempK").convert_to("tempR")).to be_within(RubyUnits::Unit.new("0.01 degR")).of(RubyUnits::Unit.new("180 tempR")) }

    specify { expect(RubyUnits::Unit.new("1 degC")).to eq(RubyUnits::Unit.new("1 degK")) }
    specify { expect(RubyUnits::Unit.new("1 degF")).to eq(RubyUnits::Unit.new("1 degR")) }
    specify { expect(RubyUnits::Unit.new("1 degC")).to eq(RubyUnits::Unit.new("1.8 degR")) }
    specify { expect(RubyUnits::Unit.new("1 degF")).to be_within(RubyUnits::Unit.new("0.001 degK")).of(RubyUnits::Unit.new("0.5555 degK")) }
  end

  context "reported bugs" do
    specify { expect(RubyUnits::Unit.new("189 Mtonne") * RubyUnits::Unit.new("1189 g/tonne")).to eq(RubyUnits::Unit.new("224721 tonne")) }
    specify { expect((RubyUnits::Unit.new("189 Mtonne") * RubyUnits::Unit.new("1189 g/tonne")).convert_to("tonne")).to eq(RubyUnits::Unit.new("224721 tonne")) }
  end

  # When converting storage units it does not make sense to do the conversions
  # with integer math. There doesn't seem to be a use case for maintaining that
  # and it leads to subtle, unexpected bugs.
  # see #203
  context "when the unit scalar is an Integer" do
    it "the conversion is done accurately" do
      expect(RubyUnits::Unit.new("1610610000 bytes").convert_to("GiB").scalar).to eq(100_663_125/67108864r)
    end

    it "the converted unit has an Integer scalar if the initial unit has an Integer scalar and the scalar is equivalent to an integer" do
      expect(RubyUnits::Unit.new("2 m").convert_to("mm").scalar).to eq(2000)
    end

    it "the scalar becomes a Rational when necessary to preserve accuracy" do
      expect(RubyUnits::Unit.new(2, "m").convert_to("ft").scalar).to eq(2500/381r)
    end
  end

  context "when the unit scalar is a Float" do
    subject(:unit) { RubyUnits::Unit.new(2.0, "m") }

    # even though the result is numerically equivalent to an Integer, we leave
    # it alone
    it "preserves the scalar type" do
      expect(unit.convert_to("mm").scalar).to eq(2000.0)
    end
  end

  context "when the unit scalar is Complex" do
    subject(:unit) { RubyUnits::Unit.new(2.0 + 1.0i, "m") }

    it "preserves the scalar type" do
      expect(unit.convert_to("mm").scalar).to eq(2000.0 + 1000.0i)
    end
  end

  context "when the unit scalar is Rational" do
    subject(:unit) { RubyUnits::Unit.new(2r, "m") }

    it "preserves the scalar type" do
      expect(unit.convert_to("mm").scalar).to eq(2000r)
    end
  end

  describe "Foot-inch conversions" do
    [
      ["76 in", %(6'4")],
      ["-153/2 in", %(-6'4-1/2")],
      ["77 in", %(6'5")],
      ["78 in", %(6'6")],
      ["79 in", %(6'7")],
      ["80 in", %(6'8")],
      ["87 in", %(7'3")],
      ["88 in", %(7'4")],
      ["89 in", %(7'5")],
      ["66 in", %(5'6")],
      ["66in", %(5'6")]
    ].each do |inches, feet|
      specify { expect(RubyUnits::Unit.new(inches)).to eq(RubyUnits::Unit.new(feet)) }
      specify { expect(RubyUnits::Unit.new(inches).to_s(:ft)).to eq(feet) }
    end
  end

  describe "pound-ounce conversions" do
    [
      ["76 oz", "4 lbs 12 oz"],
      ["-76.5 oz", "-4 lbs 12-1/2 oz"],
      ["77 oz", "4 lbs 13 oz"],
      ["78 oz", "4 lbs 14 oz"],
      ["79 oz", "4 lbs 15 oz"],
      ["80 oz", "5 lbs 0 oz"],
      ["87 oz", "5 lbs 7 oz"],
      ["88 oz", "5 lbs 8 oz"],
      ["89 oz", "5 lbs 9 oz"]
    ].each do |ounces, pounds|
      specify { expect(RubyUnits::Unit.new(ounces).convert_to("lbs")).to eq(RubyUnits::Unit.new(pounds)) }
      specify { expect(RubyUnits::Unit.new(ounces).to_s(:lbs)).to eq(pounds) }
    end
  end

  describe "stone-pound conversions" do
    [
      ["14 stone 4", "200 lbs"],
      ["14 st 4", "200 lbs"],
      ["14 stone, 4 pounds", "200 lbs"],
      ["-14 stone, 4 pounds", "-200 lbs"],
      ["14 st, 4 lbs", "200 lbs"]
    ].each do |stone, pounds|
      specify { expect(RubyUnits::Unit.new(stone).convert_to("lbs")).to eq(RubyUnits::Unit.new(pounds)) }
    end
    it { expect(RubyUnits::Unit.new("200 lbs").to_s(:stone)).to eq "14 stone 4 lbs" }
    it { expect(RubyUnits::Unit.new("-200 lbs").to_s(:stone)).to eq "-14 stone 4 lbs" }
    it { expect(RubyUnits::Unit.new("-14 stone, 4 1/2 lbs")).to eq RubyUnits::Unit.new("-200.5 lbs") }
  end
end

describe "Unit Math" do
  context "operators:" do
    context "addition (+)" do
      context "between compatible units" do
        specify { expect(RubyUnits::Unit.new("0 m") + RubyUnits::Unit.new("10 m")).to eq(RubyUnits::Unit.new("10 m")) }
        specify { expect(RubyUnits::Unit.new("5 kg") + RubyUnits::Unit.new("10 kg")).to eq(RubyUnits::Unit.new("15 kg")) }
      end

      context "between a zero unit and another unit" do
        specify { expect(RubyUnits::Unit.new("0 kg") + RubyUnits::Unit.new("10 m")).to eq(RubyUnits::Unit.new("10 m")) }
        specify { expect(RubyUnits::Unit.new("0 m") + RubyUnits::Unit.new("10 kg")).to eq(RubyUnits::Unit.new("10 kg")) }
      end

      context "between incompatible units" do
        specify { expect { RubyUnits::Unit.new("10 kg") + RubyUnits::Unit.new("10 m") }.to raise_error(ArgumentError) }
        specify { expect { RubyUnits::Unit.new("10 m") + RubyUnits::Unit.new("10 kg") }.to raise_error(ArgumentError) }
        specify { expect { RubyUnits::Unit.new("10 m") + nil }.to raise_error(ArgumentError) }
      end

      context "a number from a unit" do
        specify { expect { RubyUnits::Unit.new("10 kg") + 1 }.to raise_error(ArgumentError) }
        specify { expect { 10 + RubyUnits::Unit.new("10 kg") }.to raise_error(ArgumentError) }
      end

      context "between a unit and coerceable types" do
        specify { expect(RubyUnits::Unit.new("10 kg") + %w[1 kg]).to eq(RubyUnits::Unit.new("11 kg")) }
        specify { expect(RubyUnits::Unit.new("10 kg") + "1 kg").to eq(RubyUnits::Unit.new("11 kg")) }
      end

      context "between two temperatures" do
        specify { expect { (RubyUnits::Unit.new("100 tempK") + RubyUnits::Unit.new("100 tempK")) }.to raise_error(ArgumentError, "Cannot add two temperatures") }
      end

      context "between a temperature and a degree" do
        specify { expect(RubyUnits::Unit.new("100 tempK") + RubyUnits::Unit.new("100 degK")).to eq(RubyUnits::Unit.new("200 tempK")) }
      end

      context "between a degree and a temperature" do
        specify { expect(RubyUnits::Unit.new("100 degK") + RubyUnits::Unit.new("100 tempK")).to eq(RubyUnits::Unit.new("200 tempK")) }
      end

      context "between two unitless units" do
        specify { expect(RubyUnits::Unit.new("1") + RubyUnits::Unit.new("2")).to eq 3 }
      end
    end

    context "subtracting (-)" do
      context "compatible units" do
        specify { expect(RubyUnits::Unit.new("0 m") - RubyUnits::Unit.new("10 m")).to eq(RubyUnits::Unit.new("-10 m")) }
        specify { expect(RubyUnits::Unit.new("5 kg") - RubyUnits::Unit.new("10 kg")).to eq(RubyUnits::Unit.new("-5 kg")) }
      end

      context "a unit from a zero unit" do
        specify { expect(RubyUnits::Unit.new("0 kg") - RubyUnits::Unit.new("10 m")).to eq(RubyUnits::Unit.new("-10 m")) }
        specify { expect(RubyUnits::Unit.new("0 m") - RubyUnits::Unit.new("10 kg")).to eq(RubyUnits::Unit.new("-10 kg")) }
      end

      context "incompatible units" do
        specify { expect { RubyUnits::Unit.new("10 kg") - RubyUnits::Unit.new("10 m") }.to raise_error(ArgumentError) }
        specify { expect { RubyUnits::Unit.new("10 m") - RubyUnits::Unit.new("10 kg") }.to raise_error(ArgumentError) }
        specify { expect { RubyUnits::Unit.new("10 m") - nil }.to raise_error(ArgumentError) }
      end

      context "between a unit and coerceable types" do
        specify { expect(RubyUnits::Unit.new("10 kg") - %w[1 kg]).to eq(RubyUnits::Unit.new("9 kg")) }
        specify { expect(RubyUnits::Unit.new("10 kg") - "1 kg").to eq(RubyUnits::Unit.new("9 kg")) }
      end

      context "a number from a unit" do
        specify { expect { RubyUnits::Unit.new("10 kg") - 1 }.to raise_error(ArgumentError) }
        specify { expect { 10 - RubyUnits::Unit.new("10 kg") }.to raise_error(ArgumentError) }
      end

      context "between two temperatures" do
        specify { expect(RubyUnits::Unit.new("100 tempK") - RubyUnits::Unit.new("100 tempK")).to eq(RubyUnits::Unit.new("0 degK")) }
      end

      context "between a temperature and a degree" do
        specify { expect(RubyUnits::Unit.new("100 tempK") - RubyUnits::Unit.new("100 degK")).to eq(RubyUnits::Unit.new("0 tempK")) }
      end

      context "between a degree and a temperature" do
        specify { expect { (RubyUnits::Unit.new("100 degK") - RubyUnits::Unit.new("100 tempK")) }.to raise_error(ArgumentError, "Cannot subtract a temperature from a differential degree unit") }
      end

      context "a unitless unit from another unitless unit" do
        specify { expect(RubyUnits::Unit.new("1") - RubyUnits::Unit.new("2")).to eq(-1) }
      end
    end

    context "multiplying (*)" do
      context "between compatible units" do
        specify { expect(RubyUnits::Unit.new("0 m") * RubyUnits::Unit.new("10 m")).to eq(RubyUnits::Unit.new("0 m^2")) }
        specify { expect(RubyUnits::Unit.new("5 kg") * RubyUnits::Unit.new("10 kg")).to eq(RubyUnits::Unit.new("50 kg^2")) }
      end

      context "between incompatible units" do
        specify { expect(RubyUnits::Unit.new("0 m") * RubyUnits::Unit.new("10 kg")).to eq(RubyUnits::Unit.new("0 kg*m")) }
        specify { expect(RubyUnits::Unit.new("5 m") * RubyUnits::Unit.new("10 kg")).to eq(RubyUnits::Unit.new("50 kg*m")) }
        specify { expect { RubyUnits::Unit.new("10 m") * nil }.to raise_error(ArgumentError) }
      end

      context "between a unit and coerceable types" do
        specify { expect(RubyUnits::Unit.new("10 kg") * %w[1 kg]).to eq(RubyUnits::Unit.new("10 kg^2")) }
        specify { expect(RubyUnits::Unit.new("10 kg") * "1 kg").to eq(RubyUnits::Unit.new("10 kg^2")) }
      end

      context "by a temperature" do
        specify { expect { RubyUnits::Unit.new("5 kg") * RubyUnits::Unit.new("100 tempF") }.to raise_exception(ArgumentError) }
      end

      context "by a number" do
        specify { expect(10 * RubyUnits::Unit.new("5 kg")).to eq(RubyUnits::Unit.new("50 kg")) }
      end
    end

    context "dividing (/)" do
      context "compatible units" do
        specify { expect(RubyUnits::Unit.new("0 m") / RubyUnits::Unit.new("10 m")).to eq(RubyUnits::Unit.new(0)) }
        specify { expect(RubyUnits::Unit.new("5 kg") / RubyUnits::Unit.new("10 kg")).to eq(Rational(1, 2)) }
        specify { expect(RubyUnits::Unit.new("5 kg") / RubyUnits::Unit.new("5 kg")).to eq(1) }
      end

      context "incompatible units" do
        specify { expect(RubyUnits::Unit.new("0 m") / RubyUnits::Unit.new("10 kg")).to eq(RubyUnits::Unit.new("0 m/kg")) }
        specify { expect(RubyUnits::Unit.new("5 m") / RubyUnits::Unit.new("10 kg")).to eq(RubyUnits::Unit.new("1/2 m/kg")) }
        specify { expect { RubyUnits::Unit.new("10 m") / nil }.to raise_error(ArgumentError) }
      end

      context "between a unit and coerceable types" do
        specify { expect(RubyUnits::Unit.new("10 kg^2") / %w[1 kg]).to eq(RubyUnits::Unit.new("10 kg")) }
        specify { expect(RubyUnits::Unit.new("10 kg^2") / "1 kg").to eq(RubyUnits::Unit.new("10 kg")) }
      end

      context "by a temperature" do
        specify { expect { RubyUnits::Unit.new("5 kg") / RubyUnits::Unit.new("100 tempF") }.to raise_exception(ArgumentError) }
      end

      context "a number by a unit" do
        specify { expect(10 / RubyUnits::Unit.new("5 kg")).to eq(RubyUnits::Unit.new("2 1/kg")) }
      end

      context "a unit by a number" do
        specify { expect(RubyUnits::Unit.new("5 kg") / 2).to eq(RubyUnits::Unit.new("2.5 kg")) }
      end

      context "by zero" do
        specify { expect { RubyUnits::Unit.new("10 m") / 0 }.to raise_error(ZeroDivisionError) }
        specify { expect { RubyUnits::Unit.new("10 m") / RubyUnits::Unit.new("0 m") }.to raise_error(ZeroDivisionError) }
        specify { expect { RubyUnits::Unit.new("10 m") / RubyUnits::Unit.new("0 kg") }.to raise_error(ZeroDivisionError) }
      end
    end

    context "exponentiating (**)" do
      specify "a temperature raises an exception" do
        expect { RubyUnits::Unit.new("100 tempK")**2 }.to raise_error(ArgumentError, "Cannot raise a temperature to a power")
      end

      context RubyUnits::Unit.new("0 m") do
        it { expect(subject**1).to eq(subject) } # rubocop:disable Lint/UselessNumericOperation
        it { expect(subject**2).to eq(subject) }
      end

      context RubyUnits::Unit.new("1 m") do
        it { expect(subject**0).to eq(1) } # rubocop:disable Lint/NumericOperationWithConstantResult
        it { expect(subject**1).to eq(subject) } # rubocop:disable Lint/UselessNumericOperation
        it { expect(subject**-1).to eq(1 / subject) }
        it { expect(subject**2).to eq(RubyUnits::Unit.new("1 m^2")) }
        it { expect(subject**-2).to eq(RubyUnits::Unit.new("1 1/m^2")) }
        specify { expect { subject**Rational(1, 2) }.to raise_error(ArgumentError, "Illegal root") }
        # because 1 m^(1/2) doesn't make any sense
        specify { expect { subject**Complex(1, 1) }.to raise_error(ArgumentError, "exponentiation of complex numbers is not supported.") }
        specify { expect { subject**RubyUnits::Unit.new("1 m") }.to raise_error(ArgumentError, "Invalid Exponent") }
      end

      context RubyUnits::Unit.new("1 m^2") do
        it { expect(subject**Rational(1, 2)).to eq(RubyUnits::Unit.new("1 m")) }
        it { expect(subject**0.5).to eq(RubyUnits::Unit.new("1 m")) }

        specify { expect { subject**0.12345 }.to raise_error(ArgumentError, "Not a n-th root (1..9), use 1/n") }
        specify { expect { subject**"abcdefg" }.to raise_error(ArgumentError, "Invalid Exponent") }
      end
    end

    describe "modulo (%)" do
      it { expect(RubyUnits::Unit.new("2 m") % RubyUnits::Unit.new("1 m")).to eq(0) }
      it { expect(RubyUnits::Unit.new("2 m") % RubyUnits::Unit.new("1 m")).to eq(RubyUnits::Unit.new("0 m")) }
      it { expect(RubyUnits::Unit.new("5 m") % RubyUnits::Unit.new("2 m")).to eq(RubyUnits::Unit.new("1 m")) }
      it { expect(RubyUnits::Unit.new("5 m") % RubyUnits::Unit.new("-2 m")).to eq(RubyUnits::Unit.new("-1 m")) }
      it { expect(RubyUnits::Unit.new(127) % 2).to eq(1) }
      it { expect(RubyUnits::Unit.new(127) % -2).to eq(-1) }

      it "raises and exception with incompatible units" do
        expect { RubyUnits::Unit.new("1 m") % RubyUnits::Unit.new("1 kg") }.to raise_error(ArgumentError, "Incompatible Units ('1 m' not compatible with '1 kg')")
      end
    end

    context "unary negation (-)" do
      specify { expect(-RubyUnits::Unit.new("1 mm")).to eq(RubyUnits::Unit.new("-1 mm")) }
    end

    context "unary plus (+)" do
      specify { expect(+RubyUnits::Unit.new("1 mm")).to eq(RubyUnits::Unit.new("1 mm")) }
    end
  end

  describe "#power" do
    subject { RubyUnits::Unit.new("1 m") }

    it "raises an exception when passed a Float argument" do
      expect { subject.power(1.5) }.to raise_error(ArgumentError, "Exponent must an Integer")
    end

    it "raises an exception when passed a Rational argument" do
      expect { subject.power(Rational(1, 2)) }.to raise_error(ArgumentError, "Exponent must an Integer")
    end

    it "raises an exception when passed a Complex argument" do
      expect { subject.power(Complex(1, 2)) }.to raise_error(ArgumentError, "Exponent must an Integer")
    end

    it "raises an exception when called on a temperature unit" do
      expect { RubyUnits::Unit.new("100 tempC").power(2) }.to raise_error(ArgumentError, "Cannot raise a temperature to a power")
    end

    specify { expect(subject.power(-1)).to eq(RubyUnits::Unit.new("1 1/m")) }
    specify { expect(subject.power(0)).to eq(1) }
    specify { expect(subject.power(1)).to eq(subject) }
    specify { expect(subject.power(2)).to eq(RubyUnits::Unit.new("1 m^2")) }
  end

  describe "#root" do
    subject { RubyUnits::Unit.new("1 m") }

    it "raises an exception when passed a Float argument" do
      expect { subject.root(1.5) }.to raise_error(ArgumentError, "Exponent must an Integer")
    end

    it "raises an exception when passed a Rational argument" do
      expect { subject.root(Rational(1, 2)) }.to raise_error(ArgumentError, "Exponent must an Integer")
    end

    it "raises an exception when passed a Complex argument" do
      expect { subject.root(Complex(1, 2)) }.to raise_error(ArgumentError, "Exponent must an Integer")
    end

    it "raises an exception when called on a temperature unit" do
      expect { RubyUnits::Unit.new("100 tempC").root(2) }.to raise_error(ArgumentError, "Cannot take the root of a temperature")
    end

    specify { expect(RubyUnits::Unit.new("1 m^2").root(-2)).to eq(RubyUnits::Unit.new("1 1/m")) }
    specify { expect(subject.root(-1)).to eq(RubyUnits::Unit.new("1 1/m")) }
    specify { expect { subject.root(0) }.to raise_error(ArgumentError, "0th root undefined") }
    specify { expect(subject.root(1)).to eq(subject) }
    specify { expect(RubyUnits::Unit.new("1 m^2").root(2)).to eq(RubyUnits::Unit.new("1 m")) }
  end

  describe "#inverse" do
    specify { expect(RubyUnits::Unit.new("1 m").inverse).to eq(RubyUnits::Unit.new("1 1/m")) }
    specify { expect { RubyUnits::Unit.new("100 tempK").inverse }.to raise_error(ArgumentError, "Cannot divide with temperatures") }
  end

  context "when converting to scalars" do
    it { expect(RubyUnits::Unit.new("10").to_i).to be_a(Integer) }
    it { expect { RubyUnits::Unit.new("10 m").to_i }.to raise_error(RuntimeError, "Cannot convert '10 m' to Integer unless unitless.  Use Unit#scalar") }

    it { expect(RubyUnits::Unit.new("10.0").to_f).to be_a(Float) }
    it { expect { RubyUnits::Unit.new("10.0 m").to_f }.to raise_error(RuntimeError, "Cannot convert '10 m' to Float unless unitless.  Use Unit#scalar") }

    it { expect(RubyUnits::Unit.new("1+1i").to_c).to be_a(Complex) }
    it { expect { RubyUnits::Unit.new("1+1i m").to_c }.to raise_error(RuntimeError, "Cannot convert '1+1i m' to Complex unless unitless.  Use Unit#scalar") }

    it { expect(RubyUnits::Unit.new("3/7").to_r).to be_a(Rational) }
    it { expect { RubyUnits::Unit.new("3/7 m").to_r }.to raise_error(RuntimeError, "Cannot convert '3/7 m' to Rational unless unitless.  Use Unit#scalar") }
  end

  context "absolute value (#abs)" do
    context "of a unitless unit" do
      specify "returns the absolute value of the scalar" do
        expect(RubyUnits::Unit.new("-10").abs).to eq(10)
      end
    end

    context "of a unit" do
      specify "returns a unit with the absolute value of the scalar" do
        expect(RubyUnits::Unit.new("-10 m").abs).to eq(RubyUnits::Unit.new("10 m"))
      end
    end
  end

  describe "#ceil" do
    context "of a unitless unit" do
      it "returns the ceil of the scalar" do
        expect(RubyUnits::Unit.new("10.1").ceil).to eq(11)
      end
    end

    context "of a unit" do
      subject(:unit) { RubyUnits::Unit.new("1.2345 m") }

      it "returns a unit with the ceil of the scalar" do
        expect(unit.ceil).to eq(RubyUnits::Unit.new("2 m"))
      end

      it "forwards arguments to the scalar" do
        expect(unit.ceil(2)).to eq(RubyUnits::Unit.new("1.24 m"))
      end
    end
  end

  describe "#floor" do
    context "of a unitless unit" do
      specify "returns the floor of the scalar" do
        expect(RubyUnits::Unit.new("10.1").floor).to eq(10)
      end
    end

    context "of a unit" do
      subject(:unit) { RubyUnits::Unit.new("1.2345 m") }

      it "returns a unit with the floor of the scalar" do
        expect(unit.floor).to eq(RubyUnits::Unit.new("1 m"))
      end

      it "forwards arguments to the scalar" do
        expect(unit.floor(2)).to eq(RubyUnits::Unit.new("1.23 m"))
      end
    end
  end

  describe "#round" do
    context "of a unitless unit" do
      specify "returns the round of the scalar" do
        expect(RubyUnits::Unit.new("10.5").round).to eq(11)
      end
    end

    context "of a unit" do
      subject(:unit) { RubyUnits::Unit.new("1.2345 m") }

      it "returns a unit with the round of the scalar" do
        expect(unit.round).to eq(RubyUnits::Unit.new("1 m"))
      end

      it "forwards arguments to the scalar" do
        expect(unit.round(3, half: :down)).to eq(RubyUnits::Unit.new("1.234 m"))
      end
    end

    context "with a unit containing a centi-prefix" do
      subject(:unit) { RubyUnits::Unit.new("1.2345 cm^2") }

      it "rounds correctly for squared unit" do
        expect(unit.round).to eq(RubyUnits::Unit.new("1 cm^2"))
      end
    end
  end

  describe "#truncate" do
    context "of a unitless unit" do
      specify "returns the truncate of the scalar" do
        expect(RubyUnits::Unit.new("10.5").truncate).to eq(10)
      end
    end

    context "of a unit" do
      subject(:unit) { RubyUnits::Unit.new("1.2345 m") }

      it "returns a unit with the truncate of the scalar" do
        expect(unit.truncate).to eq(RubyUnits::Unit.new("1 m"))
      end

      it "forwards arguments to the scalar" do
        expect(unit.truncate(2)).to eq(RubyUnits::Unit.new("1.23 m"))
      end
    end

    context "of a complex unit" do
      specify "returns a unit with the truncate of the scalar" do
        expect(RubyUnits::Unit.new("10.5 kg*m/s^3").truncate).to eq(RubyUnits::Unit.new("10 kg*m/s^3"))
      end
    end
  end

  describe "#zero?" do
    it "is true when the scalar is zero on the base scale" do
      expect(RubyUnits::Unit.new("0")).to be_zero
      expect(RubyUnits::Unit.new("0 mm")).to be_zero
      expect(RubyUnits::Unit.new("-273.15 tempC")).to be_zero
    end

    it "is false when the scalar is not zero" do
      expect(RubyUnits::Unit.new("1")).not_to be_zero
      expect(RubyUnits::Unit.new("1 mm")).not_to be_zero
      expect(RubyUnits::Unit.new("0 tempC")).not_to be_zero
    end
  end

  describe "#succ" do
    specify { expect(RubyUnits::Unit.new("1").succ).to eq(RubyUnits::Unit.new("2")) }
    specify { expect(RubyUnits::Unit.new("1 mm").succ).to eq(RubyUnits::Unit.new("2 mm")) }
    specify { expect(RubyUnits::Unit.new("1 mm").next).to eq(RubyUnits::Unit.new("2 mm")) }
    specify { expect(RubyUnits::Unit.new("-1 mm").succ).to eq(RubyUnits::Unit.new("0 mm")) }
    specify { expect { RubyUnits::Unit.new("1.5 mm").succ }.to raise_error(ArgumentError, "Non Integer Scalar") }
  end

  describe "#pred" do
    specify { expect(RubyUnits::Unit.new("1").pred).to eq(RubyUnits::Unit.new("0")) }
    specify { expect(RubyUnits::Unit.new("1 mm").pred).to eq(RubyUnits::Unit.new("0 mm")) }
    specify { expect(RubyUnits::Unit.new("-1 mm").pred).to eq(RubyUnits::Unit.new("-2 mm")) }
    specify { expect { RubyUnits::Unit.new("1.5 mm").pred }.to raise_error(ArgumentError, "Non Integer Scalar") }
  end

  describe "#remainder" do
    it { expect { RubyUnits::Unit.new("5 mm").remainder(2) }.to raise_error(ArgumentError, "Incompatible Units ('5 mm' not compatible with '2')") }
    it { expect { RubyUnits::Unit.new("5 mm").remainder(RubyUnits::Unit.new("2 kg")) }.to raise_error(ArgumentError, "Incompatible Units ('5 mm' not compatible with '2 kg')") }
    it { expect(RubyUnits::Unit.new("5 mm").remainder(RubyUnits::Unit.new("2 mm"))).to eq(RubyUnits::Unit.new("1 mm")) }
    it { expect(RubyUnits::Unit.new("5 mm").remainder(RubyUnits::Unit.new("-2 mm"))).to eq(RubyUnits::Unit.new("1 mm")) }
    it { expect(RubyUnits::Unit.new("5 cm").remainder(RubyUnits::Unit.new("1 in"))).to eq(RubyUnits::Unit.new("2.46 cm")) }
    it { expect(RubyUnits::Unit.new(127).remainder(2)).to eq(1) }
    it { expect(RubyUnits::Unit.new(127).remainder(-2)).to eq(1) }
  end

  describe "#divmod" do
    it { expect(RubyUnits::Unit.new("5 mm").divmod(RubyUnits::Unit.new("2 mm"))).to eq([2, RubyUnits::Unit.new("1 mm")]) }
    it { expect(RubyUnits::Unit.new("5 mm").divmod(RubyUnits::Unit.new("-2 mm"))).to eq([-3, RubyUnits::Unit.new("-1 mm")]) }
    it { expect(RubyUnits::Unit.new("1 km").divmod(RubyUnits::Unit.new("2 m"))).to eq([500, RubyUnits::Unit.new("0 mm")]) }
    it { expect { RubyUnits::Unit.new("1 m").divmod(RubyUnits::Unit.new("2 kg")) }.to raise_error(ArgumentError, "Incompatible Units ('1 m' not compatible with '2 kg')") }
  end

  describe "#quo" do
    it { expect(RubyUnits::Unit.new("5 mm").quo(RubyUnits::Unit.new("2 mm"))).to eq(2.5) }
    it { expect(RubyUnits::Unit.new("1 km").quo(RubyUnits::Unit.new("2 s"))).to eq(RubyUnits::Unit.new("1/2 km/s")) }
    it { expect { RubyUnits::Unit.new("1 km").quo(RubyUnits::Unit.new("0 s")) }.to raise_error(ZeroDivisionError) }
  end

  describe "#div" do
    specify { expect(RubyUnits::Unit.new("23 m").div(RubyUnits::Unit.new("2 m"))).to eq(11) }
  end

  describe "#best_prefix" do
    it { expect(RubyUnits::Unit.new("1024 KiB").best_prefix).to have_attributes(scalar: 1, units: "MiB") }
    it { expect(RubyUnits::Unit.new("1000 m").best_prefix).to have_attributes(scalar: 1, units: "km") }
    it { expect(RubyUnits::Unit.new("1/1000 m").best_prefix).to have_attributes(scalar: 1.0, units: "mm") }
    it { expect(RubyUnits::Unit.new("1/100000 m").best_prefix).to have_attributes(scalar: 10, units: "um") }
    it { expect(RubyUnits::Unit.new("0 m").best_prefix).to have_attributes(scalar: 0, units: "m") }
    it { expect(RubyUnits::Unit.new("1000 kg").best_prefix).to have_attributes(scalar: 1000, units: "kg") }
    it { expect(RubyUnits::Unit.new("1_000_000 kg").best_prefix).to have_attributes(scalar: 1_000_000, units: "kg") }
    it { expect(RubyUnits::Unit.new("1/100 m").best_prefix).to have_attributes(scalar: 1, units: "cm") }
    it { expect(RubyUnits::Unit.new("1/10 m").best_prefix).to have_attributes(scalar: 10, units: "cm") }
    it { expect(RubyUnits::Unit.new("0.234 s").best_prefix).to have_attributes(scalar: 234, units: "ms") }
  end

  context "Time helper functions" do
    before do
      allow(Time).to receive(:now).and_return(Time.utc(2011, 10, 16))
      allow(DateTime).to receive(:now).and_return(DateTime.civil(2011, 10, 16))
      allow(Date).to receive(:today).and_return(Date.civil(2011, 10, 16))
    end

    describe "#since" do
      specify { expect(RubyUnits::Unit.new("min").since(Time.utc(2001, 4, 1, 0, 0, 0))).to eq(RubyUnits::Unit.new("5544000 min")) }
      specify { expect(RubyUnits::Unit.new("min").since(DateTime.civil(2001, 4, 1, 0, 0, 0))).to eq(RubyUnits::Unit.new("5544000 min")) }
      specify { expect(RubyUnits::Unit.new("min").since(Date.civil(2001, 4, 1))).to eq(RubyUnits::Unit.new("5544000 min")) }
      specify { expect { RubyUnits::Unit.new("min").since("4-1-2001") }.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime") }
      specify { expect { RubyUnits::Unit.new("min").since(nil) }.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime") }
    end

    describe "#before" do
      specify { expect(RubyUnits::Unit.new("5 min").before(Time.now)).to eq(Time.utc(2011, 10, 15, 23, 55)) }
      specify { expect(RubyUnits::Unit.new("5 min").before(DateTime.now)).to eq(DateTime.civil(2011, 10, 15, 23, 55)) }
      specify { expect(RubyUnits::Unit.new("5 min").before(Date.today)).to eq(DateTime.civil(2011, 10, 15, 23, 55)) }
      specify { expect { RubyUnits::Unit.new("5 min").before(nil) }.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime") }
      specify { expect { RubyUnits::Unit.new("5 min").before("12:00") }.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime") }
    end

    describe "#ago" do
      specify { expect(RubyUnits::Unit.new("5 min").ago).to be_a Time }
      specify { expect(RubyUnits::Unit.new("10000 y").ago).to be_a Time }
      specify { expect(RubyUnits::Unit.new("1 year").ago).to eq(Time.utc(2010, 10, 16)) }
    end

    describe "#until" do
      specify { expect(RubyUnits::Unit.new("min").until(Date.civil(2011, 10, 17))).to eq(RubyUnits::Unit.new("1440 min")) }
      specify { expect(RubyUnits::Unit.new("min").until(DateTime.civil(2011, 10, 21))).to eq(RubyUnits::Unit.new("7200 min")) }
      specify { expect(RubyUnits::Unit.new("min").until(Time.utc(2011, 10, 21))).to eq(RubyUnits::Unit.new("7200 min")) }
      specify { expect { RubyUnits::Unit.new("5 min").until(nil) }.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime") }
      specify { expect { RubyUnits::Unit.new("5 min").until("12:00") }.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime") }
    end

    describe "#from" do
      specify { expect(RubyUnits::Unit.new("1 day").from(Date.civil(2011, 10, 17))).to eq(Date.civil(2011, 10, 18)) }
      specify { expect(RubyUnits::Unit.new("5 min").from(DateTime.civil(2011, 10, 21))).to eq(DateTime.civil(2011, 10, 21, 0, 5)) }
      specify { expect(RubyUnits::Unit.new("5 min").from(Time.utc(2011, 10, 21))).to eq(Time.utc(2011, 10, 21, 0, 5)) }
      specify { expect { RubyUnits::Unit.new("5 min").from(nil) }.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime") }
      specify { expect { RubyUnits::Unit.new("5 min").from("12:00") }.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime") }
    end
  end
end

describe "#to_s (Unit Output formatting)" do
  describe "10.5 m/s^2" do
    subject(:unit) { RubyUnits::Unit.new("10.5 m/s^2") }

    it { expect(unit.to_s).to eq("10.5 m/s^2") }
    it { expect(unit.to_s(format: :exponential)).to eq("10.5 m*s^-2") }
    it { expect(unit.to_s("%0.2f")).to eq("10.50 m/s^2") }
    it { expect(unit.to_s("%0.2e km/s^2")).to eq("1.05e-02 km/s^2") }
    it { expect(unit.to_s("km/s^2")).to eq("0.0105 km/s^2") }
    it { expect(unit.to_s(STDOUT)).to eq("10.5 m/s^2") }
    it { expect { unit.to_s("random string") }.to raise_error(ArgumentError, "'random' Unit not recognized") }
  end

  context "for a unit with a custom display_name" do
    subject { RubyUnits::Unit.new("8 cups") }

    around do |example|
      RubyUnits::Unit.redefine!("cup") do |cup|
        cup.display_name = "cupz"
      end
      example.run
      RubyUnits::Unit.redefine!("cup") do |cup|
        cup.display_name = cup.aliases.first
      end
    end

    it { is_expected.to have_attributes(to_s: "8 cupz") }
  end
end

describe "Equations with Units" do
  context "Ideal Gas Law" do
    let(:p) { RubyUnits::Unit.new("100 kPa") }
    let(:v) { RubyUnits::Unit.new("1 m^3") }
    let(:n) { RubyUnits::Unit.new("1 mole") }
    let(:r) { RubyUnits::Unit.new("8.31451 J/mol*degK") }

    specify { expect(((p * v) / (n * r)).convert_to("tempK")).to be_within(RubyUnits::Unit.new("0.1 degK")).of(RubyUnits::Unit.new("12027.2 tempK")) }
  end
end

describe "Unit hash method" do
  context "should return equal values for identical units" do
    let(:kg_unit_1) { RubyUnits::Unit.new("2.2 kg") }
    let(:kg_unit_2) { RubyUnits::Unit.new("2.2 kg") }

    specify { expect(kg_unit_1).to eq(kg_unit_2) }
    specify { expect(kg_unit_1.hash).to eq(kg_unit_2.hash) }
    specify { expect([kg_unit_1, kg_unit_2].uniq.size).to eq(1) }
  end

  context "should return not equal values for differnet units" do
    let(:kg_unit) { RubyUnits::Unit.new("2.2 kg") }
    let(:lb_unit) { RubyUnits::Unit.new("2.2 lbs") }

    specify { expect(kg_unit).not_to eq(lb_unit) }
    specify { expect(kg_unit.hash).not_to eq(lb_unit.hash) }
    specify { expect([kg_unit, lb_unit].uniq.size).to eq(2) }
  end
end
