Class: RVGP::Journal::Commodity

Inherits:
Object
  • Object
show all
Defined in:
lib/rvgp/journal/commodity.rb

Overview

This abstraction defines a simple commodity entry, as would be found in a pta journal. Such commodities can appear in the form of currency, such as ‘$ 1.30’ or in any other format that hledger and ledger parse. ie ‘1 HOUSE’.

There’s a lot of additional functionality provided by this class, including math related helper functions.

NOTE: the easiest way to create a commodity in your code, is by way of the provided String#to_commodity method. Such as: ‘$ 1.30’.to_commodity.

Units of a commodity are stored in int’s, with precision. This ensures that there is no potential for floating point precision errors, affecting these commodities.

A number of constants, relating to the built-in support of various currencies, are available as part of RVGP, in the form of the iso-4217-currencies.json file. Which, is loaded automatically during initialization.

Defined Under Namespace

Classes: ConversionError, UnimplementedError

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(code, alphabetic_code, quantity, precision) ⇒ Commodity

Create a commodity, from the constituent parts

Parameters:



69
70
71
72
73
74
# File 'lib/rvgp/journal/commodity.rb', line 69

def initialize(code, alphabetic_code, quantity, precision)
  @code = code
  @alphabetic_code = alphabetic_code
  @quantity = quantity.to_i
  @precision = precision.to_i
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(attr, rvalue) ⇒ RVGP::Journal::Commodity

If an unhandled methods is encountered between ourselves, and another commodity, we dispatch that method to the quantity of self, against the quantity of the provided commodity.

Returns A new Commodity object, created using our code, and the resulting quantity.

Parameters:

Returns:



320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/rvgp/journal/commodity.rb', line 320

def method_missing(name, *args, &blk)
  # This handles most all of the numeric methods
  if @quantity.respond_to?(name) && args.length == 1 && args[0].is_a?(self.class)
    assert_commodity args[0]

    unless commodity.precision == precision
      raise UnimplementedError, format('Unimplemented operation %s Wot do?', name.inspect)
    end

    RVGP::Journal::Commodity.new code, alphabetic_code, @quantity.send(name, args[0].quantity, &blk), precision
  else
    super
  end
end

Instance Attribute Details

#alphabetic_codeString

The ISO-4217 ‘Alphabetic Code’ of this commodity. This code is used for various non-rendering functions. (Equality testing, Conversion lookups…)

Returns:

  • (String)

    the current value of alphabetic_code



34
35
36
# File 'lib/rvgp/journal/commodity.rb', line 34

def alphabetic_code
  @alphabetic_code
end

#codeString

The code of this commodity. Which, may be the same as :alphabetic_code, or, may take the form of symbol. (ie ‘$’). This code is used to render the commodity to strings.

Returns:

  • (String)

    the current value of code



34
35
36
# File 'lib/rvgp/journal/commodity.rb', line 34

def code
  @code
end

#precisionInteger

The exponent of the characteristic, which is used to separate the mantissa from the significand.

Returns:

  • (Integer)

    the current value of precision



34
35
36
# File 'lib/rvgp/journal/commodity.rb', line 34

def precision
  @precision
end

#quantityInteger

The number of units, of this currency, before applying a fractional representation (Ie “$ 2.89” is stored in the form of :quantity 289)

Returns:

  • (Integer)

    the current value of quantity



34
35
36
# File 'lib/rvgp/journal/commodity.rb', line 34

def quantity
  @quantity
end

Class Method Details

.from_s(str) ⇒ RVGP::Journal::Commodity

Given a string, such as “$ 20.57”, or “1 MERCEDESBENZ”, Construct and return a commodity representation

Parameters:

  • str (String)

    The commodity, as would be found in a PTA journal

Returns:



338
339
340
# File 'lib/rvgp/journal/commodity.rb', line 338

def self.from_s(str)
  commodity_parts_from_string str
end

.from_symbol_and_amount(symbol, amount = 0) ⇒ RVGP::Journal::Commodity

Given a code, or symbol, and a quantity - Construct and return a commodity representation.

