Module: WhittakerTech::Midas::Coin::Presenter

Included in:
WhittakerTech::Midas::Coin
Defined in:
app/models/whittaker_tech/midas/coin/presenter.rb

Overview

Presenter provides a strftime-like token grammar for rendering Coin values.

Design principles

  • Declarative — tokens map directly to named handler methods.
  • Pure — no mutation, rounding, or currency conversion.
  • Direction-safe — all text is wrapped with Unicode bidirectional isolation markers via Coin::Bidi to prevent display corruption in mixed LTR/RTL contexts.

Token reference

Patterns are strings containing %x tokens (similar to strftime).

Tokens:

  • %t: Formatted total (symbol + amount), e.g. $29.99
  • %m: Minor units (raw integer), e.g. 2999
  • %M: Major units (decimal string), e.g. 29.99
  • %c: Currency code, e.g. USD
  • %s: Currency symbol, e.g. $
  • %n: Number only (no symbol), e.g. 29.99
  • %u: Custom units label (see opts), e.g. per kg
  • %p: Custom per-exact label (see opts), e.g. 0.2997
  • %~: Approximate marker ( or empty)
  • %%: Literal percent sign

Usage

coin = Coin.value(2999, 'USD')
coin.present('%s%M %c')           # => "$29.99 USD"
coin.present('%~%t', approx: true) # => "≈$29.99"

Options

Pass keyword options to present / format to populate optional tokens:

  • approx: [Boolean] — if true, ~ expands to ; otherwise empty string.
  • units: [String] — value for %u token.
  • per_exact: [String] — value for %p token.
  • currency_dir: [Symbol] — override the currency display direction (:ltr or :rtl); defaults to the value from WhittakerTech::Midas.currency_direction_for.

Since:

  • 0.1.0

Constant Summary collapse

TOKEN_MAP =

Registry of recognised tokens and their handler method names.

Returns:

  • (Hash{String => Symbol})

Since:

  • 0.1.0

