PrimitiveWrapper

This gem creates a thin shell to encapsulate primitive literal types such as integers, floats and symbols. There are a family of wrappers which mimic the behavior of what they contain. Primitive types have several drawbacks: no constructor to call, can't create instance variables, and can't create singleton methods. There is some utility in wrapping a primitive type. You can simulate a call by reference for example. You can also simulate mutability, and pointers. Some wrappers are dedicated to holding a single type while others may hold a family of types such as the Number wrapper. What is interesting to note is Number objects do not derive from Numeric, but instead derive from Value (the wrapper base class); but at the same time, Number objects mimic the methods of Fixnum, Complex, Float, etc. Many of the wrappers can be used in an expression without having to call an access method. There are also new types: Bool which wraps true,false and Property which wraps Hash types. The Property object auto-methodizes the key names of the Hash.

Installation

Add this line to your application's Gemfile:

gem 'primitive_wrapper'

And then execute:

$ bundle

Or install it yourself as:

$ gem install primitive_wrapper

Or try it out inside irb:

$ require "primitive_wrapper"

Usage

There are 12 wrapper classes that hold these pesky primitive literal objects. Unlike other class objects, these primitives have no constructor and attempt to look like the bottom turtle literals used in compiled languages like C/C++. Such primitive literals are completely immutable. The wrappers used in this gem transform these primitive types into mutable objects. In fact, these objects cannot be frozen as that would defeat their purpose. The freeze method on these wrapper classes is disabled. The wrapped literal now behaves like a true ruby object such as Array, or Hash. See the example below:

five = Integer.new(5)  # ERROR :: undefined method `new' for Integer:Class
five = Fixnum.new(5)   # ERROR :: undefined method `new' for Fixnum:Class
five = 5               # ok, this one works!

def five.tripple
  self + self + self
end                    # ERROR :: TypeError: can't define singleton

require "primitive_wrapper"

five = Int.new(5)      # we now have a wrapped Integer object
five + 5 + five        # 15  ... wrapper has the methods of an Integer object and can be used in an expression

def five.tripple
  self + self + self
end

five.tripple           # 15

five.kind_of? Integer  # false
five.kind_of? Value    # true 

5.kind_of? Integer  # true
5.kind_of? Value    # false

five.type_of? Integer # true
5.type_of? Integer # true

As we can see from the above code, the Int class does not derive from Integer or anything that would appear numeric. Our Int class is simply a container which happens to have the methods of Integer but does not derive itself from Integer. All of these container objects derive from the Value class. Also note the new method #type_of which allows us test the inner-most entity.

Object methods added

There are six instance methods added to the base Object class necessary to implement this system. The most important method is the #to_wrapper method. When an instance variable calls this method, one of the wrapper containers will generate a new instance. The one that gets picked is the one best suited for usability. We also have a test method called #wrapped? to see if we are dealing with the original entity or its thinly wrapped sibling. Another method named #prim_value will return the inner container value or simply return self if it is not a container derived from the Value class. This is mostly used internally by the container objects. Now that we have wrapped versions of the base elemental objects, we need a type checking method that can work on both wrapped and unwrapped primitives: called #type_of?; this works like #kind_of? but instead works on the inner-most element (if Value derived type). Also, #type gets the effective class and evaluates as #prim_value.class. Also needed is a method that can duplicate a primitive or its wrapped version; this is called #pw_copy which works like dup. Note that you can't call #dup on an Integer without raising an exception.

Below shows which objects get mapped to which containers:

nil          =>   Bit
true, false  =>   Bool
Integer      =>   Int 
Float        =>   FloatW  
Complex      =>   Number  
Rational     =>   Number  
Symbol       =>   SymbolW
Hash         =>   Property
String       =>   Datum
Array        =>   XArray
Range        =>   XRange
Rational     =>   Fraction  

Some examples are as follows:

n = nil.to_wrapper      # n.class    == Bit
b = true.to_wrapper     # b.class    == Bool
y = 13.to_wrapper       # y.class    == Int
t = 3e-9.to_wrapper     # t.class    == FloatW
z = (1+6i).to_wrapper   # z.class    == Number
str = "yo!".to_wrapper  # str.class  == Datum
me = :me.to_wrapper     # me.class   == SymbolW
prop = {}.to_wrapper    # prop.class == Property
ary = [1,2].to_wrapper  # ary.class  == XArray  ... X for extended functionality
ran = (3..0).to_wrapper # ran.class  == XRange
aw = /[az]/.to_wrapper  # aw.class   == Value  ... everything else goes inside the Value container

