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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
This was a refactor of the following code: