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 = confidential, bit 0 = deprecated

["", "-", "*", "*-", "!", "!-", "!*", "!*-"].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.value)}" },
  Types::OdinInteger => ->(v) { "###{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 ─────────────────────────────────────────────────────────────────────



152
153
154
155
156
157
158
159
# File 'lib/odin/utils/format_utils.rb', line 152

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



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

def self.format_canonical_currency(value)
  dp = [value.decimal_places, 2].max
  formatted = format("%.#{dp}f", value.value.to_f)
  result = +"#$#{formatted}"
  result << ":#{value.currency_code.upcase}" if value.currency_code
  result
end

.format_canonical_number(value) ⇒ Object

Format number in canonical form: strip trailing zeros. Uses String() representation then removes unnecessary zeros.



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

def self.format_canonical_number(value)
  s = value.to_s
  # Ruby's Float#to_s already produces clean output like "3.14", "42.0"
  # But we need to handle the case where it produces "3.14" correctly
  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



175
176
177
178
179
180
181
182
183
# File 'lib/odin/utils/format_utils.rb', line 175

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), * (confidential), - (deprecated)



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.confidential
  idx |= 1 if modifiers.deprecated
  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



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

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 ─────────────────────────────────────────────────────────────────────



165
166
167
168
169
170
171
172
173
# File 'lib/odin/utils/format_utils.rb', line 165

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