vector_space 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009 Tom Stuart
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,110 @@
1
+ VectorSpace
2
+ ===========
3
+
4
+ A Ruby library for treating multidimensional values as elements of a vector space.
5
+
6
+ Examples
7
+ --------
8
+
9
+ A simple vector space:
10
+
11
+ class Round < VectorSpace::SimpleVector
12
+ has_dimension :hoegaarden
13
+ has_dimension :franziskaner
14
+ has_dimension :fruli
15
+ has_dimension :water
16
+ end
17
+
18
+ >> Round.new
19
+ => 0 and 0 and 0 and 0
20
+
21
+ >> Round.new :hoegaarden => 5, :fruli => 3
22
+ => 5 and 0 and 3 and 0
23
+
24
+ With custom descriptions:
25
+
26
+ class Round < VectorSpace::SimpleVector
27
+ has_dimension :hoegaarden, :describe => lambda { |n| "#{n} Hoegaardens" unless n.zero? }
28
+ has_dimension :franziskaner, :describe => lambda { |n| "#{n} Franziskaners" unless n.zero? }
29
+ has_dimension :fruli, :describe => lambda { |n| "#{n} Frülis" unless n.zero? }
30
+ has_dimension :water, :describe => lambda { |n| "#{n} waters" unless n.zero? }
31
+ has_zero_description 'no drinks'
32
+ end
33
+
34
+ >> Round.new
35
+ => no drinks
36
+
37
+ >> Round.new :hoegaarden => 5, :fruli => 3
38
+ => 5 Hoegaardens and 3 Frülis
39
+
40
+ >> Round.new(:hoegaarden => 2, :franziskaner => 3) + Round.new(:water => 2, :fruli => 2, :hoegaarden => 1)
41
+ => 3 Hoegaardens and 3 Franziskaners and 2 Frülis and 2 waters
42
+
43
+ >> (Round.new(:hoegaarden => 2, :franziskaner => 3) * 2) - Round.new(:hoegaarden => 1)
44
+ => 3 Hoegaardens and 6 Franziskaners
45
+
46
+ An indexed family of vector spaces with custom descriptions:
47
+
48
+ class Cocktail < VectorSpace::SimpleIndexedVector
49
+ indexed_by :units
50
+ has_dimension :gin, :describe => lambda { |units, n| "#{n}#{units} gin" unless n.zero? }
51
+ has_dimension :vermouth, :describe => lambda { |units, n| "#{n}#{units} vermouth" unless n.zero? }
52
+ has_dimension :whisky, :describe => lambda { |units, n| "#{n}#{units} whisky" unless n.zero? }
53
+ has_dimension :vodka, :describe => lambda { |units, n| "#{n}#{units} vodka" unless n.zero? }
54
+ has_dimension :kahlua, :describe => lambda { |units, n| "#{n}#{units} Kahlúa" unless n.zero? }
55
+ has_dimension :cream, :describe => lambda { |units, n| "#{n}#{units} cream" unless n.zero? }
56
+ end
57
+
58
+ >> martini = Cocktail.new :units => :cl, :gin => 5.5, :vermouth => 1.5
59
+ => 5.5cl gin and 1.5cl vermouth
60
+
61
+ >> manhattan = Cocktail.new :units => :cl, :whisky => 5, :vermouth => 2
62
+ => 2cl vermouth and 5cl whisky
63
+
64
+ >> white_russian = Cocktail.new :units => :oz, :vodka => 2, :kahlua => 1, :cream => 1.5
65
+ => 2oz vodka and 1oz Kahlúa and 1.5oz cream
66
+
67
+ >> martini * 2
68
+ => 11cl gin and 3cl vermouth
69
+
70
+ >> martini + manhattan
71
+ => 5.5cl gin and 3.5cl vermouth and 5cl whisky
72
+
73
+ >> martini + white_russian
74
+ ArgumentError: can't add 5.5cl gin and 1.5cl vermouth to 2oz vodka and 1oz Kahlúa and 1.5oz cream
75
+
76
+ A real (one-dimensional) example:
77
+
78
+ class Money < VectorSpace::SimpleIndexedVector
79
+ indexed_by :currency, :default => Currency::GBP
80
+ has_dimension :cents,
81
+ :coerce => lambda { |n| n.round }, # or just :coerce => :round if you have Symbol#to_proc
82
+ :describe => lambda { |currency, cents| "#{currency.symbol}#{cents / 100}.#{sprintf('%02d', cents % 100)}" }
83
+ end
84
+
85
+ >> a = Money.new(:currency => Currency::GBP, :cents => 9900)
86
+ => £99.00
87
+
88
+ >> b = Money.new(:currency => Currency::GBP, :cents => 1500)
89
+ => £15.00
90
+
91
+ >> a < b
92
+ => false
93
+
94
+ >> a > b
95
+ => true
96
+
97
+ >> a + b
98
+ => £114.00
99
+
100
+ >> c = Money.new(:currency => Currency::USD, :cents => 99)
101
+ => $0.99
102
+
103
+ >> a < c
104
+ => false
105
+
106
+ >> c < a
107
+ => false
108
+
109
+ >> a + c
110
+ ArgumentError: can't add $0.99 to £99.00
data/TODO ADDED
@@ -0,0 +1,3 @@
1
+ * write documentation
2
+ * release extracted specs to GitHub
3
+ * support "dynamic dimensions" (arbitrary numbers of dimensions, created when used)
@@ -0,0 +1,13 @@
1
+ require 'forwardable'
2
+
3
+ require 'vector_space/component_reflection'
4
+ require 'vector_space/dimension_reflection'
5
+
6
+ require 'vector_space/partial_order'
7
+ require 'vector_space/arithmetic'
8
+ require 'vector_space/vector_space'
9
+ require 'vector_space/simple_vector'
10
+
11
+ require 'vector_space/index_reflection'
12
+ require 'vector_space/family'
13
+ require 'vector_space/simple_indexed_vector'
@@ -0,0 +1,33 @@
1
+ module VectorSpace
2
+ # These operations depend only upon addition and multiplication of the underlying values, so work in a vector space
3
+ module Arithmetic
4
+ # Vector addition
5
+ def +(vector)
6
+ if compatible_with?(vector)
7
+ operate_on_values(vector) { |a, b| a + b }
8
+ else
9
+ raise ArgumentError, "can't add #{vector.inspect} to #{self.inspect}"
10
+ end
11
+ end
12
+
13
+ # Scalar multiplication
14
+ def *(n)
15
+ operate_on_values { |value| value * n }
16
+ end
17
+
18
+ # Additive inverse
19
+ def -@
20
+ operate_on_values { |value| -value }
21
+ end
22
+
23
+ # Vector subtraction (by addition)
24
+ def -(vector)
25
+ self + (-vector)
26
+ end
27
+
28
+ # Scalar division (by multiplication)
29
+ def /(n)
30
+ self * (1.0 / n)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ module VectorSpace
2
+ class ComponentReflection
3
+ def initialize(component, options = {})
4
+ @component, @options = component, options
5
+ end
6
+
7
+ def to_s
8
+ "#{component} component (#{default.class})"
9
+ end
10
+
11
+ attr_reader :component, :options
12
+
13
+ def getter
14
+ options[:getter] || component.to_sym
15
+ end
16
+
17
+ def value(value)
18
+ value ||= default
19
+
20
+ if options[:coerce]
21
+ options[:coerce].to_proc.call(value)
22
+ else
23
+ value
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ module VectorSpace
2
+ class DimensionReflection < ComponentReflection
3
+ def zero
4
+ @options[:zero] || 0
5
+ end
6
+
7
+ def default
8
+ zero
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,45 @@
1
+ module VectorSpace
2
+ module Family
3
+ def self.included(base)
4
+ base.class_eval do
5
+ include VectorSpace
6
+ extend ClassMethods
7
+ include InstanceMethods
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ attr_reader :index
13
+
14
+ def indexed_by(index, options = {})
15
+ @index = index
16
+ (@component_reflections ||= {})[index] = IndexReflection.new(index, options)
17
+ end
18
+
19
+ def components
20
+ [index] + super
21
+ end
22
+
23
+ def zero(attributes = {})
24
+ new Hash[*([index, attributes[index] || reflect_on_component(index).default] + dimensions.map { |dimension| [dimension, reflect_on_component(dimension).zero] }).flatten]
25
+ end
26
+ end
27
+
28
+ module InstanceMethods
29
+ def self.included(base)
30
+ base.class_eval do
31
+ def_delegator :'self.class', :index
32
+ end
33
+ end
34
+
35
+ def compatible_with?(other)
36
+ super && project(index) == other.project(index)
37
+ end
38
+
39
+ private
40
+ def operate_on_values(*vectors, &block)
41
+ self.class.new Hash[*(map_components(index, *vectors) { |*values| values.inject { |a, b| a if a == b } } + map_components(dimensions, *vectors, &block)).flatten]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,7 @@
1
+ module VectorSpace
2
+ class IndexReflection < ComponentReflection
3
+ def default
4
+ @options[:default]
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ module VectorSpace
2
+ module PartialOrder
3
+ # Ruby's Comparable isn't well-behaved for partial orders: its operations raise ArgumentError if #<=> returns nil.
4
+ # This module provides operations which simply return false when two values are incomparable.
5
+
6
+ [:<, :<=, :==, :>=, :>].each do |operation|
7
+ define_method operation do |other|
8
+ ((result = self <=> other) && result.send(operation, 0)) == true
9
+ end
10
+ end
11
+
12
+ def comparable_with?(other)
13
+ !(self <=> other).nil?
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ module VectorSpace
2
+ class SimpleIndexedVector < SimpleVector
3
+ include Family
4
+
5
+ def initialize(attributes = {})
6
+ if index_value = attributes[index] || reflect_on_component(index).default
7
+ super attributes.merge(index => index_value)
8
+ else
9
+ raise ArgumentError, "must provide value for #{index} index or configure a default"
10
+ end
11
+ end
12
+
13
+ private
14
+ def describe_value(dimension)
15
+ if describe = reflect_on_component(dimension).options[:describe]
16
+ describe.to_proc.call(project(index), project(dimension))
17
+ else
18
+ super
19
+ end
20
+ end
21
+
22
+ class << self
23
+ def indexed_by(index, attributes = {})
24
+ define_method (reflection = super(index, attributes)).getter do
25
+ reflection.value(@attributes[index])
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,70 @@
1
+ module VectorSpace
2
+ class SimpleVector
3
+ include Enumerable
4
+ include VectorSpace
5
+ def_delegators :'self.class', :description_prefix, :description_suffix, :description_separator, :zero_description
6
+
7
+ def initialize(attributes = {})
8
+ @attributes = attributes
9
+ end
10
+
11
+ def to_s
12
+ if zero? && zero_description
13
+ zero_description
14
+ else
15
+ [
16
+ description_prefix,
17
+ dimensions.map { |dimension| describe_value(dimension) }.compact.join(description_separator),
18
+ description_suffix
19
+ ].join
20
+ end
21
+ end
22
+
23
+ def [](component)
24
+ project(component)
25
+ end
26
+
27
+ def each
28
+ dimensions.each do |dimension| yield [dimension, project(dimension)] end
29
+ end
30
+
31
+ private
32
+ def describe_value(dimension)
33
+ if describe = reflect_on_component(dimension).options[:describe]
34
+ describe.to_proc.call(project(dimension))
35
+ else
36
+ project(dimension)
37
+ end
38
+ end
39
+
40
+ class << self
41
+ attr_reader :description_prefix, :description_suffix, :zero_description
42
+
43
+ def has_dimension(dimension, attributes = {})
44
+ define_method (reflection = super(dimension, attributes)).getter do
45
+ reflection.value(@attributes[dimension])
46
+ end
47
+ end
48
+
49
+ def has_description_prefix(description_prefix)
50
+ @description_prefix = description_prefix
51
+ end
52
+
53
+ def has_description_suffix(description_suffix)
54
+ @description_suffix = description_suffix
55
+ end
56
+
57
+ def has_description_separator(description_separator)
58
+ @description_separator = description_separator
59
+ end
60
+
61
+ def has_zero_description(zero_description)
62
+ @zero_description = zero_description
63
+ end
64
+
65
+ def description_separator
66
+ @description_separator || ' and '
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,101 @@
1
+ module VectorSpace
2
+ def self.included(base)
3
+ base.class_eval do
4
+ extend ClassMethods
5
+ include InstanceMethods
6
+ end
7
+ end
8
+
9
+ module ClassMethods
10
+ attr_reader :dimensions
11
+
12
+ def components
13
+ dimensions
14
+ end
15
+
16
+ def reflect_on_component(component)
17
+ @component_reflections[component]
18
+ end
19
+
20
+ def zero
21
+ new Hash[*dimensions.map { |dimension| [dimension, reflect_on_component(dimension).zero] }.flatten]
22
+ end
23
+
24
+ def order
25
+ @order || :product
26
+ end
27
+
28
+ private
29
+ def has_dimension(dimension, options = {})
30
+ (@dimensions ||= []) << dimension
31
+ (@component_reflections ||= {})[dimension] = DimensionReflection.new(dimension, options)
32
+ end
33
+
34
+ def has_order(order)
35
+ @order = order
36
+ end
37
+ end
38
+
39
+ module InstanceMethods
40
+ def self.included(base)
41
+ base.class_eval do
42
+ extend Forwardable
43
+ def_delegators :'self.class', :components, :dimensions, :order, :reflect_on_component
44
+
45
+ include PartialOrder
46
+ include Arithmetic
47
+ end
48
+ end
49
+
50
+ def project(component)
51
+ reflection = reflect_on_component(component)
52
+ reflection.value(send(reflection.getter))
53
+ end
54
+
55
+ def map_components(components, *vectors)
56
+ Array(components).map { |component| [component, yield(*([self] + vectors).map { |vector| vector.project(component) })] }
57
+ end
58
+
59
+ def map_values(components, *vectors, &block)
60
+ map_components(components, *vectors, &block).map { |component, value| value }
61
+ end
62
+
63
+ def zero?
64
+ map_values(dimensions) { |value| value.zero? }.all?
65
+ end
66
+
67
+ # Decides whether the receiver is compatible with some other object for general operations (e.g. arithmetic and comparison).
68
+ def compatible_with?(other)
69
+ other.class == self.class
70
+ end
71
+
72
+ # Compares the receiver against another object, returning -1, 0, +1 or nil depending on whether the receiver is less than,
73
+ # equal to, greater than, or incomparable with the other object.
74
+ # This works just like the #<=> expected by Comparable, but also returns nil iff the two objects are incomparable.
75
+ def <=>(other)
76
+ if compatible_with?(other)
77
+ comparisons = map_values(dimensions, other) { |a, b| a <=> b }
78
+
79
+ case order
80
+ when :product
81
+ comparisons.inject { |a, b| a + b if a && b && a * b >= 0 }
82
+ when :lexicographic
83
+ comparisons.inject { |a, b| a == 0 ? b : a }
84
+ end
85
+ end
86
+ end
87
+
88
+ def eql?(other)
89
+ map_values(dimensions, other) { |a, b| a.eql?(b) }.all?
90
+ end
91
+
92
+ def hash
93
+ map_values(dimensions) { |value| value }.hash
94
+ end
95
+
96
+ private
97
+ def operate_on_values(*vectors, &block)
98
+ self.class.new Hash[*map_components(dimensions, *vectors, &block).flatten]
99
+ end
100
+ end
101
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vector_space
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Tom Stuart
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-23 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: tom@experthuman.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - lib/vector_space/arithmetic.rb
26
+ - lib/vector_space/component_reflection.rb
27
+ - lib/vector_space/dimension_reflection.rb
28
+ - lib/vector_space/family.rb
29
+ - lib/vector_space/index_reflection.rb
30
+ - lib/vector_space/partial_order.rb
31
+ - lib/vector_space/simple_indexed_vector.rb
32
+ - lib/vector_space/simple_vector.rb
33
+ - lib/vector_space/vector_space.rb
34
+ - lib/vector_space.rb
35
+ - LICENSE
36
+ - README.markdown
37
+ - TODO
38
+ has_rdoc: true
39
+ homepage: http://github.com/tomstuart/vector_space
40
+ licenses: []
41
+
42
+ post_install_message:
43
+ rdoc_options: []
44
+
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.3.5
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: A Ruby library for treating multidimensional values as elements of a vector space
66
+ test_files: []
67
+