Module: Odin::Utils::FormatUtils

Defined in:
lib/odin/utils/format_utils.rb

Constant Summary collapse

ESCAPE_MAP =

Escape special characters in an ODIN string. Handles: \, ", n, r, t and control chars as uXXXX.

{ "\\" => "\\\\", "\"" => "\\\"", "\n" => "\\n", "\r" => "\\r", "\t" => "\\t" }.freeze
RE_ESCAPE =
/[\\"\n\r\t\x00-\x1f]/.freeze
MODIFIER_PREFIXES =

Pre-computed modifier prefix lookup table (indexed by bit flags) Bit 2 = required, bit 1 = deprecated, bit 0 = confidential

["", "*", "-", "-*", "!", "!*", "!-", "!-*"].freeze
STRINGIFY_FORMATTERS =

─────────────────────────────────────────────────────────────────────Value Formatting (Stringify — preserves raw values) ─────────────────────────────────────────────────────────────────────

{
  Types::OdinNull => ->(v) { "~" },
  Types::OdinBoolean => ->(v) { v.value ? "?true" : "?false" },
  Types::OdinString => ->(v) { format_quoted_string(v.value) },
  Types::OdinNumber => ->(v) { v.raw ? "##{v.raw}" : "##{v.value}" },
  Types::OdinInteger => ->(v) { v.raw ? "###{v.raw}" : "###{v.value}" },
  Types::OdinCurrency => ->(v) { format_stringify_currency(v) },
  Types::OdinPercent => ->(v) { v.raw ? "#%#{v.raw}" : "#%#{v.value}" },
  Types::OdinDate => ->(v) { v.raw },
  Types::OdinTimestamp => ->(v) { v.raw },
  Types::OdinTime => ->(v) { v.value },
  Types::OdinDuration => ->(v) { v.value },
  Types::OdinReference => ->(v) { "@#{v.path}" },
  Types::OdinBinary => ->(v) { format_binary(v) },
  Types::OdinVerbExpression => ->(v) { format_verb(v) },
  Types::OdinArray => ->(_v) { "[]" },
  Types::OdinObject => ->(_v) { "{}" },
}.freeze
CANONICAL_FORMATTERS =

─────────────────────────────────────────────────────────────────────Value Formatting (Canonical — deterministic, no raw) ─────────────────────────────────────────────────────────────────────

{
  Types::OdinNull => ->(v) { "~" },
  Types::OdinBoolean => ->(v) { v.value ? "true" : "false" },
  Types::OdinString => ->(v) { format_quoted_string(v.value) },
  Types::OdinNumber => ->(v) { "##{format_canonical_number(v.raw || v.value)}" },
  Types::OdinInteger => ->(v) { "###{v.raw || v.value}" },
  Types::OdinCurrency => ->(v) { format_canonical_currency(v) },
  Types::OdinPercent => ->(v) { v.raw ? "#%#{v.raw}" : "#%#{v.value}" },
  Types::OdinDate => ->(v) { v.raw },
  Types::OdinTimestamp => ->(v) { v.raw },
  Types::OdinTime => ->(v) { v.value },
  Types::OdinDuration => ->(v) { v.value },
  Types::OdinReference => ->(v) { "@#{v.path}" },
  Types::OdinBinary => ->(v) { format_binary(v) },
  Types::OdinVerbExpression => ->(v) { format_canonical_verb(v) },
  Types::OdinArray => ->(_v) { "[]" },
  Types::OdinObject => ->(_v) { "{}" },
}.freeze

Class Method Summary collapse

Class Method Details

.escape_string(value) ⇒ Object



17
18
19
20
21
# File 'lib/odin/utils/format_utils.rb', line 17

def self.escape_string(value)
  value.gsub(RE_ESCAPE) do |ch|
    ESCAPE_MAP[ch] || format("\\u%04X", ch.ord)
  end
end

.format_binary(value) ⇒ Object

─────────────────────────────────────────────────────────────────────Binary Formatting ─────────────────────────────────────────────────────────────────────



168
169
170
171
172
173
174
175
# File 'lib/odin/utils/format_utils.rb', line 168

def self.format_binary(value)
  # data is stored as base64 string already
  if value.algorithm
    "^#{value.algorithm}:#{value.data}"
  else
    "^#{value.data}"
  end
end

.format_canonical_currency(value) ⇒ Object

Canonical currency: always min 2 decimal places, code uppercase. Prefers raw to preserve precision and integer parts beyond Float range.



139
140
141
142
143
# File 'lib/odin/utils/format_utils.rb', line 139

def self.format_canonical_currency(value)
  result = +"#$#{format_canonical_currency_number(value)}"
  result << ":#{value.currency_code.upcase}" if value.currency_code
  result
end

.format_canonical_currency_number(value) ⇒ Object

Build the numeric portion of a canonical currency, at least 2 decimal places.



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/odin/utils/format_utils.rb', line 146

def self.format_canonical_currency_number(value)
  if value.raw
    raw = value.raw
    # Defensive: drop any code suffix that leaked into raw
    raw = raw.split(":", 2)[0] if raw.include?(":")
    negative = raw.start_with?("-")
    unsigned = negative ? raw[1..] : raw
    dot = unsigned.index(".")
    int_part = dot ? unsigned[0...dot] : unsigned
    frac_part = dot ? unsigned[(dot + 1)..] : ""
    frac_part = frac_part.ljust(2, "0") if frac_part.length < 2
    "#{negative ? '-' : ''}#{int_part}.#{frac_part}"
  else
    dp = [value.decimal_places, 2].max
    format("%.#{dp}f", value.value.to_f)
  end
end

.format_canonical_number(value) ⇒ Object

Format number in canonical form: strip trailing zeros. Prefers the raw string (when passed) to preserve precision beyond Float range.



108
109
110
111
112
113
114
# File 'lib/odin/utils/format_utils.rb', line 108

def self.format_canonical_number(value)
  s = value.is_a?(String) ? value : value.to_s
  if s.include?(".") && !s.include?("e") && !s.include?("E")
    s = s.sub(/\.?0+\z/, "")
  end
  s
end

.format_canonical_value(value) ⇒ Object



97
98
99
100
# File 'lib/odin/utils/format_utils.rb', line 97

def self.format_canonical_value(value)
  formatter = CANONICAL_FORMATTERS[value.class]
  formatter ? formatter.call(value) : value.to_s
end

.format_canonical_verb(value) ⇒ Object



191
192
193
194
195
196
197
198
199
# File 'lib/odin/utils/format_utils.rb', line 191

def self.format_canonical_verb(value)
  prefix = value.is_custom ? "%&" : "%"
  result = +"#{prefix}#{value.verb}"
  value.args.each do |arg|
    result << " "
    result << format_canonical_value(arg)
  end
  result
end

.format_modifier_prefix(modifiers) ⇒ Object

Format modifier prefix in canonical order: ! (required), - (deprecated), * (confidential)



37
38
39
40
41
42
43
44
# File 'lib/odin/utils/format_utils.rb', line 37

def self.format_modifier_prefix(modifiers)
  return "" unless modifiers
  idx = 0
  idx |= 4 if modifiers.required
  idx |= 2 if modifiers.deprecated
  idx |= 1 if modifiers.confidential
  MODIFIER_PREFIXES[idx]
end

.format_quoted_string(value) ⇒ Object

Format a string value as quoted ODIN string.



24
25
26
# File 'lib/odin/utils/format_utils.rb', line 24

def self.format_quoted_string(value)
  "\"#{escape_string(value)}\""
end

.format_stringify_currency(value) ⇒ Object

Stringify currency: use raw if available, else format with stored decimal places



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/odin/utils/format_utils.rb', line 121

def self.format_stringify_currency(value)
  if value.raw
    result = +"#$#{value.raw}"
    if value.currency_code && !value.raw.include?(":")
      result << ":#{value.currency_code}"
    end
    result
  else
    dp = value.decimal_places
    formatted = format("%.#{dp}f", value.value.to_f)
    result = +"#$#{formatted}"
    result << ":#{value.currency_code}" if value.currency_code
    result
  end
end

.format_value(value) ⇒ Object



69
70
71
72
# File 'lib/odin/utils/format_utils.rb', line 69

def self.format_value(value)
  formatter = STRINGIFY_FORMATTERS[value.class]
  formatter ? formatter.call(value) : value.to_s
end

.format_verb(value) ⇒ Object

─────────────────────────────────────────────────────────────────────Verb Formatting ─────────────────────────────────────────────────────────────────────



181
182
183
184
185
186
187
188
189
# File 'lib/odin/utils/format_utils.rb', line 181

def self.format_verb(value)
  prefix = value.is_custom ? "%&" : "%"
  result = +"#{prefix}#{value.verb}"
  value.args.each do |arg|
    result << " "
    result << format_value(arg)
  end
  result
end