Parameters:

  • symbol (String)

    The commodity code, or symbol, as would be found in a PTA journal

  • amount (Integer, String) (defaults to: 0)

    The commodity quantity. If this is a string, we search for periods, and calculate precision. If this is an int, we assume a precision based on the commodity code.

Returns:



357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/rvgp/journal/commodity.rb', line 357

def self.from_symbol_and_amount(symbol, amount = 0)
  currency = RVGP::Journal::Currency.from_code_or_symbol symbol
  precision, quantity = *precision_and_quantity_from_amount(amount)
  #   NOTE: Sometimes (say shares) we deal with fractions of a penny. If this
  #   is such a case, we preserve the larger precision
  if currency && currency.minor_unit > precision
    # This is a case where, say "$ 1" is passed. But, we want to store that
    # as 100
    quantity *= 10**(currency.minor_unit - precision)
    precision = currency.minor_unit
  end

  new symbol, currency ? currency.alphabetic_code : symbol, quantity, precision
end

Instance Method Details

#!=(rvalue) ⇒ TrueClass, FalseClass

Ensure that rvalue is a commodity. Then return a boolean indicating whether self.quantity is not equal to rvalue’s quantity.

Parameters:

Returns:

  • (TrueClass, FalseClass)

    Result of comparison.



217
218
219
220
221
222
223
224
225
# File 'lib/rvgp/journal/commodity.rb', line 217

%i[> < <=> >= <= == !=].each do |operation|
  define_method(operation) do |rvalue|
    assert_commodity rvalue

    lquantity, rquantity = quantities_denominated_against rvalue

    lquantity.send operation, rquantity
  end
end

#*(rvalue) ⇒ RVGP::Journal::Commodity

If the rvalue is a commodity, assert that we share the same commodity code, and if so multiple our quantity by the rvalue quantity. If rvalue is numeric, multiply our quantity by this numeric.

Parameters:

Returns:



# File 'lib/rvgp/journal/commodity.rb', line 227

#+(rvalue) ⇒ RVGP::Journal::Commodity

If the rvalue is a commodity, assert that we share the same commodity code, and if so sum our quantity with the rvalue quantity.

Parameters:

Returns:



# File 'lib/rvgp/journal/commodity.rb', line 256

#-(rvalue) ⇒ RVGP::Journal::Commodity

If the rvalue is a commodity, assert that we share the same commodity code, and if so subtract the rvalue quantity from our quantity.

Parameters:

Returns:



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/rvgp/journal/commodity.rb', line 267

%i[+ -].each do |operation|
  define_method(operation) do |rvalue|
    assert_commodity rvalue

    lquantity, rquantity, dprecision = quantities_denominated_against rvalue

    result = lquantity.send operation, rquantity

    # Adjust the dprecision. Probably there's a better way to do this, but,
    # this works
    our_currency = RVGP::Journal::Currency.from_code_or_symbol code

    # This is a special case:
    return RVGP::Journal::Commodity.new code, alphabetic_code, result, our_currency.minor_unit if result.zero?

    # If we're trying to remove more digits than minor_unit, we have to adjust
    # our cut
    if our_currency && (dprecision > our_currency.minor_unit) && /\A.+?(0+)\Z/.match(result.to_s) &&
       ::Regexp.last_match(1)
      trim_length = ::Regexp.last_match(1).length
      dprecision -= trim_length

      if dprecision < our_currency.minor_unit
        add = our_currency.minor_unit - dprecision
        dprecision += add
        trim_length -= add
      end

      result /= 10**trim_length
    end

    RVGP::Journal::Commodity.new code, alphabetic_code, result, dprecision
  end
end

#/(rvalue) ⇒ RVGP::Journal::Commodity

If the rvalue is a commodity, assert that we share the same commodity code, and if so divide our quantity by the rvalue quantity. If rvalue is numeric, divide our quantity by this numeric.

Parameters:

Returns:



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/rvgp/journal/commodity.rb', line 240

