Module:Pandorian rin

From The Paper World
Jump to navigation Jump to search

Documentation for this module may be created at Module:Pandorian rin/doc

local p = {}

-- Config: exchange rates (per 1 RIN)
local JPY_PER_RIN = 2.33
local USD_PER_RIN = 0.0182

-- Units table in 10^3 steps
local UNITS = {
  {name = "", pow = 0},
  {name = "thousand", pow = 3},
  {name = "million",  pow = 6},
  {name = "billion",  pow = 9},
  {name = "trillion", pow = 12},
  {name = "quadrillion", pow = 15},
}

-- Quick lookup for worded units in input
local UNIT_INDEX = {
  ["thousand"] = 2,
  ["million"] = 3,
  ["billion"] = 4,
  ["trillion"] = 5,
  ["quadrillion"] = 6,
}

local function lang()
  return mw.getContentLanguage() or mw.language.getContentLanguage()
end

-- Trim, remove commas/spaces around numbers
local function normalize_numstr(s)
  s = mw.text.trim(s or "")
  -- allow commas, thin spaces, etc.
  s = s:gsub("[,%s ]", "")
  return s
end

-- Round a number to N significant figures, return number
local function round_sig(x, sig)
  if x == 0 or x == nil then return 0 end
  local m = 10 ^ (sig - math.ceil(math.log10(math.abs(x))))
  return math.floor(x * m + 0.5) / m
end

-- Format integer part with periods as thousand separators
local function add_period_grouping(numstr)
  local sign, int, frac = numstr:match("^([%-]?)(%d+)(.*)$")
  local rev = int:reverse()
  local grouped = rev:gsub("(%d%d%d)", "%1.")
  grouped = grouped:reverse()
  -- Remove possible trailing dot if length was exact multiple of 3
  grouped = grouped:gsub("^%.", "")
  grouped = grouped:gsub("%.$", "")
  return sign .. grouped .. (frac or "")
end

-- Period-separated formatting for RIN only
local function format_clean_rin(n)
  local s = tostring(n)
  if s:find("[eE]") then
    s = string.format("%.15f", n)
  end
  if s:find("%.") then
    s = s:gsub("0+$", ""):gsub("%.$", "")
  end
  local integer, frac = s:match("^([%-]?%d+)(%.%d+)?$")
  if integer then
    local grouped = add_period_grouping(integer)
    if frac then
      return grouped .. frac
    else
      return grouped
    end
  else
    return s
  end
end

-- Format a number without trailing zeros (but keep decimals if present)
local function format_clean(n)
  -- Keep as plain string; language formatter for groupings:
  local s = tostring(n)
  -- If scientific notation appears, convert to plain:
  if s:find("[eE]") then
    -- rely on string.format; 15 sig figs safety
    s = string.format("%.15f", n)
  end
  -- Trim trailing zeros after decimal
  if s:find("%.") then
    s = s:gsub("0+$", ""):gsub("%.$", "")
  end
  -- Apply language digit grouping when appropriate.
  -- We only group integer part; leave decimals intact.
  local integer, frac = s:match("^([%-]?%d+)(%.%d+)?$")
  if integer then
    local grouped = add_period_grouping(integer)
    if frac then
      return grouped .. frac
    else
      return grouped
    end
  else
    -- fallback if parsing failed
    return s
  end
end

-- Choose a unit so value is in [1, 1000) whenever possible.
-- Returns scaled_value, unit_name
local function choose_unit(value)
  if value == 0 then return 0, "" end
  -- find best index
  local absval = math.abs(value)
  local idx = 1
  -- push up while >= 1000 and we have higher units
  while absval >= 1000 and idx < #UNITS do
    absval = absval / 1000
    idx = idx + 1
  end
  -- push down while < 1 and we have lower units
  while absval < 1 and idx > 1 do
    absval = absval * 1000
    idx = idx - 1
  end
  local scaled = (value / (10 ^ UNITS[idx].pow))
  return scaled, UNITS[idx].name
end