The next sections will detail which objects are allowed to be contained within each container objects.

Value class container

This is the base class for all the containers created by this gem. Any object or primitive type can be held in this container. To access the contents, simply call #val, #prim_val, #unwrap, or for non-numeric types use the tilde ~ prefix operator. Note that only Value derived objects can call #val while every object can call #prim_val. You can also replace the contents with the #replace method or the #val= method. The Value container has no restrictions on what it can hold. Other wrappers will raise an exception if the wrong type is added. The comparison binary operators ==, and != are used to compare equality and inequality; this is done on the inner contained element. The value container can only contain a single object; it is like an Array object that is only allowed to hold one item. See the example code below:

require 'PrimitiveWrapper'

# simulation of a pointer

a = Value.new(15)
b = a
b.val        # 15
a.val        # 15
a.val = 2    # assign new inner value
b.val        # a == 2  ... a,b point to the same data  

# simulation of pass by reference

def pass_by_reference(a, ref)
  ref.val += 15 + a
  return :yes
end

t = pass_by_reference(1, bjc = Value.new(10))  # t==:yes,  bjc.val==26

The Value class has the following instance methods:

:val,  :prim_val, :unwrap, :~  #   Returns the internal data object (~ overriden by Int)
:val=, :replace                #   Sets the internal data object
:valid_type(inst)              #   Returns true if `inst` variable is compatible to wrapper object
                               #   Child classes override this method
:freeze                        #   Does nothing ... defined to prevent Object#freeze from being called   
:==                            #   Compares equality of the inner elements
:!=                            #   Compares inequality of the inner elements
:inspect                       #   "(Value)==>data"  where Value is the class name of the Value derived object, and data is #val.inspect
:to_s                          #   returns #to_s on contained entity
:to_wrapper                    #   returns self ... prevents wrapping a wrapped entity
:wrapped?                      #   also on object, tests for wrapped behavior 
:type_of?                      #   also added to object, tests innermost entity or self if primitive
:type                          #   returns the class of the wrapped primitive or self if primitive  
:inspect                       #   string representing wrapped primitive             

Bool class container

The Bool container is allowed to contain only true or false primitive literals. The container adds three binary operators: &, |, ^ and the prefix operator !. Equality operators are inherited from the base class Value. When an operator is called, a new Bool type is created. Most of the other wrapper classes will return a standard type in order to make expressions less promblematic. You can mix Bool types with true and false in an expression, but you must make sure the Bool type comes first or unexpected results may occur. As with all wrapped containers, left-to-right expressions are owned by the left object. If this cannot be avoided, then calling #val will solve the problem. This class is derived from Value. See the example below:

aaa = Bool.new(true)
bbb = Bool.new(false)

aaa & !bbb | bbb & !aaa == aaa ^ bbb  # definition of XOR

t = aaa ^ true   #  creates a new Bool type with value of false
t = bbb ^ true   #  creates a new Bool type with value of true
t = true ^ aaa   #  incorrect order returns false  ... not a Bool type
t = true ^ bbb   #  incorrect order returns false  ... not a Bool type
t = true ^ "?"   #  false  ... any object will return false except nil and false
t = true ^ 0     #  false
t = true ^ false #  true   ... this is why Bool must come first when mixing Bool with true or false
t = true ^ nil   #  true   ... one of the only two primitive types that will provide a true result

# other methods
.to_i            # returns 0 if false, 1 if true
.to_int          # returns an Int object wrapper filled with either 0 or 1
.val             # returns true or false

Note that the ~ prefix operator returns the primitive value. The operator was also added to: Symbol, TrueClass, FalseClass, and NilClass which simply return self; these classes previously did not define this operator. Only Integer and Int derived objects define this as being a one's complement operator. This ~ prefix operator permits a terse way of getting to the primitive and can be used by Value derived objects or primitives described above. This is necessary when using Bool types in conditional statements. Alternatively, you can use the #val method.

