vector_number 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ class VectorNumber
4
+ # @group Similarity measures
5
+
6
+ # Calculate cosine between this vector and +other+.
7
+ #
8
+ # Cosine can be used as a measure of similarity.
9
+ #
10
+ # @example
11
+ # v = VectorNumber[2, "a"]
12
+ # v.cosine(v) # => 1.0
13
+ # v.cosine_similarity(1) # => 0.8944271909999159
14
+ # v.cosine("b") # => 0.0
15
+ # v.cosine_similarity(-v) # => -1.0
16
+ # v.cosine(0) # ZeroDivisionError
17
+ # VectorNumber[0].cosine(v) # ZeroDivisionError
18
+ #
19
+ # @see #angle
20
+ #
21
+ # @param other [VectorNumber, Any]
22
+ # @return [Numeric]
23
+ # @raise [ZeroDivisionError] if either +self+ or +other+ is a zero vector
24
+ #
25
+ # @since 0.7.0
26
+ def cosine(other)
27
+ has_direction?
28
+ return 1.0 if equal?(other)
29
+
30
+ other = new([other]) unless VectorNumber === other
31
+ has_direction?(other)
32
+ return 0.0 if (product = dot_product(other)).zero?
33
+
34
+ # Due to precision errors, the result might be slightly outside [-1, 1], so we clamp.
35
+ (product / magnitude / other.magnitude).clamp(-1.0, 1.0)
36
+ end
37
+
38
+ # @since 0.7.0
39
+ alias cosine_similarity cosine
40
+
41
+ # Calculate Jaccard index of similarity between this vector and +other+.
42
+ #
43
+ # This measure is binary: it considers only sets of non-zero dimensions in vectors,
44
+ # ignoring coefficients.
45
+ #
46
+ # @example
47
+ # v = VectorNumber[2, "a"]
48
+ # v.jaccard_index(v) # => (1/1)
49
+ # v.jaccard_index(1) # => (1/2)
50
+ # v.jaccard_index("b") # => (0/1)
51
+ # v.jaccard_index(-v) # => (1/1)
52
+ # v.jaccard_index(0) # => (0/1)
53
+ # VectorNumber[0].jaccard_index(v) # => (0/1)
54
+ # VectorNumber[0].jaccard_index(0) # ZeroDivisionError
55
+ #
56
+ # @see #jaccard_similarity
57
+ #
58
+ # @param other [VectorNumber, Any]
59
+ # @return [Rational]
60
+ # @raise [ZeroDivisionError] if both vectors are zero vectors
61
+ #
62
+ # @since 0.7.0
63
+ def jaccard_index(other)
64
+ other = new([other]) unless VectorNumber === other
65
+ intersection = units.intersection(other.units)
66
+ Rational(intersection.size, size + other.size - intersection.size)
67
+ end
68
+
69
+ # Calculate weighted Jaccard similarity index between this vector and +other+.
70
+ #
71
+ # This measure only makes sense for non-negative vectors.
72
+ #
73
+ # @example
74
+ # v = VectorNumber[2, "a"]
75
+ # v.jaccard_similarity(v) # => (1/1)
76
+ # v.jaccard_similarity(1) # => (1/3)
77
+ # v.jaccard_similarity("b") # => (0/1)
78
+ # v.jaccard_similarity(-v) # => (-1/1)
79
+ # v.jaccard_similarity(0) # => (0/1)
80
+ # VectorNumber[0].jaccard_similarity(v) # => (0/1)
81
+ # VectorNumber[0].jaccard_similarity(0) # ZeroDivisionError
82
+ #
83
+ # @see #jaccard_index
84
+ #
85
+ # @param other [VectorNumber, Any]
86
+ # @return [Rational]
87
+ # @raise [ZeroDivisionError] if both vectors are zero vectors
88
+ #
89
+ # @since 0.7.0
90
+ def jaccard_similarity(other)
91
+ other = new([other]) unless VectorNumber === other
92
+ Rational(
93
+ @data.sum { |u, c| [c, other[u]].min },
94
+ units.union(other.units).sum { |u| [self[u], other[u]].max }
95
+ )
96
+ end
97
+ end
@@ -1,20 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class VectorNumber
4
- # Class for representing special units.
4
+ # Class for representing special (unique) units.
5
5
  #