%i[* /].each do |operation|
  define_method(operation) do |rvalue|
    result = if rvalue.is_a? Numeric
               # These mul/divs are often "Divide by half" "Multiply by X" instructions
               # for which the rvalue is not, and should not be, a commodity.
               quantity_as_bigdecimal.send operation, rvalue
             else
               assert_commodity rvalue

               raise UnimplementedError
             end

    RVGP::Journal::Commodity.from_symbol_and_amount code, result.round(MAX_DECIMAL_DIGITS).to_s('F')
  end
end

#<(rvalue) ⇒ TrueClass, FalseClass

Ensure that rvalue is a commodity. Then return a boolean indicating whether self.quantity is less than rvalue’s quantity.

Parameters:

Returns:

  • (TrueClass, FalseClass)

    Result of comparison.



# File 'lib/rvgp/journal/commodity.rb', line 182

#<=(rvalue) ⇒ TrueClass, FalseClass

Ensure that rvalue is a commodity. Then return a boolean indicating whether self.quantity is less than or equal to rvalue’s quantity.

Parameters:

Returns:

  • (TrueClass, FalseClass)

    Result of comparison.



# File 'lib/rvgp/journal/commodity.rb', line 200

#<=>(rvalue) ⇒ Integer

Ensure that rvalue is a commodity. Then returns an integer indicating whether self.quantity is (spaceship) rvalue’s quantity. More specifically: -1 on <, 0 on ==, 1 on >.

Parameters:

Returns:

  • (Integer)

    Result of comparison: -1, 0, or 1.



# File 'lib/rvgp/journal/commodity.rb', line 188

#==(rvalue) ⇒ TrueClass, FalseClass

Ensure that rvalue is a commodity. Then return a boolean indicating whether self.quantity is equal to rvalue’s quantity.

Parameters:

Returns:

  • (TrueClass, FalseClass)

    Result of comparison.



# File 'lib/rvgp/journal/commodity.rb', line 206

#>(rvalue) ⇒ TrueClass, FalseClass

Ensure that rvalue is a commodity. Then return a boolean indicating whether self.quantity is greater than rvalue’s quantity.

Parameters:

Returns:

  • (TrueClass, FalseClass)

    Result of comparison.



# File 'lib/rvgp/journal/commodity.rb', line 176

#>=(rvalue) ⇒ TrueClass, FalseClass

Ensure that rvalue is a commodity. Then return a boolean indicating whether self.quantity is greater than or equal to rvalue’s quantity.

Parameters:

Returns:

  • (TrueClass, FalseClass)

    Result of comparison.



# File 'lib/rvgp/journal/commodity.rb', line 194

#absRVGP::Journal::Commodity

Returns a copy of the current Commodity, with the absolute value of quanity.

Returns:



158
159
160
# File 'lib/rvgp/journal/commodity.rb', line 158

def abs
  RVGP::Journal::Commodity.new code, alphabetic_code, quantity.abs, precision
end

#coerce(other) ⇒ Object

We’re mostly/only using this to support [].sum atm



303
304
305
306
307
# File 'lib/rvgp/journal/commodity.rb', line 303

def coerce(other)
  super unless other.is_a? Integer

  [RVGP::Journal::Commodity.new(code, alphabetic_code, other, precision), self]
end

#floor(to_digit) ⇒ RVGP::Journal::Commodity

This method returns a new Commodity, with :floor applied to its :quantity.

Parameters:

  • to_digit (Integer)

    Which digit to floor to

Returns:



165
166
167
# File 'lib/rvgp/journal/commodity.rb', line 165

def floor(to_digit)
  round_or_floor to_digit, :floor
end

#invert!RVGP::Journal::Commodity

Multiply the quantity by -1. This mutates the state of self.

Returns:



151
152
153
154
# File 'lib/rvgp/journal/commodity.rb', line 151

def invert!
  @quantity *= -1
  self
end

#negative?TrueClass, FalseClass

Returns whether or not the quantity is less than zero.

Returns:

  • (TrueClass, FalseClass)

    yes or no



145
146
147
# File 'lib/rvgp/journal/commodity.rb', line 145

def negative?
  quantity.negative?
end

#positive?TrueClass, FalseClass

Returns whether or not the quantity is greater than zero.