{
  '%' => :token_percent,
  't' => :token_total,
  'm' => :token_minor,
  'M' => :token_major,
  'c' => :token_currency_code,
  's' => :token_currency_symbol,
  'n' => :token_number_only,
  'u' => :token_units,
  'p' => :token_per_exact,
  '~' => :token_approx
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.build_context(coin, **opts) ⇒ Context

Builds a Context struct from a Coin and caller-supplied options.

Parameters:

  • coin (Coin)
  • opts (Hash)

Options Hash (**opts):

  • :currency_dir (Symbol) — default: :ltr or :rtl

    override direction

  • :approx (Boolean)

    whether to render ~ as

  • :units (String, nil)

    value for %u token

  • :per_exact (String, nil)

    value for %p token

Returns:

  • (Context)

Since:

  • 0.1.0



119
120
121
122
123
124
125
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 119

def build_context(coin, **opts)
  Context.new(coin:,
              currency_dir: opts[:currency_dir] || coin.bidi_currency_dir(coin.currency_code),
              approx: opts[:approx] || false,
              units: opts[:units] || nil,
              per_exact: opts[:per_exact] || nil)
end

.dispatch(token, ctx) ⇒ String

Dispatches a single token character to its handler.

Parameters:

  • token (String)

    single character following %

  • ctx (Context)

Returns:

  • (String)

Raises:

  • (ArgumentError)

    if the token is not in TOKEN_MAP

Since:

  • 0.1.0



159
160
161
162
163
164
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 159

def dispatch(token, ctx)
  handler = TOKEN_MAP[token]
  raise ArgumentError, "Unknown presenter token: %#{token}" unless handler

  send(handler, **ctx.to_h)
end

.format(coin, pattern) ⇒ String

Formats a Coin using a pattern string.

This is the class-level entry point; instance-level access is via Coin#present.

Parameters:

  • coin (Coin)

    the value to format

  • pattern (String)

    the format pattern

  • opts (Hash)

    optional context overrides

Returns:

  • (String)

Raises:

  • (ArgumentError)

    if pattern is nil, contains an unknown token, or has an unterminated % escape

Since:

  • 0.1.0



103
104
105
106
107
108
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 103

def format(coin, pattern, **)
  raise ArgumentError, 'pattern required' if pattern.nil?

  ctx = build_context(coin, **)
  scan(pattern, ctx)
end

.scan(pattern, ctx) ⇒ String

Scans a pattern string, expanding %x tokens into rendered values.

Parameters:

  • pattern (String)
  • ctx (Context)

Returns:

  • (String)

Raises:

  • (ArgumentError)

    on unknown or unterminated tokens

Since:

  • 0.1.0



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 133

def scan(pattern, ctx)
  is_token = false
  out = +'' # output buffer

  pattern.to_s.each_char do |char|
    if is_token
      out << dispatch(char, ctx)
      is_token = false
    elsif char == '%'
      is_token = true
    else
      out << char
    end
  end

  raise ArgumentError, "Unterminated token in pattern: #{pattern}" if is_token

  out
end

.token_approx(approx:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 0.1.0



225
226
227
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 225

def token_approx(approx:, **)
  approx ? '' : ''
end

.token_currency_code(coin:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 0.1.0



191
192
193
194
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 191

def token_currency_code(coin:, **)
  # Currency codes are neutral; isolate as LTR for stability
  coin.bidi_isolate(coin.currency_code, dir: :ltr)
end

.token_currency_symbol(coin:, currency_dir:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 0.1.0



197
198
199
200
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 197

def token_currency_symbol(coin:, currency_dir:, **)
  symbol = Money::Currency.new(coin.currency_code).symbol
  coin.bidi_isolate(symbol, dir: currency_dir)
end

.token_major(coin:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 0.1.0



186
187
188
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 186

def token_major(coin:, **)
  coin.bidi_isolate_number(coin.major.to_s('F'))
end

.token_minor(coin:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 0.1.0



181
182
183
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 181

def token_minor(coin:, **)
  coin.bidi_isolate_number(coin.currency_minor)
end

.token_number_only(coin:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 0.1.0



203
204
205
206
207
208
209
210
211
212
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 203

def token_number_only(coin:, **)
  # Best-effort extraction of the numeric portion
  formatted = coin.amount.format
  symbol = Money::Currency.new(coin.currency_code).symbol.to_s

  numberish =
    symbol.empty? ? formatted : formatted.gsub(symbol, '').strip

  coin.bidi_isolate_number(numberish)
end

.token_per_exact(per_exact:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 0.1.0



220
221
222
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 220

def token_per_exact(per_exact:, **)
  per_exact.nil? ? '' : per_exact.to_s
end

.token_percentObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 0.1.0



171
172
173
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 171

def token_percent(**)
  '%'
end

.token_total(coin:, currency_dir:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 0.1.0



176
177
178
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 176

def token_total(coin:, currency_dir:, **)
  coin.bidi_isolate(coin.amount.format, dir: currency_dir)
end

.token_units(units:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 0.1.0



215
216
217
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 215

def token_units(units:, **)
  units.nil? ? '' : units.to_s
end

Instance Method Details

#present(pattern) ⇒ String

Renders this Coin using a format pattern.

Examples:

coin.present('%s%M')          # => "$29.99"
coin.present('%t (%c)')       # => "$29.99 (USD)"
coin.present('~%t', approx: true) # => "≈$29.99"

Parameters:

  • pattern (String)

    the format pattern containing %x tokens

  • opts (Hash)

    optional rendering context overrides (see module docs)

Returns:

  • (String)

Raises:

  • (ArgumentError)

    if the pattern contains an unknown or unterminated token

Since:

  • 0.1.0



71
72
73
# File 'app/models/whittaker_tech/midas/coin/presenter.rb', line 71

def present(pattern, **)
  WhittakerTech::Midas::Coin::Presenter.format(self, pattern, **)
end