From f7319a08a9306ccbbc7c06c3dc70fc424f1418e0 Mon Sep 17 00:00:00 2001 From: Sergei Poljanski Date: Thu, 2 Jul 2026 18:40:20 +0400 Subject: [PATCH] fmt: shared number formatting for HTML and PDF --- asxpio.rb | 11 +++-------- lib/fmt.rb | 25 +++++++++++++++++++++++++ lib/invoice_pdf.rb | 14 ++++---------- test/fmt_test.rb | 22 ++++++++++++++++++++++ 4 files changed, 54 insertions(+), 18 deletions(-) create mode 100644 lib/fmt.rb create mode 100644 test/fmt_test.rb diff --git a/asxpio.rb b/asxpio.rb index afe0c12..802640f 100644 --- a/asxpio.rb +++ b/asxpio.rb @@ -17,6 +17,7 @@ $root = __dir__ $logger = Logger.new($stdout) $env = ENV.fetch('RACK_ENV', 'development') +require_relative 'lib/fmt' require_relative 'lib/mailer' require_relative 'lib/rate_limit' require_relative 'lib/db' @@ -73,18 +74,12 @@ class AsxpioWeb < Sinatra::Base request.ip end - # Exchange rate for display: full captured precision (up to 8 dp), trailing - # zeros trimmed, padded to at least min_dp. Mirrors InvoicePdf#fmt_rate so - # the HTML and PDF agree. (4 dp matches the NBG's GEL quoting.) def fmt_rate(value, min_dp = 4) - frac = BigDecimal(value.to_s).round(8).to_s('F').split('.').last.sub(/0+$/, '') - whole = BigDecimal(value.to_s).to_i - "#{whole}.#{frac.ljust(min_dp, '0')}" + Fmt.rate(value, min_dp: min_dp) end - # LTC amount: up to 8 dp, trailing zeros trimmed, no padding. def fmt_ltc(value) - BigDecimal(value.to_s).round(8).to_s('F').sub(/(\.\d*?)0+$/, '\1').sub(/\.$/, '') + Fmt.ltc(value) end end diff --git a/lib/fmt.rb b/lib/fmt.rb new file mode 100644 index 0000000..c1765fe --- /dev/null +++ b/lib/fmt.rb @@ -0,0 +1,25 @@ +require 'bigdecimal' + +# Number formatting shared by the HTML views and the PDF so they can't drift. +module Fmt + module_function + + # Exchange rate: full captured precision (up to 8 dp), trailing zeros + # trimmed, padded to at least min_dp so it reads as a rate. (4 dp matches + # the NBG's GEL quoting.) + def rate(value, min_dp: 4) + int, frac = BigDecimal(value.to_s).round(8).to_s('F').split('.') + frac = (frac || '').sub(/0+$/, '').ljust(min_dp, '0') + "#{group(int)}.#{frac}" + end + + # LTC amount: up to 8 dp, trailing zeros trimmed, no padding. + def ltc(value) + BigDecimal(value.to_s).round(8).to_s('F').sub(/(\.\d*?)0+$/, '\1').sub(/\.$/, '') + end + + # "1234567" -> "1,234,567" + def group(int) + int.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse + end +end diff --git a/lib/invoice_pdf.rb b/lib/invoice_pdf.rb index 2c7962c..018f0d1 100644 --- a/lib/invoice_pdf.rb +++ b/lib/invoice_pdf.rb @@ -2,6 +2,7 @@ require 'prawn' require 'prawn/table' require 'bigdecimal' require 'stringio' +require_relative 'fmt' class InvoicePdf FONTS_DIR = File.join($root, 'public', 'fonts') @@ -333,18 +334,11 @@ class InvoicePdf end def fmt_ltc(value) - BigDecimal(value.to_s).round(8).to_s('F').sub(/(\.\d*?)0+$/, '\\1').sub(/\.$/, '') + Fmt.ltc(value) end - # Exchange rate: keep full captured precision (up to 8 dp), trim trailing - # zeros, but pad to at least `min_dp` so it reads as a rate. GEL/USD rates are - # quoted to 4 dp by the NBG, so the default min is 4. def fmt_rate(value, min_dp: 4) - s = BigDecimal(value.to_s).round(8).to_s('F') - int, frac = s.split('.') - frac = (frac || '').sub(/0+$/, '') - frac = frac.ljust(min_dp, '0') - "#{with_thousands("#{int}.00").split('.').first}.#{frac}" + Fmt.rate(value, min_dp: min_dp) end def fmt_qty(value) @@ -354,7 +348,7 @@ class InvoicePdf def with_thousands(numstr) int, frac = numstr.split('.') - int = int.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse + int = Fmt.group(int) frac ? "#{int}.#{frac.ljust(2, '0')[0, 2]}" : "#{int}.00" end end diff --git a/test/fmt_test.rb b/test/fmt_test.rb new file mode 100644 index 0000000..9ef96f5 --- /dev/null +++ b/test/fmt_test.rb @@ -0,0 +1,22 @@ +require_relative 'test_helper' + +class FmtTest < Minitest::Test + def test_rate_pads_to_min_dp + assert_equal '2.9500', Fmt.rate('2.95') + assert_equal '3.0000', Fmt.rate(3) + end + + def test_rate_keeps_captured_precision + assert_equal '2.95123456', Fmt.rate('2.95123456') + end + + def test_rate_groups_thousands + assert_equal '1,050.2500', Fmt.rate('1050.25') + end + + def test_ltc_trims_trailing_zeros + assert_equal '1.25', Fmt.ltc('1.250000') + assert_equal '2', Fmt.ltc('2.0') + assert_equal '0.00123456', Fmt.ltc('0.00123456') + end +end