Bit class container

The Bit container is derived from Bool, but overrides the operators to return primitive values. The Bit container also allows nil as a valid container entity. Bit objects are therefore thinner than Bool objects and are most likely easier to work in expressions. There are also FixedBit derived classes called: Null, TrueW, FalseW. These are essentially Bit objects restricted to a single type. See the example below:

a  = Bit.new(nil)
aa = Null.new       # a==aa ... ~a => nil   ~aa => nil    ~nil => nil 

b  = Bit.new(true)
bb = TrueW.new      # b==bb ... ~b => true  ~bb => true   ~true => true

c  = Bit.new(false)
cc = FalseW.new     # c==cc ... ~c => false ~bb => false  ~false => false

bb ^ b              # false
y = ~b ? 3:4        # y == 3  ... ~b shortcut for b.val

FixedBit.new        #  *** RuntimeError Exception: FixedBit cannot create instance
aa.val = nil        #  *** RuntimeError Exception: can't assign primitive type

Int class container

The Int container looks like a duck, quacks and waddles like a duck, swims like a duck, but it ain't a duck! The ancestor chain of Int only shares Value and does not include Numeric. With a little meta-magic, the Int class mimics the methods of Integer so that it can be directly used within an expression or without having to call #val to get the contained entity. Note that ~ does not get the contained entity, but instead returns the one's complement. You can have your cake and eat it too by using two tildes ~~. This undoes the one's complement and results in the primitive value. The #to_int method on Int returns self, while #to_i accesses the primitive value. Most of the time if Integer does not recognize something it calls coerce on the foreign object (which we grabbed from Integer). This means that most expressions should work in whatever order you wish: if not, you will have to convert the Int object into an Integer object. Pretty much all expressions with Int will create a primitive Integer. We also have new methods #inc and #dec which work like the C/C++ post increment operators. Prefix version methods are #pre_inc and #pre_dec. See the example below:

aaa = 100.to_wrapper   # method added to Object
bbb = Int.new(200)     # standard constructor

ttt = aaa + bbb        # ttt==300   ... ttt.class == Fixnum
~aaa                   # -101
~~aaa                  # 100

aaa.to_int             # returns self, an Int object
aaa.to_i               # returns 100
100.to_i               # returns 100
100.to_int             # returns 100
3 + aaa                # returns 103
aaa + 3                # returns 103

FloatW class container

The FloatW container wraps Float types and possesses the thin wrapper similar to how Int works. This can also be directly used within an expression. Only the Float derived objects may be placed inside this container. The wrapper is so thin, you might think that you are actually a Float type. See the example below:

aaa = 3.1415926.to_wrapper
bbb = FloatW.new(3e0)

aaa.to_i    # 3
aaa.to_int  # 
aaa ** bbb  # 31.006275093569673
aaa.to_i    # 3
aaa.to_int  # (Int==>3)

Number class container

The Number container object can contain any object that is derived from Numeric. This includes Float, Fixnum, Bignum, Complex, Rational, or even something that you create yourself so long as it derives from Numeric. The class generator for Number grabs method names from all the standard library of Numeric derived classes. This means that there is a possibility that the contained entity will not support some of the method calls. This is no different than calling an undefined method. Int, FloatW, Number, Datum all derive from ValueAdd which is a child of Value. You can't create an instance of ValueAdd, but it is used to store common methods as well as some class construction methods. These will be described later on. This will come handy if you have a custom Numeric class and wish to import some of the methods defined there. This works just like the other specific number wrappers previously described.

Datum class container

The Datum container holds everything a Number can hold and allows String data to be added. This represents things that a human can type into a form as a general data thing. The methods of String are also added. Errors will occur if you try to call string methods on a Datum that is holding a number and visa versa.

Fraction class container

The Fraction container holds a Rational primitive. All the methods of Rational are mimicked with some important modifications. All operators of Fraction return a Fraction type. Note that Rational types when operated with Float types will return Float types. The mutating methods of the Value base class as well as some new mutating methods have also been added. The constructor can take 1 to 3 parameters. The main idea behind this wrapper is to support mixed fractions as well as proper fractions. See below:

