Skip to content

Instantly share code, notes, and snippets.

@soulcutter
Last active November 16, 2020 19:53
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save soulcutter/9dcd3aa75274253df3a77f88eb0d6fc8 to your computer and use it in GitHub Desktop.
Save soulcutter/9dcd3aa75274253df3a77f88eb0d6fc8 to your computer and use it in GitHub Desktop.
Threshold
module Threshold
# In order to convert a String or Numeric to a Threshold this module can be used as a refinement:
# `using Threshold::Conversion` will allow you to access the conversion method `Threshold(obj)`
# e.g. Threshold("123ms") or Threshold("12%") or Threshold(1_000)
module Conversion
refine Kernel do
def Threshold(obj)
case obj
when AbstractThreshold then obj
when Numeric then NumericThreshold.new(obj)
when /\A(?<scalar>-?\d+(\.\d+)?)\z/ # a number "12.3"
NumericThreshold.new($~["scalar"])
when /\A(?<scalar>-?\d+)(?<unit>m?s)\z/ # a whole number time period "123s" or "123ms"
TimeThreshold.new($~["scalar"], $~["unit"])
when /\A(?<scalar>\d+(\.\d+)?)(?<unit>%)\z/ # a percentage "12.3%"
PercentThreshold.new($~["scalar"])
else
raise ArgumentError.new("#{obj.inspect} not convertible to a Threshold")
end
end
end
end
end
module Threshold
class AbstractThreshold
include Comparable
using Conversion
attr_reader :scalar, :unit
# Intended for use by subclasses, not intended to be used directly
# To instantiate, see subclasses or Threshold::Conversion
def initialize(scalar, unit)
@raw_scalar = scalar
@scalar = BigDecimal(scalar)
@unit = unit
freeze
end
def <=>(other)
other = Threshold(other)
# types must be an exact match for comparison to avoid things like
# "123s" and "123" counting as ==
return nil if other.class != self.class
self.to_f <=> other.to_f
rescue ArgumentError
nil # when `other` is not convertible via Threshold(other)
end
def to_f
scalar.to_f
end
def to_s
"#{@raw_scalar}#{unit}"
end
end
end
module Threshold
class NumericThreshold < AbstractThreshold
NO_UNIT = Object.new.tap do |obj|
class << obj
def inspect
"NumericThreshold::NO_UNIT"
end
def to_s
""
end
end
end
def initialize(scalar)
super(scalar, NO_UNIT)
end
end
end
module Threshold
class PercentThreshold < AbstractThreshold
def initialize(scalar)
super(scalar, "%")
end
def to_f
scalar / 100.0
end
end
end
module Threshold
class TimeThreshold < AbstractThreshold
UNITS = %w(s ms)
def initialize(scalar, unit)
raise ArgumentError.new("Unrecognized unit #{unit.inspect}") unless UNITS.include?(unit)
super
end
def to_f
return scalar / 1000.0 if unit == "ms"
scalar.to_f
end
end
end
require 'spec_helper'
RSpec.describe Threshold do
using Threshold::Conversion
it "separates a string into scalar value and unit" do
threshold = Threshold("100ms")
expect(threshold.scalar).to eq 100
expect(threshold.unit).to eq "ms"
end
it "implements to_f for percentages" do
threshold = Threshold("95.2%")
expect(threshold.to_f).to eq(0.952)
end
it "implements to_f for milliseconds" do
threshold = Threshold("950ms")
expect(threshold.to_f).to eq(0.95)
end
it "implements to_f for seconds" do
threshold = Threshold("950s")
expect(threshold.to_f).to eq(950.0)
end
it "implements to_f for numeric thresholds" do
threshold = Threshold(950)
expect(threshold.to_f).to eq 950.0
end
it "implements to_f for string-numeric thresholds", :aggregate_failures do
expect(Threshold("950").to_f).to eq 950.0
expect(Threshold("-950").to_f).to eq -950.0
expect(Threshold("950.2").to_f).to eq 950.2
end
it "implements to_s for thresholds", :aggregate_failures do
expect(Threshold("150ms").to_s).to eq "150ms"
expect(Threshold("150s").to_s).to eq "150s"
expect(Threshold("150").to_s).to eq "150"
expect(Threshold(150).to_s).to eq "150"
expect(Threshold("150%").to_s).to eq "150%"
end
it "comparing incompatible types is well-behaved", :aggregate_failures do
time_period = Threshold("123s")
numeric = Threshold("123")
expect(time_period).not_to eq numeric
expect(numeric).not_to eq time_period
expect { numeric > time_period }.to raise_error(ArgumentError)
expect { numeric < time_period }.to raise_error(ArgumentError)
end
it "is comparable between different time units", :aggregate_failures do
expect(Threshold("1s")).to eq Threshold("1000ms")
expect(Threshold("1s")).to be > Threshold("1ms")
expect(Threshold("1ms")).to be < Threshold("1s")
end
end
@soulcutter
Copy link
Author

This was a refactor of the following code:

      def threshold_to_float(num)
        if num.is_a?(Numeric)
          num
        elsif num.end_with?('ms')
          BigDecimal(num.sub('ms', '')) / 1000.0
        elsif num.end_with?('s')
          BigDecimal(num.sub('s', ''))
        elsif num.end_with?('%')
          BigDecimal(num.sub('%', '')) / 100.0
        else
          raise ArgumentError, "Unable to convert #{num} to a numeric"
        end
      end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment