Last active
May 1, 2017 15:37
-
-
Save mklbtz/553ec615b87a61007c0612d936cc2425 to your computer and use it in GitHub Desktop.
Porting Apple's OptionSets to Ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Please excuse any typos