# Construction:  mixed fractions using 3 parameters
frac = Fraction.new(3,4,5)   # frac.inspect == "(Fraction==>(19/5))"  ... frac.to_s == "(3 4/5)"  ... frac.to_s(false) == "3 4/5"
frac = Fraction(5,6,7)       # shorter function call   ... (Fraction==>(41/7))
frac = Fraction(-5,-6,7)     # negative                ... (Fraction==>(-41/7))
# note: always negate first two parameters to define negative fractions

# Construction:  proper fractions using 2 parameters
frac = Fraction(27,7)            # frac.inspect == "(Fraction==>(27/7))" ... frac.to_s false == "3 6/7"

# Construction:  using float with/without approximation
frac = Fraction(Math::PI, 0.1)      # fract.inspect == "(Fraction==>(22/7))"  ... second parameter => percent error
frac = Fraction(Math::PI, 0.0001)   # fract.inspect == "(Fraction==>(355/113))" 
frac = Fraction(Math::PI)           # fract.inspect == "(Fraction==>(884279719003555/281474976710656))"
frac = Math::PI.to_fraction(0.0001) # (Fraction==>(355/113))
frac = Math::PI.to_fraction         # (Fraction==>(884279719003555/281474976710656))
# note: Rational does not have approximate construction

# Construction:  using float with target denominator
frac = Fraction(Math::PI, 113)  # (Fraction==>(355/113))
frac = Fraction(Math::PI, 7)    # (Fraction==>(22/7))

# mutating methods:
frac = Math::PI.to_fraction(0.0001)
frac.negate!      # make negative
frac.to_s(false)  # "-3 -16/113"  ... note: true add parenthesis, false removes parenthesis
frac.abs!         # make positive
frac.to_s(false)  # "3 16/113"
frac.to_r.to_s    # "355/113"     ... convert to rational, then to string for proper fraction
frac *= 113       # (Fraction==>(355/1))  ... operators return type of Fraction
frac.to_s         # 355   ... no parenthesis if whole number           

SymbolW class container

The SymbolW class container holds a Symbol primitive. It derives directly from Value. Although rarely used, Symbol types do have some immutable methods that have some string-like behavior. The SymbolW class implements most of these, and adds some mutating methods as well. This makes the SymolW object appear to be completely mutable; however, the inner element remains immutable. See below:

# non-mutating methods taken from Symbol:
# :<, :<=, :>, :>=, :[], :between?, :capitalize, :downcase, :empty?, :length, :next, :size, :succ, :pred, :swapcase, :upcase

# non-mutating methods taken from String:
# :append, :include?, :prepend   ... actually append renamed from :<<

# mutating methods of SymbolW
# :[]=, :append!, :capitalize!, :downcase!, :next!, :prepend!, :replace, :succ!, :pred!, :swapcase!, :upcase!

# construction:
wsym = :my_symbol.to_wrapper
tsym = SymbolW.new(:my_other_symbol)

# example:
wsym[1] = :e   #  wsym == (SymbolW==>:me_symbol)

Property class container

The Property class container holds a Hash object. You can also place a Hash object inside the Value container, but there are no exposed methods there. You can initialize the property class with a Hash instance, or start with a blank slate. What is cool about the Property object is the internal hash keys become setters and getters on the instance so long as they obey the rules. Additionally the internal hash is the original hash and not a copy; so if the contained hash object is changed outside of the Property container, new access methods will automatically be generated. Hash keys that comprise upper and lowercase characters, underscore, and digits (with the first character not a number) will become methods on the Property instance. Note that these names cannot compete with existing instance method names of Object or Property including private methods. Nested hierarchical names are not added: only the first-level names. Non-compliant names are still added to the internal Hash instance; they just don't have the corresponding method names. Another bonus is we have an unmolested Hash unlike other competing gems that trash the Hash with property getters and setters. Most Hash methods except for :[], :[]= have not been implemented in order to allow for more property names. You can quickly get to the internal Hash by using the ~ tilde prefix operator however. See the code below:

abc = Property.new
xyz = Property.new {:fred => "freddy", :george => "georgey"}

