From b1d37705556986c42ffb1d68c6ad49ff4f0525e6 Mon Sep 17 00:00:00 2001 From: Joel Drapper Date: Fri, 20 Dec 2024 10:50:28 +0000 Subject: [PATCH] Make enums ordered (#269) Enums are now by default ordered by their definition order, which means they are now range-able. --- lib/literal.rb | 2 +- lib/literal/enum.rb | 86 ++++++++++++++++++++++++++++++--------------- test/enum.test.rb | 54 ++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 29 deletions(-) diff --git a/lib/literal.rb b/lib/literal.rb index c8c4175..b45c42f 100644 --- a/lib/literal.rb +++ b/lib/literal.rb @@ -31,7 +31,7 @@ module Literal def self.Enum(type) Class.new(Literal::Enum) do - prop :value, type, :positional + prop :value, type, :positional, reader: :public end end diff --git a/lib/literal/enum.rb b/lib/literal/enum.rb index 1c91349..e03b284 100644 --- a/lib/literal/enum.rb +++ b/lib/literal/enum.rb @@ -9,6 +9,7 @@ class << self attr_reader :members def values = @values.keys + def names = @names def prop(name, type, kind = :keyword, reader: :public, predicate: false, default: nil) super(name, type, kind, reader:, writer: false, predicate:, default:) @@ -17,9 +18,10 @@ def prop(name, type, kind = :keyword, reader: :public, predicate: false, default def inherited(subclass) subclass.instance_exec do @values = {} - @members = Set[] + @members = [] + @indexes_definitions = {} @indexes = {} - @index = {} + @names = {} end if RUBY_ENGINE != "truffleruby" @@ -32,8 +34,16 @@ def inherited(subclass) end end + def position_of(member) + coerce(member).__position__ + end + + def at_position(n) + @members[n] + end + def index(name, type, unique: true, &block) - @indexes[name] = [type, unique, block || name.to_proc] + @indexes_definitions[name] = [type, unique, block || name.to_proc] end def where(**kwargs) @@ -43,11 +53,11 @@ def where(**kwargs) key, value = kwargs.first - types = @indexes.fetch(key) + types = @indexes_definitions.fetch(key) type = types.first Literal.check(actual: value, expected: type) { |c| raise NotImplementedError } - @index.fetch(key)[value] + @indexes.fetch(key)[value] end def find_by(**kwargs) @@ -57,15 +67,15 @@ def find_by(**kwargs) key, value = kwargs.first - unless @indexes.fetch(key)[1] + unless @indexes_definitions.fetch(key)[1] raise ArgumentError.new("You can only use `find_by` on unique indexes.") end - unless (type = @indexes.fetch(key)[0]) === value + unless (type = @indexes_definitions.fetch(key)[0]) === value raise Literal::TypeError.expected(value, to_be_a: type) end - @index.fetch(key)[value]&.first + @indexes.fetch(key)[value]&.first end def _load(data) @@ -77,12 +87,8 @@ def const_added(name) object = const_get(name) if self === object - if @values.key?(object.value) - raise ArgumentError.new("The value #{object.value} is already used by #{@values[object.value].name}.") - end - object.instance_variable_set(:@name, name) - @values[object.value] = object - @members << object + # object.instance_variable_set(:@name, name) + @names[object] = name define_method("#{name.to_s.gsub(/([^A-Z])([A-Z]+)/, '\1_\2').downcase}?") { self == object } object.freeze end @@ -90,12 +96,21 @@ def const_added(name) def new(*args, **kwargs, &block) raise ArgumentError if frozen? + new_object = super(*args, **kwargs, &nil) - if block - new_object.instance_exec(&block) + if @values.key?(new_object.value) + raise ArgumentError.new("The value #{new_object.value} is already used by #{@values[new_object.value].name}.") end + @values[new_object.value] = new_object + + new_object.instance_variable_set(:@__position__, @members.length) + + @members << new_object + + new_object.instance_exec(&block) if block + new_object end @@ -106,7 +121,7 @@ def __after_defined__ constants(false).each { |name| const_added(name) } end - @indexes.each do |name, (type, unique, block)| + @indexes_definitions.each do |name, (type, unique, block)| index = @members.group_by(&block).freeze index.each do |key, values| @@ -119,7 +134,7 @@ def __after_defined__ end end - @index[name] = index + @indexes[name] = index end @values.freeze @@ -179,17 +194,9 @@ def to_h(&) end end - def initialize(name, value, &block) - @name = name - @value = value - instance_exec(&block) if block - freeze - end - - attr_reader :value - def name - "#{self.class.name}::#{@name}" + klass = self.class + "#{klass.name}::#{klass.names[self]}" end alias_method :inspect, :name @@ -207,4 +214,27 @@ def deconstruct_keys(keys) def _dump(level) Marshal.dump(@value) end + + def <=>(other) + case other + when self.class + @__position__ <=> other.__position__ + else + raise ArgumentError.new("Can't compare instances of #{other.class} to instances of #{self.class}") + end + end + + def succ + self.class.members[@__position__ + 1] + end + + def pred + if @__position__ <= 0 + nil + else + self.class.members[@__position__ - 1] + end + end + + attr_reader :__position__ end diff --git a/test/enum.test.rb b/test/enum.test.rb index ba1e08d..70377f7 100644 --- a/test/enum.test.rb +++ b/test/enum.test.rb @@ -79,6 +79,30 @@ class SymbolTypedEnum < Literal::Enum(Symbol) assert_equal SymbolTypedEnum.coerce(:A), SymbolTypedEnum::B end +test ".position_of with member" do + assert_equal Color.position_of(Color::Red), 0 + assert_equal Color.position_of(Color::Green), 1 + assert_equal Color.position_of(Color::Blue), 2 +end + +test ".position_of with name" do + assert_equal Color.position_of(:Red), 0 + assert_equal Color.position_of(:Green), 1 + assert_equal Color.position_of(:Blue), 2 +end + +test ".position_of with value" do + assert_equal Color.position_of(1), 0 + assert_equal Color.position_of(2), 1 + assert_equal Color.position_of(3), 2 +end + +test ".at_position" do + assert_equal Color.at_position(0), Color::Red + assert_equal Color.at_position(1), Color::Green + assert_equal Color.at_position(2), Color::Blue +end + test ".to_set" do assert_equal Color.to_set, Set[ Color::Red, @@ -103,6 +127,36 @@ class SymbolTypedEnum < Literal::Enum(Symbol) ] end +test "#succ" do + assert_equal Color::Red.succ, Color::Green + assert_equal Color::Green.succ, Color::Blue + assert_equal Color::Blue.succ, nil +end + +test "#pred" do + assert_equal Color::Red.pred, nil + assert_equal Color::Green.pred, Color::Red + assert_equal Color::Blue.pred, Color::Green +end + +test "#<=>" do + assert_equal Color::Red <=> Color::Green, -1 + assert_equal Color::Red <=> Color::Red, 0 + assert_equal Color::Green <=> Color::Red, 1 +end + +test "#name" do + assert Color::Red.name.end_with?("Color::Red") +end + +test "enums are rangeable" do + range = (Color::Red..Color::Green) + + assert Range === range + assert_equal Color::Red, range.begin + assert_equal Color::Green, range.end +end + test "#where" do assert_equal [Color::Red], Color.where(hex: "#FF0000") end