-- Parse the input amount parameter. Supports:
-- "398.1 million", "1.234 billion", "300.25", etc.
-- Returns:
--   rin_value (in base RIN, numeric),
--   display_amount (number to display on left),
--   display_unit (string unit to display on left),
--   used_worded_unit (bool, to control 4-sig-fig behavior)
local function parse_amount(param1)
  local raw = mw.text.trim(param1 or "")
  if raw == "" then
    return 0, 0, "", false
  end

  -- Detect any unit word present
  local unit_word = nil
  local lower = mw.ustring.lower(raw)

  for word, idx in pairs(UNIT_INDEX) do
    if mw.ustring.find(lower, word) then
      unit_word = word
      break
    end
  end

  -- Extract the numeric part (first number found)
  local numstr = raw:match("([%d%.,%s %-]+)") or raw
  numstr = normalize_numstr(numstr)
  local num = tonumber(numstr)

  if not num then
    -- Could not parse; treat as 0
    return 0, 0, "", false
  end

  local pow = 0
  if unit_word then
    pow = UNITS[UNIT_INDEX[unit_word]].pow
  end

  local rin_value = num * (10 ^ pow)

  -- For display on the left:
  local display_amount = num
  local display_unit = unit_word or ""

  -- If worded unit is used, clamp left-side amount to 4 sig figs
  if unit_word then
    display_amount = round_sig(display_amount, 4)
  end

  return rin_value, display_amount, display_unit or "", unit_word ~= nil
end

-- Format a (value, unit) pair with 4 significant figures and a unit word.
local function format_with_unit(value)
  local scaled, uname = choose_unit(value)
  local rounded = round_sig(scaled, 4)
  return format_clean(rounded), uname
end

-- Build the left-side RIN string
local function build_left(link, display_amount, display_unit)
  local star
  if link == "yes" then
    star = "[[Pandorian rin|✧]]"
  else
    star = "✧"
  end

  local amt = format_clean_rin(display_amount)
  local unit_suffix = (display_unit ~= "" and (" " .. display_unit) or "")
  return string.format('<span style="white-space: nowrap">%s%s%s</span>&nbsp;RIN',
    star, amt ~= "" and amt or "0", unit_suffix)
end

-- Build conversion strings
local function build_conversions(rin_value, mode)
  if mode ~= "yes" and mode ~= "USD" and mode ~= "JPY" then
    return ""
  end

  local parts = {}

  local function push(s) table.insert(parts, s) end

  if mode == "yes" or mode == "JPY" then
    local jpy_val = rin_value * JPY_PER_RIN
    local jpy_amt, jpy_unit = format_with_unit(jpy_val)
    local unit_part = (jpy_unit ~= "" and (" " .. jpy_unit) or "")
    push("~JP¥" .. jpy_amt .. unit_part)
  end

  if mode == "yes" or mode == "USD" then
    local usd_val = rin_value * USD_PER_RIN
    local usd_amt, usd_unit = format_with_unit(usd_val)
    local unit_part = (usd_unit ~= "" and (" " .. usd_unit) or "")
    push("~US$" .. usd_amt .. unit_part)
  end

  if #parts == 0 then return "" end
  return " ''(" .. table.concat(parts, " or ") .. ")''"
end

function p.rin(frame)
  local args = frame:getParent() and frame:getParent().args or frame.args

  local param1 = args[1]
  local link = mw.text.trim(args["link"] or "no"):lower()
  local conversion = mw.text.trim(args["conversion"] or "no"):upper() -- normalize to YES/USD/JPY

  if conversion == "YES" then conversion = "yes" end
  if conversion ~= "yes" and conversion ~= "USD" and conversion ~= "JPY" then
    conversion = "no"
  end

  local rin_value, display_amount, display_unit = parse_amount(param1)

  local left = build_left(link, display_amount, display_unit)
  local right = build_conversions(rin_value, conversion)

  -- Preserve your original HTML comment break before conversions
  if right ~= "" then
    return left .. "<!-- -->" .. right
  else
    return left
  end
end

return p