abc.seven = 7
abc.property? :seven         # true
abc.seven                    # 7
abc.val                      # {:seven => 7}
abc.to_s=15                  # this won't create a property because :to_s is reserved
abc.property? :to_s          # false
abc.deferred? :to_s          # true    ... internal hash owns this, but no property is created
abc.to_s                     # "{:seven=>7, :to_s=>15}"

abc.defined_properties!      # [:seven]
xyz.defined_properties!      # [:fred, :george]
abc.deferred_properties!     # [:to_s]
xyz.deferred_properties!     # []

abc.keys                     # NoMethodError Exception: undefined method `keys' for (Property==>{...}):Property 
(~abc).keys                  # [:seven, :to_s] ... Note ~abc.keys does not work as this is interpreted as: ~(abc.keys)
abc.val.keys                 # [:seven, :to_s] ... probably best syntax
hash = ~abc                  # grab internal Hash object
hash[:cool] = "yes!"
abc.cool                     # "yes!"  ... wrapper behavior looks pointer-ish
abc[:works2] = "yep!"
abc.works2                   # "yep!"  ... wrapper supports array access
abc[:seven]                  # 7
abc.sam = {}
abc.defined_properties!      # [:cool, :sam, :seven, :works2]
abc.val = {}
abc.defined_properties!      # []  ... you just zapped the entire hash after abc.val = {}

# Property Instance methods
# :~ :unwrap :val :prim_value   ...  returns internal hash
# :val= :replace                ...  assigns new hash to wrapper
# :[] :[]=                      ...  access internal hash element
# :deferred? key                ...  true if key is in internal hash but will not create a property access variable
# :property? key                ...  true if key is registered as a property access variable 
# :split!                       ...  returns an array of two Hash instances with [valid_property_hash, deferred_hash]
# :rekey!                       ...  rekeys property and deferred keys ... called internally
# :valid_type inst_var          ...  used internally for #replace or #val=  ... true if Hash or Property instance
# :method_missing               ...  used internally to simulate property methods
# :import_hash! ext_hash        ...  merge foreign hash into self 
# :ensure_valid                 ...  used internally ... raises error on type mismatch from :valid_type  
# :deferred_properties!         ...  returns list of keys that do not have property methods
# :defined_properties!          ...  returns list of keys that have property methods
# :define_properties! [] , dflt ...  assignes default properties to a list of property keys

# Property Class methods
# ::good_candidate_name? name   ...  test to see if name is formatted corectly, and is of type Hash
# ::good_key? name              ...  true if properly formatted and not a reserved word
# ::bad_key? name               ...  true if wrong type, or reserved word, or not formatted correctly
# ::reserve_property_names! []  ...  adds additional reserved words    

XRange class container for Range with extensions

While inheritance could have accomplished similar goals, this gem is all about wrapped objects. Mutability would not be possible with an inherited version. Note that Range objects are not completely immutable; range_instance.first.upcase! will modify the range that is defined with strings. The XRange object redefines much of the behavior of Range with regard to ranges that descend rather than ascend. Note that descending Range objects will return false on methods such as #include? and #cover? and will not enumerate with #each. There is great utility in having a functional downward range that actually does what would naturally be expected. This gem includes the gem pred which makes reverse behavior possible. Integers and Strings use the method #succ within the classic Range to get the next sequence value. The method #pred is added to to Integers and Strings to get the inverse sequence value. The XRange container captures the methods of Range but redefines most of the methods to define reverse behavior. Below is a list of what has changed:

# :reverse         Range, XRange  ... reverses first and last, creates a new object of same type as caller
# :reverse!        XRange         ... self-modified version of :reverse
# :reverse?        Range, XRange  ... true if descending
# :simplify        Range, XRange  ... converts if possible excluded to non-excluded range with same behavior
# :simplefy!       XRange         ... self-modified version of :simplify
# :reorder         Range, XRange  ... creates ascending Range or XRange
# :reorder!        XRange         ... self-modified version of :reorder  
# :to_range        Range, XRange  ... accesses inner element or returns self if Range
# :to_xr           Range, XRange  ... creates an XRange object or returns self if XRange
# :size, :count    XRange         ... redefined from nil to actual size if descending were reversed
# :include?        XRange         ... redefined from false to behave as if reversed
# :cover?          XRange         ... redefined from false to behave as if reversed
# :member?         XRange         ... redefined from false to behave as if reversed
# :to_a            XRange         ... redefined from empty [] to list ordered sequence
# :max, :min       XRange         ... redefined from nil to return maximum/minimum range values
# :eql? :==        XRange         ... compares using contained entity
# :step(n)         XRange         ... redefined to work with descending sequences
# :reverse_step(n) XRange         ... reverses sequence before stepping
# :each            XRange         ... redefined to work with descending sequences
# :reverse_each    XRange         ... reverse version of :each
# :re_range(ng)    XRange         ... creates new range converting negative start, end to positive given size==ng
#                                     used by XArray access operators that use descending ranges.