6
- # The public API consists of:
7
- # - +#==+/+#eql?+/+#equal?+ (from +Object+)
8
- # - +#hash+ (from +Object+)
9
- # - {#to_s}
10
- # - {#inspect}
6
+ # There usually isn't much point in using these, aside from {VectorNumber::NUMERIC_UNITS}.
7
+ # However, they can be helpful to denote units that aren't equal to any other object.
8
+ #
9
+ # +VectorNumber#to_s+ (and +inspect+) will use {#to_s} text instead of {#inspect} text
10
+ # for these units by default, without a multiplication operator. However, consider
11
+ # using customization of +VectorNumber#to_s+ instead if this is what you want.
11
12
  #
12
13
  # @since 0.6.0
13
14
  class SpecialUnit
14
- # @api private
15
+ # Returns a new, unique unit, not equal to any other unit.
16
+ #
15
17
  # @param unit [#to_s] name for {#inspect}
16
18
  # @param text [String] text for {#to_s}
17
- def initialize(unit, text)
19
+ def initialize(unit, text = unit.to_s)
18
20
  @unit = unit
19
21
  @text = text
20
22
  end
@@ -26,6 +28,16 @@ class VectorNumber
26
28
  @text
27
29
  end
28
30
 
31
+ # Support for PP, outputs the same text as {#inspect}.
32
+ #
33
+ # @param pp [PP]
34
+ # @return [void]
35
+ #
36
+ # @since 0.7.0
37
+ def pretty_print(pp)
38
+ pp.text(inspect)
39
+ end
40
+
29
41
  # Get string representation of the unit for debugging.
30
42
  #
31
43
  # @return [String]
@@ -15,29 +15,30 @@ class VectorNumber
15
15
 
16
16
  # @group Miscellaneous methods
17
17
 
18
- # Return string representation of the vector.
18
+ # Return string representation of the vector suitable for output.
19
19
  #
20
20
  # An optional block can be supplied to provide customized substrings
21
21
  # for each unit and coefficient pair.
22
22
  # Care needs to be taken in handling +VectorNumber::R+ and +VectorNumber::I+ units.
23
- # {.numeric_unit?} can be used to check if a particular unit needs special handling.
23
+ # {.numeric_unit?}/{.special_unit?} can be used to check if a particular unit
24
+ # requires different logic.
24
25
  #
25
26
  # @example
26
27
  # VectorNumber[5, "s"].to_s # => "5 + 1⋅\"s\""
27
28
  # VectorNumber["s", 5].to_s # => "1⋅\"s\" + 5"
28
29
  # @example with :mult argument
29
- # VectorNumber[5, :s].to_s(mult: :asterisk) # => "5 + 1*s"
30
- # (-VectorNumber[5, :s]).to_s(mult: "~~~") # => "-5 - 1~~~s"
30
+ # VectorNumber[5, :s].to_s(mult: :asterisk) # => "5 + 1*:s"
31
+ # (-VectorNumber[5, :s]).to_s(mult: "~~~") # => "-5 - 1~~~:s"
31
32
  # @example with a block
32
33
  # VectorNumber[5, :s].to_s { |k, v| "#{format("%+.0f", v)}%#{k}" } # => "+5%1+1%s"
33
34
  # VectorNumber[5, :s].to_s(mult: :cross) { |k, v, i, op|
34
- # "#{',' unless i.zero?}#{v}#{op+k.to_s unless k == VectorNumber::R}"
35
- # } # => "5,1×s"
35
+ # "#{',' unless i.zero?}#{v}#{op+k.inspect unless k == VectorNumber::R}"
36
+ # } # => "5,1×:s"
36
37
  #
37
38
  # @param mult [Symbol, String]