Returns:

  • (TrueClass, FalseClass)

    yes or no



139
140
141
# File 'lib/rvgp/journal/commodity.rb', line 139

def positive?
  quantity.positive?
end

#quantity_as_bigdecimalBigDecimal

Returns the quantity component of the commodity, as a BigDecimal

Returns:

  • (BigDecimal)


101
102
103
# File 'lib/rvgp/journal/commodity.rb', line 101

def quantity_as_bigdecimal
  BigDecimal quantity_as_s
end

#quantity_as_decimal_pairArray<Integer>

This returns the characteristic and mantissa for our quantity, given our precision, note that we do not return the +/- signage. That information is destroyed here

Returns:

  • (Array<Integer>)

    A two-value array, with characteristic at [0], and fraction at [1]



108
109
110
111
# File 'lib/rvgp/journal/commodity.rb', line 108

def quantity_as_decimal_pair
  characteristic = quantity.abs.to_i / (10**precision)
  [characteristic, quantity.abs.to_i - (characteristic * (10**precision))]
end

#quantity_as_s(options = {}) ⇒ String

Render the :quantity, to a string. This is output without code notation, and merely expressed the quantity with the expected symbols (commas, periods) .

Parameters:

  • options (Hash) (defaults to: {})

    formatting specifiers, affecting what output is produced

Options Hash (options):

  • precision (Integer)

    Use the provided precision, instead of the :precision accessor

  • commatize (TrueClass, FalseClass) — default: false

    Whether or not to insert commas in the output, between every three digits, in the characteristic

Returns:

  • (String)

    The formatted quantity



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/rvgp/journal/commodity.rb', line 83

def quantity_as_s(options = {})
  characteristic, mantissa = if options.key? :precision
                               round(options[:precision]).quantity_as_decimal_pair
                             else
                               quantity_as_decimal_pair
                             end

  characteristic = characteristic.to_s
  to_precision = options[:precision] || precision
  mantissa = to_precision.positive? ? format("%0#{to_precision}d", mantissa) : nil

  characteristic = characteristic.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse if options[:commatize]

  [negative? ? '-' : nil, characteristic, mantissa ? '.' : nil, mantissa].compact.join
end

#respond_to_missing?(name, _include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


309
310
311
# File 'lib/rvgp/journal/commodity.rb', line 309

def respond_to_missing?(name, _include_private = false)
  @quantity.respond_to? name
end

#round(to_digit) ⇒ RVGP::Journal::Commodity

This method returns a new Commodity, with :round applied to its :quantity.

Parameters:

  • to_digit (Integer)

    Which digit to round to

Returns:



172
173
174
# File 'lib/rvgp/journal/commodity.rb', line 172

def round(to_digit)
  round_or_floor to_digit, :round
end

#to_fFloat

Returns the quantity component of the commodity, after being adjusted for :precision, as a Float. Consider using #quantity_as_bigdecimal instead.

Returns:

  • (Float)


133
134
135
# File 'lib/rvgp/journal/commodity.rb', line 133

def to_f
  quantity_as_s.to_f
end

#to_s(options = {}) ⇒ String

Render the commodity to a string, in the form it would appear in a journal. This output includes the commodity code, as well as a period and, optionally commas.

Parameters:

  • options (Hash) (defaults to: {})

    formatting specifiers, affecting what output is produced

Options Hash (options):

  • precision (Integer)

    Use the provided precision, instead of the :precision accessor

  • commatize (TrueClass, FalseClass) — default: false

    Whether or not to insert commas in the output, between every three digits, in the characteristic

  • no_code (TrueClass, FalseClass) — default: false

    If true, the code is omitted in the output

Returns:

  • (String)

    The formatted quantity



121
122
123
124
125
126
127
128
# File 'lib/rvgp/journal/commodity.rb', line 121

def to_s(options = {})
  ret = [quantity_as_s(options)]
  if code && !options[:no_code]
    operand = code.count(' ').positive? ? ['"', code, '"'].join : code
    code.length == 1 ? ret.unshift(operand) : ret.push(operand)
  end
  ret.join(' ')
end