XArray class container for Array with extensions

This creates a wrapped Array with behavior that enhances the access operators of Array; additionally a few new methods were added to the wrapped version as well as the original Array class. XArray mimics the behavior of Array but redefines :[] and :[]= to include lists which comprise indexes, ranges, and arrays of indexes. There is also behavior that defines many-to-one, one-to-many, and many-to-many operations. Note that one-to-one behavior has not changed. An example best explains how this works:

  ary = [2,3,5,7,11,13,17,19,23,29,31,37,41].to_xa
  tst = ary[5..0,10,-1]  # tst == [13, 11, 7, 5, 3, 2, 31, 41]  ... note reverse range

  # many-to-many
  ary[4,5,1] = "a", "b", "c", "d"  # ary == [2, "c", 5, 7, "a", "b", 17, 19, 23, 29, 31, 37, 41]
  # note above, unused ignored

  # one-to-many
  ary[8..-1] = :s  #  ary == [2, 3, 5, 7, 11, 13, 17, 19, :s, :s, :s, :s, :s]
  # note: ary.prim_value[8..-1] = :s  returns this:  [2, 3, 5, 7, 11, 13, 17, 19, :s] ... which is useless

  # many-to-one
  ary[-1] = 1,2,3  # ary == [2, "c", 5, 7, "a", "b", 17, 19, :s, :s, :s, :s, [1, 2, 3]]
  ary[-2] = [:a, :b] # ary == [2, "c", 5, 7, "a", "b", 17, 19, :s, :s, :s, [:a, :b], [1, 2, 3]]

The are a few more upgrades as well as follows:

# :to_xa          Array, XArray  ... Converts Array to XArray unless already an XArray
# :include?       XArray         ... Updated to support a list of items all of which must be included to return true
# :include_any?   XArray         ... true if any of the list of itmes are included
# :delete_at      XArray         ... enhanced version can delete a list of indexes or ranges or arrays of indexes
#                                    returns what was deleted in the order of the list
# :ssort, :ssort! XArray         ... sorts with each element called with method :to_s     
# :isort, :isort! XArray         ... sorts with each element called with method :inspect     

Customization

Things should work out of the box, until they don't. This section will detail what customization is possible with what container. First, Property has one method to reserve allowed property key names. This is done as follows:

Property.reserve_property_names! [:Fred, :Wilma, :BamBam, :Barney, :Betty, :Pebbles]

bedrock = Property.new
bedrock.Fred = "Yaba Daba Doo!"
bedrock.property? :Fred    # false
bedrock.deferred? :Fred    # true
bedrock[:Fred]             # "Yaba Daba Doo!"
bedrock.Fred               # *** NoMethodError Exception: undefined method `Fred' for (Property==>{...}):Property
bedrock.Test = "pass"
bedrock.Test               # "pass"

The containers Int, FloatW, Number, and Datum all derive from ValueAdd which has two class methods. The ValueAdd class derives from Value, but cannot create an instance. Its purpose is to generate methods that belong somewhere else. If we peer into the code we see this:

class ValueAdd < Value
  def valid_type(prm) # must override
    false
  end
  def self.bestow_methods(*args)
    args = args.first if args.first.kind_of? Array
    args.each do |meth|
      define_method meth do |*args, &block|
        @value.send(meth, *args, &block)
      end  
    end    
  end  
  def self.capture_base_methods(type, except=Object)
    add_me = type.instance_methods - except.instance_methods - Value.instance_methods - [:singleton_method_added]
    bestow_methods add_me
  end
end