38
39
  # text to use between coefficient and unit,
39
40
  # can be one of the keys in {MULT_STRINGS} or an arbitrary string
40
- # @yieldparam unit [Object]
41
+ # @yieldparam unit [Any]
41
42
  # @yieldparam coefficient [Numeric]
42
43
  # @yieldparam index [Integer]
43
44
  # @yieldparam operator [String]
@@ -46,27 +47,57 @@ class VectorNumber
46
47
  # @raise [ArgumentError]
47
48
  # if +mult+ is not a String and is not in {MULT_STRINGS}'s keys
48
49
  def to_s(mult: :dot, &block)
49
- if !mult.is_a?(String) && !MULT_STRINGS.key?(mult)
50
+ if !(String === mult) && !MULT_STRINGS.key?(mult)
50
51
  raise ArgumentError, "unknown key #{mult.inspect}", caller
51
52
  end
52
53
  return "0" if zero?
53
54
 
54
- operator = mult.is_a?(String) ? mult : MULT_STRINGS[mult]
55
+ operator = (String === mult) ? mult : MULT_STRINGS[mult]
55
56
  build_string(operator, &block)
56
57
  end
57
58
 
58
- # Return string representation of the vector.
59
+ # Return string representation of the vector suitable for display.
59
60
  #
60
- # This is similar to +Complex#inspect+: it returns result of {#to_s} in round brackets.
61
+ # This is similar to +Complex#inspect+ it returns result of {#to_s} in round brackets.
61
62
  #
62
63
  # @example
63
- # VectorNumber[5, :s].inspect # => "(5 + 1s)"
64
+ # VectorNumber[5, :s].inspect # => "(5 + 1⋅:s)"
64
65
  #
65
66
  # @return [String]
66
67
  #
67
68
  # @see to_s
68
69
  def inspect
69
- "(#{self})"
70
+ return "(0)" if zero?
71
+
72
+ "(#{build_string("⋅")})"
73
+ end
74
+
75
+ # Support for PP, usually outputs the same text as {#inspect}.
76
+ #
77
+ # @param pp [PP]
78
+ # @return [void]
79
+ #
80
+ # @since 0.7.0
81
+ def pretty_print(pp) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
82
+ pp.text("(0)") and return if zero?
83
+
84
+ pp.group(1, "(", ")") do
85
+ # This should use `pp.fill_breakable`, but PrettyPrint::SingleLine haven't had it.
86
+ pp.seplist(@data, -> { fill_breakable(pp) }, :each_with_index) do |(unit, coefficient), i|
87
+ pp.text("-") if coefficient.negative?
88
+ unless i.zero?
89
+ pp.text("+") if coefficient.positive?
90
+ fill_breakable(pp)
91
+ end
92
+ pp.pp(coefficient.abs)
93
+ if SpecialUnit === unit
94
+ pp.text(unit.to_s)
95
+ else
96
+ pp.text("⋅")
97
+ pp.pp(unit)
98
+ end
99
+ end
100
+ end
70
101
  end
71
102
 
72
103
  private
@@ -90,16 +121,19 @@ class VectorNumber
90
121
  result
91
122
  end
92
123
 
93
- # @param unit [Object]
124
+ # @param unit [Any]
94
125
  # @param coefficient [Numeric]
95
126
  # @param operator [String]
96
127
  # @return [String]
97
128
  def value_to_s(unit, coefficient, operator)
98
- if NUMERIC_UNITS.include?(unit)
99
- "#{coefficient}#{unit}"
129
+ if SpecialUnit === unit
130
+ "#{coefficient.inspect}#{unit}"
100
131
  else
101
- unit = unit.inspect if unit.is_a?(String)
102
- "#{coefficient}#{operator}#{unit}"
132
+ "#{coefficient.inspect}#{operator}#{unit.inspect}"
103
133
  end
104
134
  end
135
+
136
+ def fill_breakable(pp)
137
+ pp.group { pp.breakable }
138
+ end
105
139
  end