Skip to content

Instantly share code, notes, and snippets.

@mklbtz
Last active May 1, 2017 15:37
Show Gist options
  • Save mklbtz/553ec615b87a61007c0612d936cc2425 to your computer and use it in GitHub Desktop.
Save mklbtz/553ec615b87a61007c0612d936cc2425 to your computer and use it in GitHub Desktop.
Porting Apple's OptionSets to Ruby
require 'active_support'
module OptionSet
extend ActiveSupport::Concern
included do
attr_reader :value
include Comparable
include Enumerable
def initialize(value)
@value = value
end
delegate :<=>, to: :value
delegate :option_hash, to: :class
def include?(other)
(self & other) == other
end
def union(other)
self.class.new(value | other.value)
end
def union!(other)
@value = value | other.value
self
end
alias_method :+, :union
alias_method :|, :union
def intersect(other)
self.class.new(value & other.value)
end
def intersect!(other)
@value = value & other.value
self
end
alias_method :*, :intersect
alias_method :&, :intersect
def exclude(other)
self.class.new(value ^ other.value)
end
def exclude!(other)
@value = value ^ other.value
self
end
alias_method :-, :exclude
alias_method :'^', :exclude
# We mask the inverted value so that we don't
# flip flags that don't represent an option.
def invert
self.class.new(~value | all_mask)
end
def invert!
@value = ~value | all_mask
self
end
alias_method :~, :invert
alias_method :not, :invert
alias_method :not!, :invert!
# Yields new instances of the class
# each initialized with a component value,
# effectively iterating the set of active options.
def each
option_count.times.map do |i|
component = value & (1 << i)
next if component == 0
yield self.class.new(component)
end
end
# The value of self.class.all
def all_mask
~(~0 << self.option_count)
end
end
class_methods do
def [] *options
valid = options.select do |opt|
opt.is_a?(Symbol) || opt.is_a?(String) || opt.is_a?(Numeric)
end
numbers, words = valid.partition do |opt|
opt.is_a?(Numeric)
end
if words.present?
word_vals = option_hash.values_at(*words.map(&:to_sym))
end
if numbers.present?
number_vals = option_hash.values.select { |val| numbers.include?(val) }
end
values = [word_vals, number_vals].select(&:present?).flatten(1)
values.map(&method(:new)).reduce(:+)
end
private
def option_set(options)
set = options.map(&:to_sym).uniq
values = Array.new(set.count) { |i| 1 << i }.freeze
option_hash = set.zip(values).to_h.freeze
option_hash.each do |option, value|
define_singleton_method(option) { new(value) }
end
define_singleton_method(:option_hash) { option_hash }
define_singleton_method(:option_count) { self.option_hash.count }
define_singleton_method(:all) { ~none }
define_singleton_method(:none) { new(0) }
end
end
end
require_relative 'option_set'
class Food
include OptionSet
option_set %i(beef potato banana)
end
RSpec.describe Food do
describe '::[]' do
context ':beef, :potato' do
subject { Food[:beef, :potato] }
it { is_expected.to eq Food.beef + Food.potato }
end
context ':beef, :beef' do
subject { Food[:beef, :beef] }
it { is_expected.to eq Food.beef }
end
context ':beef, 0b10' do
subject { Food[:beef, 0b010] }
it { is_expected.to eq Food.beef + Food.potato }
end
end
describe '::option_hash' do
subject { Food.option_hash }
let(:expected_value) do
{
beef: 0b001, # 1
potato: 0b010, # 2
banana: 0b100 # 4
}
end
it { is_expected.to eq expected_value }
end
describe '::beef.value' do
subject { Food.beef.value }
it { is_expected.to eq Food.option_hash[:beef] }
end
describe '[:beef, :potato].include?' do
subject { Food[:beef, :potato].include?(food) }
context 'beef' do
let(:food) { Food.new(0b001) }
it { is_expected.to eq true }
end
context 'potato' do
let(:food) { Food.new(0b010) }
it { is_expected.to eq true }
end
context 'banana' do
let(:food) { Food.new(0b100) }
it { is_expected.to eq false }
end
context 'beef + potato' do
let(:food) { Food.new(0b011) }
it { is_expected.to true }
end
context 'beef + banana' do
let(:food) { Food.new(0b101) }
it { is_expected.to eq false }
end
end
describe '~beef' do
subject { Food.beef.invert }
let(:potato_and_banana) { Food.new(0b110) }
it { is_expected.to eq potato_and_banana }
end
describe 'beef + potato' do
subject { Food.beef.union(Food.potato) }
let(:beef_and_potato) { Food.new(0b011) }
it { is_expected.to eq beef_and_potato }
end
describe '(beef + potato) & potato' do
subject { Food.beef.union(Food.potato).intersect(Food.potato) }
it { is_expected.to eq Food.potato }
end
describe '(beef + potato) - beef' do
subject { Food.beef.union(Food.potato).exclude(Food.beef) }
it { is_expected.to eq Food.potato }
end
describe '(beef + potato).to_a' do
subject { Food[:beef, :potato].to_a }
it { is_expected.to eq([:beef, :potato]) }
end
describe '(beef + potato).to_s' do
subject { Food[:beef, :potato].to_s }
it { is_expected.to eq 'Food[beef, potato]' }
end
end
@mklbtz
Copy link
Author

mklbtz commented Feb 6, 2017

Please excuse any typos

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