If we call Int.capture_base_methods(Fixnum), all of the methods that a Fixnum can do is bestowed to the Int class. If you define a new method to Integer called :do_my_math_thing, you can teach Int to do it by calling Int.bestow_methods(:do_my_math_thing). You should also add it to Number and Datum. Let's say you have a really cool Matrix class that you would like to teach the Number wrapper. Here is how you would do it:

class Number
  capture_base_methods(Matrix, self)
end

Some of the most extensive customizations involve the SymbolW wrapper class. Only a small subset of String methods were added to this wrapper. They were chosen to complement the non-mutating String like functions already available to Symbols. For example, #upcase! was added to complement #upcase. Note that the Symbol class does not and cannot have a mutating method. There are several Class methods on SymbolW that capture Symbol and String methods in several configurable ways. If we look under the hood at the source code we will see how they got defined by example:

# ClassW class methods:
#
#  -- single defines
# ::bestow_symbol_method(meth_def_name, meth_call_name, mutate_self=false)
# ::bestow_string_non_mutate_method(meth_def_name, meth_call_name, return_type = SymbolW)
# ::bestow_string_mutate_method(meth_def_name, meth_call_name)
#
# -- group defines
# ::bestow_string_mutate_methods(meths)
# ::bestow_string_non_mutate_methods(meths, return_type = SymbolW)
# ::bestow_symbol_methods(meths, mutate_self=false)
#

The group defines are used when you don't need to rename a method and wish to install a bunch of them. As an example SymbolW.bestow_symbol_methods [:next, :succ, :[], :length, :size, :upcase, :downcase, ...] was called to install the standard set of methods that exist on Symbol. The last parameter when set to true will mutate the inner Symbol which means that it gets replaced with the result of the method. Some methods that do not return a string like #length should not be used as a source for mutating methods. Passing true as the second parameter will auto-bang the name turning #upcase into #upcase!. The singular version of the method gives you more control allowing you to completely rename the method. The number of Symbol methods is fairly small when compared to the number of String methods. It was intentional to leave most of these open as it there are many ways to transform these methods. The self mutating version is the most straight forward as we don't need to worry about the return type. See the code below to see how it was used:

  SymbolW.bestow_string_mutate_method(:prepend!, :prepend)
  SymbolW.bestow_string_mutate_method(:append!, :<<)
  SymbolW.bestow_string_mutate_method(:[]=, :[]=)

Looking at the code, we see that the #append! method is a renamed version of :<<. What is interesting to note is that both in the String and the Symbol classes #append is not found. Only string implements :<< which is a binary operator that does not mutate. Because we are bestowing this self-mutating method to SymbolW, we don't need to consider what the return type is as self is the only logical choice. The non-mutating version must choose what kind of type is returned. We have four choices for return types: (1) force a conversion to SymbolW, (2) return Symbol, (3) return String, or (4) return whatever the string method would otherwise return. We would choose the 4th version for any non-string return method such as #match. To select this we pass nil to the return type which says let the string method decide. Now we are left with the head-scratcher on how to decide which is the best return type to give to a string method bestowed upon a wrapped symbol thing. This can be mostly a matter of taste, but we can choose custom names that convey this information. Now if you have an extension library such as used by the gstring gem, you have an even larger list to choose from. So it is left to you to decide.

Revision History

Version 2.3.0

  • Fixnum, Bignum warning messages removed.
  • Documentation updates

Version 2.2.0

  • Bug fixes for Fraction wrapper
  • New construction method for Fraction

Version 2.1.0

  • Added Fraction wrapper for Rational types

Version 2.0.0

  • Fixed Range reversals defined with excluded end
  • Added #type to Object, Value
  • Added #simple? to Range, XRange

Version 1.0.1

  • Updated documentation to include undocumented methods
  • Fixed XArray #delete_at bugs

Version 1.0.0

  • Fixed wrapper inside wrapper causing stack overflow
  • Documentation updates
  • Added new wrappers for Range, Array as XRange and XArray
  • Added primitive methods
  • Added Value methods, and enhanced other derived wrappers

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install.

Contributing

I need to control this for the time being. You are welcome to shoot me an EMAIL if you have any issues or suggestions.

License

The gem is available as open source under the terms of the MIT License.