Sửa đổi Mô đun:Convert
Chú ý: Bạn chưa đăng nhập và địa chỉ IP của bạn sẽ hiển thị công khai khi lưu các sửa đổi.
Bạn có thể tham gia như người biên soạn chuyên nghiệp và lâu dài ở Bách khoa Toàn thư Việt Nam, bằng cách đăng ký và đăng nhập - IP của bạn sẽ không bị công khai và có thêm nhiều lợi ích khác.
Các sửa đổi có thể được lùi lại. Xin hãy kiểm tra phần so sánh bên dưới để xác nhận lại những gì bạn muốn làm, sau đó lưu thay đổi ở dưới để hoàn tất việc lùi lại sửa đổi.
Bản hiện tại | Nội dung bạn nhập | ||
Dòng 1: | Dòng 1: | ||
+ | -- Convert a value from one unit of measurement to another. | ||
+ | -- Example: {{convert|123|lb|kg}} --> 123 pounds (56 kg) | ||
+ | |||
local MINUS = '−' -- Unicode U+2212 MINUS SIGN (UTF-8: e2 88 92) | local MINUS = '−' -- Unicode U+2212 MINUS SIGN (UTF-8: e2 88 92) | ||
local abs = math.abs | local abs = math.abs | ||
Dòng 13: | Dòng 16: | ||
local numdot -- must be '.' or ',' or a character which works in a regex | local numdot -- must be '.' or ',' or a character which works in a regex | ||
local numsep, numsep_remove, numsep_remove2 | local numsep, numsep_remove, numsep_remove2 | ||
− | local | + | local data_code, all_units |
local text_code | local text_code | ||
local varname -- can be a code to use variable names that depend on value | local varname -- can be a code to use variable names that depend on value | ||
Dòng 31: | Dòng 34: | ||
local extra_module -- name of module with extra units | local extra_module -- name of module with extra units | ||
local extra_units -- nil or table of extra units from extra_module | local extra_units -- nil or table of extra units from extra_module | ||
+ | |||
+ | -- Some options in the invoking template can set variables used later in the module. | ||
+ | local currency_text -- for a user-defined currency symbol: {{convert|12|$/ha|$=€}} (euro replaces dollar) | ||
local function from_en(text) | local function from_en(text) | ||
Dòng 81: | Dòng 87: | ||
end | end | ||
− | local add_warning -- forward | + | local add_warning, with_separator -- forward declarations |
local function to_en_with_check(text, parms) | local function to_en_with_check(text, parms) | ||
− | -- Version of to_en() | + | -- Version of to_en() for a wiki using numdot = ',' and numsep = '.' to check |
− | -- text (an input number as a string) | + | -- text (an input number as a string) which might have been copied from enwiki. |
-- For example, in '1.234' the '.' could be a decimal mark or a group separator. | -- For example, in '1.234' the '.' could be a decimal mark or a group separator. | ||
+ | -- From viwiki. | ||
if to_en_table then | if to_en_table then | ||
text = ustring.gsub(text, '%d', to_en_table) | text = ustring.gsub(text, '%d', to_en_table) | ||
Dòng 92: | Dòng 99: | ||
local original = text | local original = text | ||
text = text:gsub(',', '') -- for example, interpret "1,234.5" as an enwiki value | text = text:gsub(',', '') -- for example, interpret "1,234.5" as an enwiki value | ||
− | + | if parms then | |
− | + | add_warning(parms, 0, 'cvt_enwiki_num', original, with_separator({}, text)) | |
− | + | end | |
− | |||
else | else | ||
if numsep_remove then | if numsep_remove then | ||
Dòng 110: | Dòng 116: | ||
end | end | ||
− | local function | + | local function omit_separator(id) |
− | -- Return true if id (a unit | + | -- Return true if there should be no separator before id (a unit symbol or name). |
-- For zhwiki, there should be no separator if id uses local characters. | -- For zhwiki, there should be no separator if id uses local characters. | ||
-- The following kludge should be a sufficient test. | -- The following kludge should be a sufficient test. | ||
− | if id:sub(1, 2) == '-{' then -- for "-{...}-" content language variant | + | if omitsep then |
− | + | if id:sub(1, 2) == '-{' then -- for "-{...}-" content language variant | |
− | + | return true | |
− | + | end | |
− | + | if id:byte() > 127 then | |
− | + | local first = usub(id, 1, 1) | |
− | + | if first ~= 'Å' and first ~= '°' and first ~= 'µ' then | |
+ | return true | ||
+ | end | ||
end | end | ||
end | end | ||
− | return | + | return id:sub(1, 1) == '/' -- no separator before units like "/ha" |
end | end | ||
local spell_module -- name of module that can spell numbers | local spell_module -- name of module that can spell numbers | ||
− | local speller -- function from that module to handle spelling (set if | + | local speller -- function from that module to handle spelling (set if needed) |
+ | local wikidata_module, wikidata_data_module -- names of Wikidata modules | ||
+ | local wikidata_code, wikidata_data -- exported tables from those modules (set if needed) | ||
− | local function set_config( | + | local function set_config(args) |
-- Set configuration options from template #invoke or defaults. | -- Set configuration options from template #invoke or defaults. | ||
− | config = | + | config = args |
maxsigfig = config.maxsigfig or 14 -- maximum number of significant figures | maxsigfig = config.maxsigfig or 14 -- maximum number of significant figures | ||
− | + | local data_module, text_module | |
− | + | local sandbox = config.sandbox and ('/' .. config.sandbox) or '' | |
− | local data_module, text_module | + | data_module = "Module:Convert/data" .. sandbox |
− | + | text_module = "Module:Convert/text" .. sandbox | |
− | + | extra_module = "Module:Convert/extra" .. sandbox | |
− | + | wikidata_module = "Module:Convert/wikidata" .. sandbox | |
− | + | wikidata_data_module = "Module:Convert/wikidata/data" .. sandbox | |
− | + | spell_module = "Module:ConvertNumeric" | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
data_code = mw.loadData(data_module) | data_code = mw.loadData(data_module) | ||
text_code = mw.loadData(text_module) | text_code = mw.loadData(text_module) | ||
− | |||
− | |||
all_units = data_code.all_units | all_units = data_code.all_units | ||
local translation = text_code.translation_table | local translation = text_code.translation_table | ||
Dòng 248: | Dòng 248: | ||
end | end | ||
− | local function wanted_category( | + | local function table_len(t) |
− | -- Return | + | -- Return length (<100) of a numbered table to replace #t which is |
− | + | -- documented to not work if t is accessed via mw.loadData(). | |
+ | for i = 1, 100 do | ||
+ | if t[i] == nil then | ||
+ | return i - 1 | ||
+ | end | ||
+ | end | ||
+ | end | ||
+ | |||
+ | local function wanted_category(catkey, catsort, want_warning) | ||
+ | -- Return message category if it is wanted in current namespace, | ||
+ | -- otherwise return ''. | ||
+ | local cat | ||
local title = mw.title.getCurrentTitle() | local title = mw.title.getCurrentTitle() | ||
if title then | if title then | ||
Dòng 257: | Dòng 268: | ||
for _, v in ipairs(split(config.nscat or nsdefault, ',')) do | for _, v in ipairs(split(config.nscat or nsdefault, ',')) do | ||
if namespace == tonumber(v) then | if namespace == tonumber(v) then | ||
− | + | cat = text_code.all_categories[want_warning and 'warning' or catkey] | |
+ | if catsort and catsort ~= '' and cat:sub(-2) == ']]' then | ||
+ | cat = cat:sub(1, -3) .. '|' .. mw.text.nowiki(usub(catsort, 1, 20)) .. ']]' | ||
+ | end | ||
+ | break | ||
end | end | ||
end | end | ||
end | end | ||
+ | return cat or '' | ||
end | end | ||
− | local function message(mcode) | + | local function message(parms, mcode, is_warning) |
− | -- Return error message, including category if specified | + | -- Return wikitext for an error message, including category if specified |
-- for the message type. | -- for the message type. | ||
-- mcode = numbered table specifying the message: | -- mcode = numbered table specifying the message: | ||
-- mcode[1] = 'cvt_xxx' (string used as a key to get message info) | -- mcode[1] = 'cvt_xxx' (string used as a key to get message info) | ||
− | -- mcode[2] = 'parm1' (string to replace | + | -- mcode[2] = 'parm1' (string to replace '$1' if any in message) |
− | -- mcode[3] = 'parm2' (string to replace | + | -- mcode[3] = 'parm2' (string to replace '$2' if any in message) |
− | -- mcode[4] = 'parm3' (string to replace | + | -- mcode[4] = 'parm3' (string to replace '$3' if any in message) |
− | local msg = text_code.all_messages[mcode[1]] | + | local msg |
− | local | + | if type(mcode) == 'table' then |
+ | if mcode[1] == 'cvt_no_output' then | ||
+ | -- Some errors should cause convert to output an empty string, | ||
+ | -- for example, for an optional field in an infobox. | ||
+ | return '' | ||
+ | end | ||
+ | msg = text_code.all_messages[mcode[1]] | ||
+ | end | ||
+ | parms.have_problem = true | ||
+ | local function subparm(fmt, ...) | ||
+ | local rep = {} | ||
+ | for i, v in ipairs({...}) do | ||
+ | rep['$' .. i] = v | ||
+ | end | ||
+ | return (fmt:gsub('$%d+', rep)) | ||
+ | end | ||
if msg then | if msg then | ||
local parts = {} | local parts = {} | ||
Dòng 285: | Dòng 316: | ||
end | end | ||
-- Escape user input so it does not break the message. | -- Escape user input so it does not break the message. | ||
− | -- To avoid | + | -- To avoid tags (like {{convert|1<math>23</math>|m}}) breaking |
− | -- | + | -- the mouseover title, any strip marker starting with char(127) is |
− | -- replaced with | + | -- replaced with '...' (text not needing i18n). |
− | local append | + | local append |
local pos = s:find(string.char(127), 1, true) | local pos = s:find(string.char(127), 1, true) | ||
if pos then | if pos then | ||
− | + | append = '...' | |
− | |||
− | |||
− | |||
− | |||
s = s:sub(1, pos - 1) | s = s:sub(1, pos - 1) | ||
end | end | ||
if limit and ulen(s) > limit then | if limit and ulen(s) > limit then | ||
s = usub(s, 1, limit) | s = usub(s, 1, limit) | ||
− | + | append = '...' | |
− | |||
− | |||
end | end | ||
− | s = nowiki(s) .. append | + | s = mw.text.nowiki(s) .. (append or '') |
else | else | ||
s = '?' | s = '?' | ||
end | end | ||
− | parts[i] = s | + | parts['$' .. i] = s |
end | end | ||
− | local title = | + | local function ispreview() |
− | local text = msg[2] or 'Missing message' | + | -- Return true if a prominent message should be shown. |
− | local cat = wanted_category( | + | if parms.test == 'preview' or parms.test == 'nopreview' then |
+ | -- For testing, can preview a real message or simulate a preview | ||
+ | -- when running automated tests. | ||
+ | return parms.test == 'preview' | ||
+ | end | ||
+ | local success, revid = pcall(function () | ||
+ | return (parms.frame):preprocess('{{REVISIONID}}') end) | ||
+ | return success and (revid == '') | ||
+ | end | ||
+ | local want_warning = is_warning and | ||
+ | not config.warnings and -- show unobtrusive warnings if config.warnings not configured | ||
+ | not msg.nowarn -- but use msg settings, not standard warning, if specified | ||
+ | local title = string.gsub(msg[1] or 'Missing message', '$%d+', parts) | ||
+ | local text = want_warning and '*' or msg[2] or 'Missing message' | ||
+ | local cat = wanted_category(msg[3], mcode[2], want_warning) | ||
local anchor = msg[4] or '' | local anchor = msg[4] or '' | ||
− | local fmt = text_code.all_messages[ | + | local fmtkey = ispreview() and 'cvt_format_preview' or |
− | + | (want_warning and 'cvt_format2' or msg.format or 'cvt_format') | |
− | + | local fmt = text_code.all_messages[fmtkey] or 'convert: bug' | |
+ | return subparm(fmt, title:gsub('"', '"'), text, cat, anchor) | ||
end | end | ||
return 'Convert internal error: unknown message' | return 'Convert internal error: unknown message' | ||
Dòng 323: | Dòng 363: | ||
function add_warning(parms, level, key, text1, text2) -- for forward declaration above | function add_warning(parms, level, key, text1, text2) -- for forward declaration above | ||
-- If enabled, add a warning that will be displayed after the convert result. | -- If enabled, add a warning that will be displayed after the convert result. | ||
+ | -- A higher level is more verbose: more kinds of warnings are displayed. | ||
-- To reduce output noise, only the first warning is displayed. | -- To reduce output noise, only the first warning is displayed. | ||
− | + | if level <= (tonumber(config.warnings) or 1) then | |
− | + | if parms.warnings == nil then | |
− | + | parms.warnings = message(parms, { key, text1, text2 }, true) | |
− | |||
− | |||
end | end | ||
end | end | ||
Dòng 349: | Dòng 388: | ||
success, speller = pcall(get_speller, spell_module) | success, speller = pcall(get_speller, spell_module) | ||
if not success or type(speller) ~= 'function' then | if not success or type(speller) ~= 'function' then | ||
− | add_warning(parms, 1, 'cvt_no_spell') | + | add_warning(parms, 1, 'cvt_no_spell', 'spell') |
return nil | return nil | ||
end | end | ||
Dòng 547: | Dòng 586: | ||
local unit_per_mt = { | local unit_per_mt = { | ||
− | -- Metatable to get values for a | + | -- Metatable to get values for a per unit of form "x/y". |
− | -- This is never called to determine a unit name or link because | + | -- This is never called to determine a unit name or link because per units |
-- are handled as a special case. | -- are handled as a special case. | ||
+ | -- Similarly, the default output is handled elsewhere, and for a symbol | ||
+ | -- this is only called from get_default() for default_exceptions. | ||
__index = function (self, key) | __index = function (self, key) | ||
local value | local value | ||
Dòng 574: | Dòng 615: | ||
} | } | ||
− | local function | + | local function make_per(unitcode, unit_table, ulookup) |
− | -- Return true, t where t is a | + | -- Return true, t where t is a per unit with unit codes expanded to unit tables, |
-- or return false, t where t is an error message table. | -- or return false, t where t is an error message table. | ||
− | -- | + | local result = { |
− | + | unitcode = unitcode, | |
− | -- | + | utype = unit_table.utype, |
− | -- Parameter 'what' determines whether combination units are accepted: | + | per = {} |
− | -- 'no_combination' : single unit only | + | } |
− | -- 'any_combination' : single unit or combination or output multiple | + | override_from(result, unit_table, { 'invert', 'iscomplex', 'default', 'link', 'symbol', 'symlink' }) |
− | -- 'only_multiple' : single unit or output multiple only | + | result.symbol_raw = (result.symbol or false) -- to distinguish between a defined exception and a metatable calculation |
+ | local prefix | ||
+ | for i, v in ipairs(unit_table.per) do | ||
+ | if i == 1 and v == '' then | ||
+ | -- First unit symbol can be empty; that gives a nil first unit table. | ||
+ | elseif i == 1 and text_code.currency[v] then | ||
+ | prefix = currency_text or v | ||
+ | else | ||
+ | local success, t = ulookup(v) | ||
+ | if not success then return false, t end | ||
+ | result.per[i] = t | ||
+ | end | ||
+ | end | ||
+ | local multiplier = unit_table.multiplier | ||
+ | if not result.utype then | ||
+ | -- Creating an automatic per unit. | ||
+ | local unit1 = result.per[1] | ||
+ | local utype = (unit1 and unit1.utype or prefix or '') .. '/' .. result.per[2].utype | ||
+ | local t = data_code.per_unit_fixups[utype] | ||
+ | if t then | ||
+ | if type(t) == 'table' then | ||
+ | utype = t.utype or utype | ||
+ | result.link = result.link or t.link | ||
+ | multiplier = multiplier or t.multiplier | ||
+ | else | ||
+ | utype = t | ||
+ | end | ||
+ | end | ||
+ | result.utype = utype | ||
+ | end | ||
+ | result.scalemultiplier = multiplier or 1 | ||
+ | result.vprefix = prefix or false -- set to non-nil to avoid calling __index | ||
+ | return true, setmetatable(result, unit_per_mt) | ||
+ | end | ||
+ | |||
+ | local function lookup(parms, unitcode, what, utable, fails, depth) | ||
+ | -- Return true, t where t is a copy of the unit's converter table, | ||
+ | -- or return false, t where t is an error message table. | ||
+ | -- Parameter 'what' determines whether combination units are accepted: | ||
+ | -- 'no_combination' : single unit only | ||
+ | -- 'any_combination' : single unit or combination or output multiple | ||
+ | -- 'only_multiple' : single unit or output multiple only | ||
-- Parameter unitcode is a symbol (like 'g'), with an optional SI prefix (like 'kg'). | -- Parameter unitcode is a symbol (like 'g'), with an optional SI prefix (like 'kg'). | ||
-- If, for example, 'kg' is in this table, that entry is used; | -- If, for example, 'kg' is in this table, that entry is used; | ||
Dòng 593: | Dòng 675: | ||
-- Wikignomes may also put two spaces or " " in combinations, so | -- Wikignomes may also put two spaces or " " in combinations, so | ||
-- replace underscore, " ", and multiple spaces with a single space. | -- replace underscore, " ", and multiple spaces with a single space. | ||
− | utable = utable or all_units | + | utable = utable or parms.unittable or all_units |
fails = fails or {} | fails = fails or {} | ||
depth = depth and depth + 1 or 1 | depth = depth and depth + 1 or 1 | ||
Dòng 606: | Dòng 688: | ||
end | end | ||
unitcode = unitcode:gsub('_', ' '):gsub(' ', ' '):gsub(' +', ' ') | unitcode = unitcode:gsub('_', ' '):gsub(' ', ' '):gsub(' +', ' ') | ||
+ | local function call_make_per(t) | ||
+ | return make_per(unitcode, t, | ||
+ | function (ucode) return lookup(parms, ucode, 'no_combination', utable, fails, depth) end | ||
+ | ) | ||
+ | end | ||
local t = utable[unitcode] | local t = utable[unitcode] | ||
if t then | if t then | ||
Dòng 611: | Dòng 698: | ||
return false, { 'cvt_should_be', t.shouldbe } | return false, { 'cvt_should_be', t.shouldbe } | ||
end | end | ||
− | |||
if t.sp_us then | if t.sp_us then | ||
− | + | parms.opt_sp_us = true | |
− | |||
end | end | ||
local target = t.target -- nil, or unitcode is an alias for this target | local target = t.target -- nil, or unitcode is an alias for this target | ||
if target then | if target then | ||
− | local success, result = lookup(target | + | local success, result = lookup(parms, target, what, utable, fails, depth) |
if not success then return false, result end | if not success then return false, result end | ||
override_from(result, t, { 'customary', 'default', 'link', 'symbol', 'symlink' }) | override_from(result, t, { 'customary', 'default', 'link', 'symbol', 'symlink' }) | ||
Dòng 628: | Dòng 713: | ||
return true, result | return true, result | ||
end | end | ||
− | + | if t.per then | |
− | + | return call_make_per(t) | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | return | ||
end | end | ||
local combo = t.combination -- nil or a table of unitcodes | local combo = t.combination -- nil or a table of unitcodes | ||
Dòng 667: | Dòng 727: | ||
local cvt = result.combination | local cvt = result.combination | ||
for i, v in ipairs(combo) do | for i, v in ipairs(combo) do | ||
− | local success, t = lookup(v | + | local success, t = lookup(parms, v, multiple and 'no_combination' or 'only_multiple', utable, fails, depth) |
if not success then return false, t end | if not success then return false, t end | ||
cvt[i] = t | cvt[i] = t | ||
Dòng 674: | Dòng 734: | ||
end | end | ||
local result = shallow_copy(t) | local result = shallow_copy(t) | ||
− | result. | + | result.unitcode = unitcode |
if result.prefixes then | if result.prefixes then | ||
result.si_name = '' | result.si_name = '' | ||
Dòng 693: | Dòng 753: | ||
if t and t.prefixes then | if t and t.prefixes then | ||
local result = shallow_copy(t) | local result = shallow_copy(t) | ||
− | + | result.unitcode = unitcode | |
− | + | result.si_name = parms.opt_sp_us and si.name_us or si.name | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
result.si_prefix = si.prefix or prefix | result.si_prefix = si.prefix or prefix | ||
result.scale = t.scale * 10 ^ (si.exponent * t.prefixes) | result.scale = t.scale * 10 ^ (si.exponent * t.prefixes) | ||
return true, setmetatable(result, unit_prefixed_mt) | return true, setmetatable(result, unit_prefixed_mt) | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
end | end | ||
Dòng 735: | Dòng 767: | ||
local err_is_fatal | local err_is_fatal | ||
local combo = collection() | local combo = collection() | ||
− | if | + | if unitcode:find('+', 1, true) then |
err_is_fatal = true | err_is_fatal = true | ||
for item in (unitcode .. '+'):gmatch('%s*(.-)%s*%+') do | for item in (unitcode .. '+'):gmatch('%s*(.-)%s*%+') do | ||
Dòng 755: | Dòng 787: | ||
local cvt = result.combination | local cvt = result.combination | ||
for i, v in ipairs(combo) do | for i, v in ipairs(combo) do | ||
− | local success, t = lookup(v | + | local success, t = lookup(parms, v, 'only_multiple', utable, fails, depth) |
if not success then return false, t end | if not success then return false, t end | ||
if i == 1 then | if i == 1 then | ||
Dòng 774: | Dòng 806: | ||
end | end | ||
end | end | ||
− | if not get_range(unitcode) then | + | -- Accept any unit with an engineering notation prefix like "e6cuft" |
+ | -- (million cubic feet), but not chained prefixes like "e3e6cuft", | ||
+ | -- and not if the unit is a combination or multiple, | ||
+ | -- and not if the unit has an offset or is a built-in. | ||
+ | -- Only en digits are accepted. | ||
+ | local exponent, baseunit = unitcode:match('^e(%d+)(.*)') | ||
+ | if exponent then | ||
+ | local engscale = text_code.eng_scales[exponent] | ||
+ | if engscale then | ||
+ | local success, result = lookup(parms, baseunit, 'no_combination', utable, fails, depth) | ||
+ | if success and not (result.offset or result.builtin or result.engscale) then | ||
+ | result.unitcode = unitcode -- 'e6cuft' not 'cuft' | ||
+ | result.defkey = unitcode -- key to lookup default exception | ||
+ | result.engscale = engscale | ||
+ | result.scale = result.scale * 10 ^ tonumber(exponent) | ||
+ | return true, result | ||
+ | end | ||
+ | end | ||
+ | end | ||
+ | -- Look for x/y; split on right-most slash to get scale correct (x/y/z is x/y per z). | ||
+ | local top, bottom = unitcode:match('^(.-)/([^/]+)$') | ||
+ | if top and not unitcode:find('e%d') then | ||
+ | -- If valid, create an automatic per unit for an "x/y" unit code. | ||
+ | -- The unitcode must not include extraneous spaces. | ||
+ | -- Engineering notation (apart from at start and which has been stripped before here), | ||
+ | -- is not supported so do not make a per unit if find text like 'e3' in unitcode. | ||
+ | local success, result = call_make_per({ per = {top, bottom} }) | ||
+ | if success then | ||
+ | return true, result | ||
+ | end | ||
+ | end | ||
+ | if not parms.opt_ignore_error and not get_range(unitcode) then | ||
+ | -- Want the "what links here" list for the extra_module to show only cases | ||
+ | -- where an extra unit is used, so do not require it if invoked from {{val}} | ||
+ | -- or if looking up a range word which cannot be a unit. | ||
if not extra_units then | if not extra_units then | ||
local success, extra = pcall(function () return require(extra_module).extra_units end) | local success, extra = pcall(function () return require(extra_module).extra_units end) | ||
Dòng 787: | Dòng 853: | ||
fails[unitcode] = true | fails[unitcode] = true | ||
local other = (utable == all_units) and extra_units or all_units | local other = (utable == all_units) and extra_units or all_units | ||
− | local success, result = lookup(unitcode | + | local success, result = lookup(parms, unitcode, what, other, fails, depth) |
if success then | if success then | ||
return true, result | return true, result | ||
Dòng 798: | Dòng 864: | ||
local en_code = ustring.gsub(unitcode, '%d', to_en_table) | local en_code = ustring.gsub(unitcode, '%d', to_en_table) | ||
if en_code ~= unitcode then | if en_code ~= unitcode then | ||
− | return lookup(en_code | + | return lookup(parms, en_code, what, utable, fails, depth) |
end | end | ||
end | end | ||
Dòng 812: | Dòng 878: | ||
return true | return true | ||
end | end | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
Dòng 875: | Dòng 908: | ||
return name:sub(1, pos+1) .. name:sub(pos+2):gsub(' ', '-') | return name:sub(1, pos+1) .. name:sub(pos+2):gsub(' ', '-') | ||
end | end | ||
− | elseif name:sub( | + | elseif name:sub(-1) == ')' then |
pos = name:find('(', 1, true) | pos = name:find('(', 1, true) | ||
if pos then | if pos then | ||
Dòng 926: | Dòng 959: | ||
end | end | ||
return sep .. id .. mid | return sep .. id .. mid | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
Dòng 944: | Dòng 969: | ||
end | end | ||
− | local function | + | local function digit_groups(parms, text, method) |
− | -- Return a table | + | -- Return a numbered table of groups of digits (left-to-right, in local language). |
− | |||
− | |||
− | |||
-- Parameter method is a number or nil: | -- Parameter method is a number or nil: | ||
− | -- 3 for 3-digit grouping, or | + | -- 3 for 3-digit grouping (default), or |
− | -- 2 for 3-then-2 grouping. | + | -- 2 for 3-then-2 grouping (only for digits before decimal mark). |
− | -- | + | local len_right |
− | + | local len_left = text:find('.', 1, true) | |
− | n = 0 | + | if len_left then |
− | add | + | len_right = #text - len_left |
− | + | len_left = len_left - 1 | |
− | + | else | |
− | + | len_left = #text | |
− | + | end | |
− | -- | + | local twos = method == 2 and len_left > 5 |
− | if | + | local groups = collection() |
− | + | local run = len_left | |
− | + | local n | |
− | + | if run < 4 or (run == 4 and parms.opt_comma5) then | |
+ | if parms.opt_gaps then | ||
+ | n = run | ||
+ | else | ||
+ | n = #text | ||
+ | end | ||
+ | elseif twos then | ||
+ | n = run % 2 == 0 and 1 or 2 | ||
+ | else | ||
+ | n = run % 3 == 0 and 3 or run % 3 | ||
+ | end | ||
+ | while run > 0 do | ||
+ | groups:add(n) | ||
+ | run = run - n | ||
+ | n = (twos and run > 3) and 2 or 3 | ||
+ | end | ||
+ | if len_right then | ||
+ | if groups.n == 0 then | ||
+ | groups:add(0) | ||
+ | end | ||
+ | if parms.opt_gaps and len_right > 3 then | ||
+ | local want4 = not parms.opt_gaps3 -- true gives no gap before trailing single digit | ||
+ | local isfirst = true | ||
+ | run = len_right | ||
+ | while run > 0 do | ||
+ | n = (want4 and run == 4) and 4 or (run > 3 and 3 or run) | ||
+ | if isfirst then | ||
+ | isfirst = false | ||
+ | groups[groups.n] = groups[groups.n] + 1 + n | ||
+ | else | ||
+ | groups:add(n) | ||
end | end | ||
− | + | run = run - n | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
− | + | else | |
− | + | groups[groups.n] = groups[groups.n] + 1 + len_right | |
− | + | end | |
− | + | end | |
− | + | local pos = 1 | |
− | + | for i, length in ipairs(groups) do | |
− | + | groups[i] = from_en(text:sub(pos, pos + length - 1)) | |
− | + | pos = pos + length | |
− | + | end | |
− | + | return groups | |
− | end | ||
− | |||
end | end | ||
− | function with_separator(parms, text) | + | function with_separator(parms, text) -- for forward declaration above |
− | -- Input text is a number in en digits | + | -- Input text is a number in en digits with optional '.' decimal mark. |
− | -- Return an equivalent | + | -- Return an equivalent, formatted for display: |
-- with a custom decimal mark instead of '.', if wanted | -- with a custom decimal mark instead of '.', if wanted | ||
-- with thousand separators inserted, if wanted | -- with thousand separators inserted, if wanted | ||
-- digits in local language | -- digits in local language | ||
− | -- The given text is like '123' or ' | + | -- The given text is like '123' or '123.' or '12345.6789'. |
− | |||
-- The text has no sign (caller inserts that later, if necessary). | -- The text has no sign (caller inserts that later, if necessary). | ||
− | -- | + | -- When using gaps, they are inserted before and after the decimal mark. |
− | -- | + | -- Separators are inserted only before the decimal mark. |
− | + | -- A trailing dot (as in '123.') is removed because their use appears to | |
− | + | -- be accidental, and such a number should be shown as '123' or '123.0'. | |
− | + | -- It is useful for convert to suppress the dot so, for example, '4000.' | |
− | + | -- is a simple way of indicating that all the digits are significant. | |
− | + | if text:sub(-1) == '.' then | |
− | + | text = text:sub(1, -2) | |
− | |||
− | |||
end | end | ||
− | if | + | if #text < 4 or parms.opt_nocomma or numsep == '' then |
return from_en(text) | return from_en(text) | ||
end | end | ||
− | local groups = | + | local groups = digit_groups(parms, text, group_method) |
− | + | if parms.opt_gaps then | |
− | + | if groups.n <= 1 then | |
− | local | + | return groups[1] or '' |
− | groups | + | end |
− | + | local nowrap = '<span style="white-space: nowrap">' | |
+ | local gap = '<span style="margin-left: 0.25em">' | ||
+ | local close = '</span>' | ||
+ | return nowrap .. groups[1] .. gap .. table.concat(groups, close .. gap, 2, groups.n) .. close .. close | ||
end | end | ||
− | return groups | + | return table.concat(groups, numsep) |
end | end | ||
− | -- | + | -- An input value like 1.23e12 is displayed using scientific notation (1.23×10¹²). |
− | + | -- That also makes the output use scientific notation, except for small values. | |
− | -- | + | -- In addition, very small or very large output values use scientific notation. |
− | -- Use format(fmtpower, significand, '10', exponent) where each | + | -- Use format(fmtpower, significand, '10', exponent) where each argument is a string. |
local fmtpower = '%s<span style="margin:0 .15em 0 .25em">×</span>%s<sup>%s</sup>' | local fmtpower = '%s<span style="margin:0 .15em 0 .25em">×</span>%s<sup>%s</sup>' | ||
− | local function with_exponent(show, exponent) | + | local function with_exponent(parms, show, exponent) |
-- Return wikitext to display the implied value in scientific notation. | -- Return wikitext to display the implied value in scientific notation. | ||
-- Input uses en digits; output uses digits in local language. | -- Input uses en digits; output uses digits in local language. | ||
− | + | return format(fmtpower, with_separator(parms, show), from_en('10'), use_minus(from_en(tostring(exponent)))) | |
− | |||
− | |||
− | return format(fmtpower, | ||
end | end | ||
Dòng 1.154: | Dòng 1.195: | ||
-- * Uses a custom decimal mark, if wanted. | -- * Uses a custom decimal mark, if wanted. | ||
-- * Has digits grouped where necessary, if wanted. | -- * Has digits grouped where necessary, if wanted. | ||
− | -- * Uses scientific notation for very small or large values | + | -- * Uses scientific notation if requested, or for very small or large values |
− | -- (which forces | + | -- (which forces result to not be spelled). |
-- * Has no more than maxsigfig significant digits | -- * Has no more than maxsigfig significant digits | ||
-- (same as old template and {{#expr}}). | -- (same as old template and {{#expr}}). | ||
+ | local xhi, xlo -- these control when scientific notation (exponent) is used | ||
+ | if parms.opt_scientific then | ||
+ | xhi, xlo = 4, 2 -- default for output if input uses e-notation | ||
+ | elseif parms.opt_scientific_always then | ||
+ | xhi, xlo = 0, 0 -- always use scientific notation (experimental) | ||
+ | else | ||
+ | xhi, xlo = 10, 4 -- default | ||
+ | end | ||
local sign = isnegative and MINUS or '' | local sign = isnegative and MINUS or '' | ||
local maxlen = maxsigfig | local maxlen = maxsigfig | ||
Dòng 1.168: | Dòng 1.217: | ||
if not tfrac and not exponent then | if not tfrac and not exponent then | ||
local integer, dot, decimals = show:match('^(%d*)(%.?)(.*)') | local integer, dot, decimals = show:match('^(%d*)(%.?)(.*)') | ||
− | if | + | if integer == '0' or integer == '' then |
− | |||
− | |||
− | |||
local zeros, figs = decimals:match('^(0*)([^0]?.*)') | local zeros, figs = decimals:match('^(0*)([^0]?.*)') | ||
if #figs == 0 then | if #figs == 0 then | ||
Dòng 1.177: | Dòng 1.223: | ||
show = '0.' .. zeros:sub(1, maxlen) | show = '0.' .. zeros:sub(1, maxlen) | ||
end | end | ||
− | elseif #zeros >= | + | elseif #zeros >= xlo then |
show = figs | show = figs | ||
exponent = -#zeros | exponent = -#zeros | ||
Dòng 1.183: | Dòng 1.229: | ||
show = '0.' .. zeros .. figs:sub(1, maxlen) | show = '0.' .. zeros .. figs:sub(1, maxlen) | ||
end | end | ||
+ | elseif #integer >= xhi then | ||
+ | show = integer .. decimals | ||
+ | exponent = #integer | ||
else | else | ||
maxlen = maxlen + #dot | maxlen = maxlen + #dot | ||
Dòng 1.191: | Dòng 1.240: | ||
end | end | ||
if exponent then | if exponent then | ||
+ | local function zeros(n) | ||
+ | return string.rep('0', n) | ||
+ | end | ||
if #show > maxlen then | if #show > maxlen then | ||
show = show:sub(1, maxlen) | show = show:sub(1, maxlen) | ||
end | end | ||
− | if exponent > | + | if exponent > xhi or exponent <= -xlo or (exponent == xhi and show ~= '1' .. zeros(xhi - 1)) then |
− | -- | + | -- When xhi, xlo = 10, 4 (the default), scientific notation is used if the |
+ | -- rounded value satisfies: value >= 1e9 or value < 1e-4 (1e9 = 0.1e10), | ||
+ | -- except if show is '1000000000' (1e9), for example: | ||
+ | -- {{convert|1000000000|m|m|sigfig=10}} → 1,000,000,000 metres (1,000,000,000 m) | ||
+ | local significand | ||
+ | if #show > 1 then | ||
+ | significand = show:sub(1, 1) .. '.' .. show:sub(2) | ||
+ | else | ||
+ | significand = show | ||
+ | end | ||
return { | return { | ||
clean = '.' .. show, | clean = '.' .. show, | ||
exponent = exponent, | exponent = exponent, | ||
sign = sign, | sign = sign, | ||
− | show = sign .. with_exponent( | + | show = sign .. with_exponent(parms, significand, exponent-1), |
is_scientific = true, | is_scientific = true, | ||
} | } | ||
end | end | ||
if exponent >= #show then | if exponent >= #show then | ||
− | show = show .. | + | show = show .. zeros(exponent - #show) -- result has no dot |
elseif exponent <= 0 then | elseif exponent <= 0 then | ||
− | show = '0.' .. | + | show = '0.' .. zeros(-exponent) .. show |
else | else | ||
show = show:sub(1, exponent) .. '.' .. show:sub(exponent+1) | show = show:sub(1, exponent) .. '.' .. show:sub(exponent+1) | ||
Dòng 1.235: | Dòng 1.296: | ||
local function extract_fraction(parms, text, negative) | local function extract_fraction(parms, text, negative) | ||
-- If text represents a fraction, return | -- If text represents a fraction, return | ||
− | -- value, altvalue, show | + | -- value, altvalue, show, denominator |
-- where | -- where | ||
-- value is a number (value of the fraction in argument text) | -- value is a number (value of the fraction in argument text) | ||
-- altvalue is an alternate interpretation of any fraction for the hands | -- altvalue is an alternate interpretation of any fraction for the hands | ||
− | -- unit where " | + | -- unit where "12.1+3/4" means 12 hands 1.75 inches |
-- show is a string (formatted text for display of an input value, | -- show is a string (formatted text for display of an input value, | ||
-- and is spelled if wanted and possible) | -- and is spelled if wanted and possible) | ||
− | |||
-- denominator is value of the denominator in the fraction | -- denominator is value of the denominator in the fraction | ||
-- Otherwise, return nil. | -- Otherwise, return nil. | ||
-- Input uses en digits and '.' decimal mark (input has been translated). | -- Input uses en digits and '.' decimal mark (input has been translated). | ||
− | -- Output uses digits in local language and | + | -- Output uses digits in local language and local decimal mark, if any. |
− | -- | + | ------------------------------------------------------------------------ |
− | -- | + | -- Originally this function accepted x+y/z where x, y, z were any valid |
− | -- | + | -- numbers, possibly with a sign. For example '1.23e+2+1.2/2.4' = 123.5, |
− | -- | + | -- and '2-3/8' = 1.625. However, such usages were found to be errors or |
− | -- | + | -- misunderstandings, so since August 2014 the following restrictions apply: |
− | -- | + | -- x (if present) is an integer or has a single digit after decimal mark |
− | -- | + | -- y and z are unsigned integers |
− | -- | + | -- e-notation is not accepted |
− | -- 2 | + | -- The overall number can start with '+' or '-' (so '12+3/4' and '+12+3/4' |
− | -- 1 + | + | -- and '-12-3/4' are valid). |
− | -- 1 | + | -- Any leading negative sign is removed by the caller, so only inputs |
− | -- | + | -- like the following are accepted here (may have whitespace): |
− | -- ( | + | -- negative = false false true (there was a leading '-') |
− | -- | + | -- text = '2/3' '+2/3' '2/3' |
− | + | -- text = '1+2/3' '+1+2/3' '1-2/3' | |
− | + | -- text = '12.3+1/2' '+12.3+1/2' '12.3-1/2' | |
+ | -- Values like '12.3+1/2' are accepted, but are intended only for use | ||
+ | -- with the hands unit (not worth adding code to enforce that). | ||
+ | ------------------------------------------------------------------------ | ||
+ | local leading_plus, prefix, numstr, slashes, denstr = | ||
+ | text:match('^%s*(%+?)%s*(.-)%s*(%d+)%s*(/+)%s*(%d+)%s*$') | ||
+ | if not leading_plus then | ||
+ | -- Accept a single U+2044 fraction slash because that may be pasted. | ||
+ | leading_plus, prefix, numstr, denstr = | ||
+ | text:match('^%s*(%+?)%s*(.-)%s*(%d+)%s*⁄%s*(%d+)%s*$') | ||
+ | slashes = '/' | ||
+ | end | ||
+ | local numerator = tonumber(numstr) | ||
local denominator = tonumber(denstr) | local denominator = tonumber(denstr) | ||
− | if denominator == nil then return nil end | + | if numerator == nil or denominator == nil or (negative and leading_plus ~= '') then |
− | local wholestr | + | return nil |
− | if | + | end |
− | wholestr = | + | local whole, wholestr |
+ | if prefix == '' then | ||
+ | wholestr = '' | ||
whole = 0 | whole = 0 | ||
− | |||
else | else | ||
+ | -- Any prefix must be like '12+' or '12-' (whole number and fraction sign); | ||
+ | -- '12.3+' and '12.3-' are also accepted (single digit after decimal point) | ||
+ | -- because '12.3+1/2 hands' is valid (12 hands 3½ inches). | ||
+ | local num1, num2, frac_sign = prefix:match('^(%d+)(%.?%d?)%s*([+%-])$') | ||
+ | if num1 == nil then return nil end | ||
+ | if num2 == '' then -- num2 must be '' or like '.1' but not '.' or '.12' | ||
+ | wholestr = num1 | ||
+ | else | ||
+ | if #num2 ~= 2 then return nil end | ||
+ | wholestr = num1 .. num2 | ||
+ | end | ||
+ | if frac_sign ~= (negative and '-' or '+') then return nil end | ||
whole = tonumber(wholestr) | whole = tonumber(wholestr) | ||
if whole == nil then return nil end | if whole == nil then return nil end | ||
− | |||
end | end | ||
− | + | local value = whole + numerator / denominator | |
− | local | + | if not valid_number(value) then return nil end |
− | + | local altvalue = whole + numerator / (denominator * 10) | |
− | + | local style = #slashes -- kludge: 1 or 2 slashes can be used to select style | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | local style = # | ||
if style > 2 then style = 2 end | if style > 2 then style = 2 end | ||
− | local wikitext = format_fraction(parms, 'in', negative, wholestr, numstr, denstr, | + | local wikitext = format_fraction(parms, 'in', negative, leading_plus .. wholestr, numstr, denstr, parms.opt_spell_in, style) |
− | return value, altvalue, wikitext | + | return value, altvalue, wikitext, denominator |
end | end | ||
Dòng 1.310: | Dòng 1.372: | ||
-- where info is a table with the result, | -- where info is a table with the result, | ||
-- or return false, t where t is an error message table. | -- or return false, t where t is an error message table. | ||
− | -- Input can use en digits or digits in local language. | + | -- Input can use en digits or digits in local language and can |
+ | -- have references at the end. Accepting references is intended | ||
+ | -- for use in infoboxes with a field for a value passed to convert. | ||
-- Parameter another = true if the expected value is not the first. | -- Parameter another = true if the expected value is not the first. | ||
-- Before processing, the input text is cleaned: | -- Before processing, the input text is cleaned: | ||
-- * Any thousand separators (valid or not) are removed. | -- * Any thousand separators (valid or not) are removed. | ||
− | -- * Any sign | + | -- * Any sign is replaced with '-' (if negative) or '' (otherwise). |
− | |||
-- That replaces Unicode minus with '-'. | -- That replaces Unicode minus with '-'. | ||
-- If successful, the returned info table contains named fields: | -- If successful, the returned info table contains named fields: | ||
Dòng 1.321: | Dòng 1.384: | ||
-- altvalue = a valid number, usually same as value but different | -- altvalue = a valid number, usually same as value but different | ||
-- if fraction used (for hands unit) | -- if fraction used (for hands unit) | ||
− | -- singular = true if value is 1 (to use singular form of units | + | -- singular = true if value is 1 or -1 (to use singular form of units) |
− | |||
-- clean = cleaned text with any separators and sign removed | -- clean = cleaned text with any separators and sign removed | ||
-- (en digits and '.' decimal mark) | -- (en digits and '.' decimal mark) | ||
− | -- show = text formatted for output | + | -- show = text formatted for output, possibly with ref strip markers |
-- (digits in local language and custom decimal mark) | -- (digits in local language and custom decimal mark) | ||
-- The resulting show: | -- The resulting show: | ||
Dòng 1.334: | Dòng 1.396: | ||
-- '+' (if the input text used '+'), or is '' (if no sign in input). | -- '+' (if the input text used '+'), or is '' (if no sign in input). | ||
text = strip(text or '') | text = strip(text or '') | ||
+ | local reference | ||
+ | local pos = text:find('\127', 1, true) | ||
+ | if pos then | ||
+ | local before = text:sub(1, pos - 1) | ||
+ | local remainder = text:sub(pos) | ||
+ | local refs = {} | ||
+ | while #remainder > 0 do | ||
+ | local ref, spaces | ||
+ | ref, spaces, remainder = remainder:match('^(\127[^\127]*UNIQ[^\127]*%-ref[^\127]*\127)(%s*)(.*)') | ||
+ | if ref then | ||
+ | table.insert(refs, ref) | ||
+ | else | ||
+ | refs = {} | ||
+ | break | ||
+ | end | ||
+ | end | ||
+ | if #refs > 0 then | ||
+ | text = strip(before) | ||
+ | reference = table.concat(refs) | ||
+ | end | ||
+ | end | ||
local clean = to_en(text, parms) | local clean = to_en(text, parms) | ||
if clean == '' then | if clean == '' then | ||
Dòng 1.355: | Dòng 1.438: | ||
local valstr | local valstr | ||
for _, prefix in ipairs({ '-', MINUS, '−' }) do | for _, prefix in ipairs({ '-', MINUS, '−' }) do | ||
− | -- Including '-' | + | -- Including '-' sets isnegative in case input is a fraction like '-2-3/4'. |
− | |||
local plen = #prefix | local plen = #prefix | ||
if clean:sub(1, plen) == prefix then | if clean:sub(1, plen) == prefix then | ||
valstr = clean:sub(plen + 1) | valstr = clean:sub(plen + 1) | ||
+ | if valstr:match('^%s') then -- "- 1" is invalid but "-1 - 1/2" is ok | ||
+ | return false, { 'cvt_bad_num', text } | ||
+ | end | ||
break | break | ||
end | end | ||
Dòng 1.370: | Dòng 1.455: | ||
end | end | ||
if value == nil then | if value == nil then | ||
− | |||
if not no_fraction then | if not no_fraction then | ||
− | value, altvalue, show | + | value, altvalue, show, denominator = extract_fraction(parms, clean, isnegative) |
end | end | ||
if value == nil then | if value == nil then | ||
Dòng 1.386: | Dòng 1.470: | ||
end | end | ||
if show == nil then | if show == nil then | ||
− | + | -- clean is a non-empty string with no spaces, and does not represent a fraction, | |
− | + | -- and value = tonumber(clean) is a number >= 0. | |
− | + | -- If the input uses e-notation, show will be displayed using a power of ten, but | |
− | + | -- we use the number as given so it might not be normalized scientific notation. | |
− | + | -- The input value is spelled if specified so any e-notation is ignored; | |
+ | -- that allows input like 2e6 to be spelled as "two million" which works | ||
+ | -- because the spell module converts '2e6' to '2000000' before spelling. | ||
+ | local function rounded(value, default, exponent) | ||
+ | local precision = parms.opt_ri | ||
+ | if precision then | ||
+ | local fmt = '%.' .. format('%d', precision) .. 'f' | ||
+ | local result = fmt:format(tonumber(value) + 2e-14) -- fudge for some common cases of bad rounding | ||
+ | if not exponent then | ||
+ | singular = (tonumber(result) == 1) | ||
+ | end | ||
+ | return result | ||
+ | end | ||
+ | return default | ||
+ | end | ||
+ | singular = (value == 1) | ||
+ | local scientific | ||
+ | local significand, exponent = clean:match('^([%d.]+)[Ee]([+%-]?%d+)') | ||
+ | if significand then | ||
+ | show = with_exponent(parms, rounded(significand, significand, exponent), exponent) | ||
+ | scientific = true | ||
else | else | ||
− | show = clean | + | show = with_separator(parms, rounded(value, clean)) |
end | end | ||
− | show = propersign .. | + | show = propersign .. show |
if parms.opt_spell_in then | if parms.opt_spell_in then | ||
− | show = spell_number(parms, 'in', propersign .. clean) or show | + | show = spell_number(parms, 'in', propersign .. rounded(value, clean)) or show |
+ | scientific = false | ||
+ | end | ||
+ | if scientific then | ||
+ | parms.opt_scientific = true | ||
end | end | ||
end | end | ||
− | |||
if isnegative and (value ~= 0) then | if isnegative and (value ~= 0) then | ||
value = -value | value = -value | ||
− | altvalue = -altvalue | + | altvalue = -(altvalue or value) |
end | end | ||
return true, { | return true, { | ||
value = value, | value = value, | ||
− | altvalue = altvalue, | + | altvalue = altvalue or value, |
singular = singular, | singular = singular, | ||
clean = clean, | clean = clean, | ||
− | show = show, | + | show = show .. (reference or ''), |
denominator = denominator, | denominator = denominator, | ||
} | } | ||
Dòng 1.425: | Dòng 1.532: | ||
local number = tonumber(to_en(text)) | local number = tonumber(to_en(text)) | ||
if number then | if number then | ||
− | local | + | local _, fracpart = math.modf(number) |
return number, (fracpart == 0) | return number, (fracpart == 0) | ||
end | end | ||
Dòng 1.503: | Dòng 1.610: | ||
-- p2 is text to insert before the output unit | -- p2 is text to insert before the output unit | ||
-- p1 or p2 may be nil to mean "no preunit" | -- p1 or p2 may be nil to mean "no preunit" | ||
− | -- Using '+ ' gives output like "5+ feet" (no | + | -- Using '+' gives output like "5+ feet" (no space before, but space after). |
− | local function withspace(text, | + | local function withspace(text, wantboth) |
− | -- | + | -- Return text with space before and, if wantboth, after. |
− | -- However, no space is | + | -- However, no space is added if there is a space or ' ' or '-' |
− | -- | + | -- at that position ('-' is for adjectival text). |
− | local | + | -- There is also no space if text starts with '&' |
− | if | + | -- (e.g. '°' would display a degree symbol with no preceding space). |
− | return text | + | local char = text:sub(1, 1) |
+ | if char == '&' then | ||
+ | return text -- an html entity can be used to specify the exact display | ||
end | end | ||
− | if | + | if not (char == ' ' or char == '-' or char == '+') then |
− | + | text = ' ' .. text | |
− | |||
− | |||
end | end | ||
− | if | + | if wantboth then |
− | + | char = text:sub(-1, -1) | |
+ | if not (char == ' ' or char == '-' or text:sub(-6, -1) == ' ') then | ||
+ | text = text .. ' ' | ||
+ | end | ||
end | end | ||
− | + | return text | |
− | |||
− | |||
− | |||
end | end | ||
+ | local PLUS = '+ ' | ||
preunit1 = preunit1 or '' | preunit1 = preunit1 or '' | ||
local trim1 = strip(preunit1) | local trim1 = strip(preunit1) | ||
Dòng 1.531: | Dòng 1.639: | ||
return nil | return nil | ||
end | end | ||
− | return | + | if trim1 == '+' then |
+ | return PLUS | ||
+ | end | ||
+ | return withspace(preunit1, true) | ||
end | end | ||
+ | preunit1 = withspace(preunit1) | ||
preunit2 = preunit2 or '' | preunit2 = preunit2 or '' | ||
local trim2 = strip(preunit2) | local trim2 = strip(preunit2) | ||
− | if trim1 == '' | + | if trim1 == '+' then |
− | + | if trim2 == '' or trim2 == '+' then | |
+ | return PLUS, PLUS | ||
+ | end | ||
+ | preunit1 = PLUS | ||
end | end | ||
− | if trim1 | + | if trim2 == '' then |
− | + | if trim1 == '' then | |
− | + | return nil, nil | |
− | + | end | |
+ | preunit2 = preunit1 | ||
+ | elseif trim2 == '+' then | ||
+ | preunit2 = PLUS | ||
+ | elseif trim2 == ' ' then -- trick to make preunit2 empty | ||
preunit2 = nil | preunit2 = nil | ||
− | + | else | |
− | + | preunit2 = withspace(preunit2) | |
− | |||
− | preunit2 = withspace(preunit2 | ||
end | end | ||
return preunit1, preunit2 | return preunit1, preunit2 | ||
end | end | ||
− | local function range_text(range, want_name, parms, before, after) | + | local function range_text(range, want_name, parms, before, after, inout) |
-- Return before .. rtext .. after | -- Return before .. rtext .. after | ||
-- where rtext is the text that separates two values in a range. | -- where rtext is the text that separates two values in a range. | ||
local rtext, adj_text, exception | local rtext, adj_text, exception | ||
if type(range) == 'table' then | if type(range) == 'table' then | ||
− | -- Table must specify range text for | + | -- Table must specify range text for ('off' and 'on') or ('input' and 'output'), |
-- and may specify range text for 'adj=on', | -- and may specify range text for 'adj=on', | ||
-- and may specify exception = true. | -- and may specify exception = true. | ||
− | rtext = range[want_name and 'off' or 'on'] | + | rtext = range[want_name and 'off' or 'on'] or |
+ | range[((inout == 'in') == (parms.opt_flip == true)) and 'output' or 'input'] | ||
adj_text = range['adj'] | adj_text = range['adj'] | ||
exception = range['exception'] | exception = range['exception'] | ||
Dòng 1.576: | Dòng 1.694: | ||
end | end | ||
− | local function get_composite(parms, iparm | + | local function get_composite(parms, iparm, in_unit_table) |
− | -- Look for a composite input unit. For example, | + | -- Look for a composite input unit. For example, {{convert|1|yd|2|ft|3|in}} |
-- would result in a call to this function with | -- would result in a call to this function with | ||
-- iparm = 3 (parms[iparm] = "2", just after the first unit) | -- iparm = 3 (parms[iparm] = "2", just after the first unit) | ||
− | + | -- in_unit_table = (unit table for "yd"; contains value 1 for number of yards) | |
− | -- in_unit_table = (unit table for "yd") | ||
-- Return true, iparm, unit where | -- Return true, iparm, unit where | ||
-- iparm = index just after the composite units (7 in above example) | -- iparm = index just after the composite units (7 in above example) | ||
Dòng 1.590: | Dòng 1.707: | ||
local composite_units, count = { in_unit_table }, 1 | local composite_units, count = { in_unit_table }, 1 | ||
local fixups = {} | local fixups = {} | ||
+ | local total = in_unit_table.valinfo[1].value | ||
local subunit = in_unit_table | local subunit = in_unit_table | ||
while subunit.subdivs do -- subdivs is nil or a table of allowed subdivisions | while subunit.subdivs do -- subdivs is nil or a table of allowed subdivisions | ||
local subcode = strip(parms[iparm+1]) | local subcode = strip(parms[iparm+1]) | ||
− | local subdiv = subunit.subdivs[subcode] | + | local subdiv = subunit.subdivs[subcode] or subunit.subdivs[(all_units[subcode] or {}).target] |
if not subdiv then | if not subdiv then | ||
break | break | ||
end | end | ||
local success | local success | ||
− | success, subunit = lookup(subcode | + | success, subunit = lookup(parms, subcode, 'no_combination') |
if not success then return false, subunit end -- should never occur | if not success then return false, subunit end -- should never occur | ||
success, subinfo = extract_number(parms, parms[iparm]) | success, subinfo = extract_number(parms, parms[iparm]) | ||
Dòng 1.626: | Dòng 1.744: | ||
composite_units[i].fixed_name = name | composite_units[i].fixed_name = name | ||
else | else | ||
− | local success, alternate = lookup(unit | + | local success, alternate = lookup(parms, unit, 'no_combination') |
if not success then return false, alternate end -- should never occur | if not success then return false, alternate end -- should never occur | ||
alternate.inout = 'in' | alternate.inout = 'in' | ||
Dòng 1.647: | Dòng 1.765: | ||
-- Also, checks are performed which may display warnings, if enabled. | -- Also, checks are performed which may display warnings, if enabled. | ||
-- Return true if successful or return false, t where t is an error message table. | -- Return true if successful or return false, t where t is an error message table. | ||
+ | currency_text = nil -- local testing can hold module in memory; must clear globals | ||
+ | local accept_any_text = { | ||
+ | input = true, | ||
+ | qid = true, | ||
+ | qual = true, | ||
+ | stylein = true, | ||
+ | styleout = true, | ||
+ | tracking = true, | ||
+ | } | ||
if kv_pairs.adj and kv_pairs.sing then | if kv_pairs.adj and kv_pairs.sing then | ||
-- For enwiki (before translation), warn if attempt to use adj and sing | -- For enwiki (before translation), warn if attempt to use adj and sing | ||
Dòng 1.655: | Dòng 1.782: | ||
kv_pairs.sing = nil | kv_pairs.sing = nil | ||
end | end | ||
+ | kv_pairs.comma = kv_pairs.comma or config.comma -- for plwiki who want default comma=5 | ||
for loc_name, loc_value in pairs(kv_pairs) do | for loc_name, loc_value in pairs(kv_pairs) do | ||
local en_name = text_code.en_option_name[loc_name] | local en_name = text_code.en_option_name[loc_name] | ||
if en_name then | if en_name then | ||
local en_value | local en_value | ||
− | if en_name == 'frac' or en_name == 'sigfig' then | + | if en_name == '$' or en_name == 'frac' or en_name == 'sigfig' then |
if loc_value == '' then | if loc_value == '' then | ||
add_warning(parms, 2, 'cvt_empty_option', loc_name) | add_warning(parms, 2, 'cvt_empty_option', loc_name) | ||
+ | elseif en_name == '$' then | ||
+ | -- Value should be a single character like "€" for the euro currency symbol, but anything is accepted. | ||
+ | currency_text = (loc_value == 'euro') and '€' or loc_value | ||
else | else | ||
local minimum | local minimum | ||
Dòng 1.677: | Dòng 1.808: | ||
en_value = number | en_value = number | ||
else | else | ||
− | add_warning(parms, 1, (en_name == 'frac' and 'cvt_bad_frac' or 'cvt_bad_sigfig'), loc_value) | + | add_warning(parms, 1, (en_name == 'frac' and 'cvt_bad_frac' or 'cvt_bad_sigfig'), loc_name .. '=' .. loc_value) |
end | end | ||
+ | end | ||
+ | elseif accept_any_text[en_name] then | ||
+ | en_value = loc_value ~= '' and loc_value or nil -- accept non-empty user text with no validation | ||
+ | if en_name == 'input' then | ||
+ | -- May have something like {{convert|input=}} (empty input) if source is an infobox | ||
+ | -- with optional fields. In that case, want to output nothing rather than an error. | ||
+ | parms.input_text = loc_value -- keep input because parms.input is nil if loc_value == '' | ||
end | end | ||
else | else | ||
en_value = text_code.en_option_value[en_name][loc_value] | en_value = text_code.en_option_value[en_name][loc_value] | ||
+ | if en_value and en_value:sub(-1) == '?' then | ||
+ | en_value = en_value:sub(1, -2) | ||
+ | add_warning(parms, -1, 'cvt_deprecated', loc_name .. '=' .. loc_value) | ||
+ | end | ||
if en_value == nil then | if en_value == nil then | ||
if loc_value == '' then | if loc_value == '' then | ||
add_warning(parms, 2, 'cvt_empty_option', loc_name) | add_warning(parms, 2, 'cvt_empty_option', loc_name) | ||
else | else | ||
− | + | add_warning(parms, 1, 'cvt_unknown_option', loc_name .. '=' .. loc_value) | |
− | |||
− | |||
− | |||
end | end | ||
elseif en_value == '' then | elseif en_value == '' then | ||
Dòng 1.695: | Dòng 1.834: | ||
elseif type(en_value) == 'string' and en_value:sub(1, 4) == 'opt_' then | elseif type(en_value) == 'string' and en_value:sub(1, 4) == 'opt_' then | ||
for _, v in ipairs(split(en_value, ',')) do | for _, v in ipairs(split(en_value, ',')) do | ||
− | parms[v] = true | + | local lhs, rhs = v:match('^(.-)=(.+)$') |
+ | if rhs then | ||
+ | parms[lhs] = tonumber(rhs) or rhs | ||
+ | else | ||
+ | parms[v] = true | ||
+ | end | ||
end | end | ||
en_value = nil | en_value = nil | ||
Dòng 1.705: | Dòng 1.849: | ||
end | end | ||
end | end | ||
− | + | local abbr_entered = parms.abbr | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
local cfg_abbr = config.abbr | local cfg_abbr = config.abbr | ||
if cfg_abbr then | if cfg_abbr then | ||
Dòng 1.729: | Dòng 1.866: | ||
end | end | ||
if parms.abbr then | if parms.abbr then | ||
− | parms.abbr_org = parms.abbr -- original abbr | + | if parms.abbr == 'unit' then |
+ | parms.abbr = 'on' | ||
+ | parms.number_word = true | ||
+ | end | ||
+ | parms.abbr_org = parms.abbr -- original abbr, before any flip | ||
elseif parms.opt_hand_hh then | elseif parms.opt_hand_hh then | ||
parms.abbr_org = 'on' | parms.abbr_org = 'on' | ||
Dòng 1.735: | Dòng 1.876: | ||
else | else | ||
parms.abbr = 'out' -- default is to abbreviate output only (use symbol, not name) | parms.abbr = 'out' -- default is to abbreviate output only (use symbol, not name) | ||
+ | end | ||
+ | if parms.opt_order_out then | ||
+ | -- Disable options that do not work in a useful way with order=out. | ||
+ | parms.opt_flip = nil -- override adj=flip | ||
+ | parms.opt_spell_in = nil | ||
+ | parms.opt_spell_out = nil | ||
+ | parms.opt_spell_upper = nil | ||
+ | end | ||
+ | if parms.opt_spell_out and not abbr_entered then | ||
+ | parms.abbr = 'off' -- should show unit name when spelling the output value | ||
end | end | ||
if parms.opt_flip then | if parms.opt_flip then | ||
Dòng 1.758: | Dòng 1.909: | ||
end | end | ||
if parms.opt_table or parms.opt_tablecen then | if parms.opt_table or parms.opt_tablecen then | ||
− | if | + | if abbr_entered == nil and parms.lk == nil then |
parms.opt_values = true | parms.opt_values = true | ||
end | end | ||
− | + | parms.table_align = parms.opt_table and 'right' or 'center' | |
− | parms. | + | end |
+ | if parms.table_align or parms.opt_sortable_on then | ||
+ | parms.need_table_or_sort = true | ||
end | end | ||
local disp_joins = text_code.disp_joins | local disp_joins = text_code.disp_joins | ||
Dòng 1.776: | Dòng 1.929: | ||
local abbr = parms.abbr | local abbr = parms.abbr | ||
if disp == 'slash' then | if disp == 'slash' then | ||
− | if | + | if abbr_entered == nil then |
disp = 'slash-nbsp' | disp = 'slash-nbsp' | ||
elseif abbr == 'in' or abbr == 'out' then | elseif abbr == 'in' or abbr == 'out' then | ||
Dòng 1.792: | Dòng 1.945: | ||
parms.joins = disp_joins[disp] or default_joins | parms.joins = disp_joins[disp] or default_joins | ||
parms.join_between = parms.joins[3] or parms.join_between | parms.join_between = parms.joins[3] or parms.join_between | ||
+ | parms.wantname = parms.joins.wantname | ||
end | end | ||
if (en_default and not parms.opt_lang_local and (parms[1] or ''):find('%d')) or parms.opt_lang_en then | if (en_default and not parms.opt_lang_local and (parms[1] or ''):find('%d')) or parms.opt_lang_en then | ||
Dòng 1.821: | Dòng 1.975: | ||
-- If the parameter is not a value, try unpacking it as a range ("1-23" for "1 to 23"). | -- If the parameter is not a value, try unpacking it as a range ("1-23" for "1 to 23"). | ||
-- However, "-1-2/3" is a negative fraction (-1⅔), so it must be extracted first. | -- However, "-1-2/3" is a negative fraction (-1⅔), so it must be extracted first. | ||
+ | -- Do not unpack a parameter if it is like "3-1/2" which is sometimes incorrectly | ||
+ | -- used instead of "3+1/2" (and which should not be interpreted as "3 to ½"). | ||
-- Unpacked items are inserted into the parms table. | -- Unpacked items are inserted into the parms table. | ||
+ | -- The tail recursion allows combinations like "1x2 to 3x4". | ||
local valstr = strip(parms[i]) -- trim so any '-' as a negative sign will be at start | local valstr = strip(parms[i]) -- trim so any '-' as a negative sign will be at start | ||
local success, result = extract_number(parms, valstr, i > 1) | local success, result = extract_number(parms, valstr, i > 1) | ||
if not success and valstr and i < 20 then -- check i to limit abuse | if not success and valstr and i < 20 then -- check i to limit abuse | ||
− | for _, sep in ipairs(text_code.ranges.words) do | + | local lhs, sep, rhs = valstr:match('^(%S+)%s+(%S+)%s+(%S.*)') |
− | + | if lhs and not (sep == '-' and rhs:match('/')) then | |
− | + | if sep:find('%d') then | |
− | + | return success, result -- to reject {{convert|1 234 567|m}} with a decent message (en only) | |
− | + | end | |
− | + | parms[i] = rhs | |
− | + | table.insert(parms, i, sep) | |
+ | table.insert(parms, i, lhs) | ||
+ | return extractor(i) | ||
+ | end | ||
+ | if not valstr:match('%-.*/') then | ||
+ | for _, sep in ipairs(text_code.ranges.words) do | ||
+ | local start, stop = valstr:find(sep, 2, true) -- start at 2 to skip any negative sign for range '-' | ||
+ | if start then | ||
+ | parms[i] = valstr:sub(stop + 1) | ||
+ | table.insert(parms, i, sep) | ||
+ | table.insert(parms, i, valstr:sub(1, start - 1)) | ||
+ | return extractor(i) | ||
+ | end | ||
end | end | ||
end | end | ||
Dòng 1.848: | Dòng 2.017: | ||
end | end | ||
valinfo:add(info) | valinfo:add(info) | ||
− | local | + | local range_item = get_range(strip(parms[i])) |
− | |||
if not range_item then | if not range_item then | ||
break | break | ||
Dòng 1.856: | Dòng 2.024: | ||
range:add(range_item) | range:add(range_item) | ||
if type(range_item) == 'table' then | if type(range_item) == 'table' then | ||
− | parms. | + | -- For range "x", if append unit to some values, append it to all. |
+ | parms.in_range_x = parms.in_range_x or range_item.in_range_x | ||
+ | parms.out_range_x = parms.out_range_x or range_item.out_range_x | ||
+ | parms.abbr_range_x = parms.abbr_range_x or range_item.abbr_range_x | ||
is_change = range_item.is_range_change | is_change = range_item.is_range_change | ||
end | end | ||
Dòng 1.873: | Dòng 2.044: | ||
local function simple_get_values(parms) | local function simple_get_values(parms) | ||
-- If input is like "{{convert|valid_value|valid_unit|...}}", | -- If input is like "{{convert|valid_value|valid_unit|...}}", | ||
− | -- return true, | + | -- return true, i, in_unit, in_unit_table |
− | -- | + | -- i = index in parms of what follows valid_unit, if anything. |
− | |||
-- The valid_value is not negative and does not use a fraction, and | -- The valid_value is not negative and does not use a fraction, and | ||
-- no options requiring further processing of the input are used. | -- no options requiring further processing of the input are used. | ||
− | -- Otherwise, return nothing | + | -- Otherwise, return nothing or return false, parm1 for caller to interpret. |
-- Testing shows this function is successful for 96% of converts in articles, | -- Testing shows this function is successful for 96% of converts in articles, | ||
-- and that on average it speeds up converts by 8%. | -- and that on average it speeds up converts by 8%. | ||
− | |||
local clean = to_en(strip(parms[1] or ''), parms) | local clean = to_en(strip(parms[1] or ''), parms) | ||
− | if #clean > 10 or not clean:match('^[0-9.]+$') then return end | + | if parms.opt_ri or parms.opt_spell_in or #clean > 10 or not clean:match('^[0-9.]+$') then |
+ | return false, clean | ||
+ | end | ||
local value = tonumber(clean) | local value = tonumber(clean) | ||
if not value then return end | if not value then return end | ||
Dòng 1.894: | Dòng 2.065: | ||
} | } | ||
local in_unit = strip(parms[2]) | local in_unit = strip(parms[2]) | ||
− | local success, in_unit_table = lookup(in_unit | + | local success, in_unit_table = lookup(parms, in_unit, 'no_combination') |
if not success then return end | if not success then return end | ||
− | + | in_unit_table.valinfo = { info } | |
+ | return true, 3, in_unit, in_unit_table | ||
end | end | ||
− | local function get_parms( | + | local function wikidata_call(parms, operation, ...) |
− | -- If successful, return true | + | -- Return true, s where s is the result of a Wikidata operation, |
+ | -- or return false, t where t is an error message table. | ||
+ | local function worker(...) | ||
+ | wikidata_code = wikidata_code or require(wikidata_module) | ||
+ | wikidata_data = wikidata_data or mw.loadData(wikidata_data_module) | ||
+ | return wikidata_code[operation](wikidata_data, ...) | ||
+ | end | ||
+ | local success, status, result = pcall(worker, ...) | ||
+ | if success then | ||
+ | return status, result | ||
+ | end | ||
+ | if parms.opt_sortable_debug then | ||
+ | -- Use debug=yes to crash if an error while accessing Wikidata. | ||
+ | error('Error accessing Wikidata: ' .. status, 0) | ||
+ | end | ||
+ | return false, { 'cvt_wd_fail' } | ||
+ | end | ||
+ | |||
+ | local function get_parms(parms, args) | ||
+ | -- If successful, update parms and return true, unit where | ||
-- parms is a table of all arguments passed to the template | -- parms is a table of all arguments passed to the template | ||
-- converted to named arguments, and | -- converted to named arguments, and | ||
-- unit is the input unit table; | -- unit is the input unit table; | ||
-- or return false, t where t is an error message table. | -- or return false, t where t is an error message table. | ||
+ | -- For special processing (not a convert), can also return | ||
+ | -- true, wikitext where wikitext is the final result. | ||
-- The returned input unit table may be for a fake unit using the specified | -- The returned input unit table may be for a fake unit using the specified | ||
-- unit code as the symbol and name, and with bad_mcode = message code table. | -- unit code as the symbol and name, and with bad_mcode = message code table. | ||
Dòng 1.911: | Dòng 2.104: | ||
-- whitespace entered in the template, and whitespace is used by some | -- whitespace entered in the template, and whitespace is used by some | ||
-- parameters (example: the numbered parameters associated with "disp=x"). | -- parameters (example: the numbered parameters associated with "disp=x"). | ||
− | |||
local kv_pairs = {} -- table of input key:value pairs where key is a name; needed because cannot iterate parms and add new fields to it | local kv_pairs = {} -- table of input key:value pairs where key is a name; needed because cannot iterate parms and add new fields to it | ||
− | for k, v in pairs( | + | for k, v in pairs(args) do |
if type(k) == 'number' or k == 'test' then -- parameter "test" is reserved for testing and is not translated | if type(k) == 'number' or k == 'test' then -- parameter "test" is reserved for testing and is not translated | ||
parms[k] = v | parms[k] = v | ||
Dòng 1.919: | Dòng 2.111: | ||
kv_pairs[k] = v | kv_pairs[k] = v | ||
end | end | ||
+ | end | ||
+ | if parms.test == 'wikidata' then | ||
+ | local ulookup = function (ucode) | ||
+ | -- Use empty table for parms so it does not accumulate results when used repeatedly. | ||
+ | return lookup({}, ucode, 'no_combination') | ||
+ | end | ||
+ | return wikidata_call(parms, '_listunits', ulookup) | ||
end | end | ||
local success, msg = translate_parms(parms, kv_pairs) | local success, msg = translate_parms(parms, kv_pairs) | ||
if not success then return false, msg end | if not success then return false, msg end | ||
− | local success | + | if parms.input then |
+ | success, msg = wikidata_call(parms, '_adjustparameters', parms, 1) | ||
+ | if not success then return false, msg end | ||
+ | end | ||
+ | local success, i, in_unit, in_unit_table = simple_get_values(parms) | ||
if not success then | if not success then | ||
+ | if type(i) == 'string' and i:match('^NNN+$') then | ||
+ | -- Some infoboxes have examples like {{convert|NNN|m}} (3 or more "N"). | ||
+ | -- Output an empty string for these. | ||
+ | return false, { 'cvt_no_output' } | ||
+ | end | ||
+ | local valinfo | ||
success, valinfo, i = get_values(parms) | success, valinfo, i = get_values(parms) | ||
if not success then return false, valinfo end | if not success then return false, valinfo end | ||
in_unit = strip(parms[i]) | in_unit = strip(parms[i]) | ||
i = i + 1 | i = i + 1 | ||
− | success, in_unit_table = lookup(in_unit | + | success, in_unit_table = lookup(parms, in_unit, 'no_combination') |
if not success then | if not success then | ||
− | + | in_unit = in_unit or '' | |
− | |||
− | |||
if parms.opt_ignore_error then -- display given unit code with no error (for use with {{val}}) | if parms.opt_ignore_error then -- display given unit code with no error (for use with {{val}}) | ||
in_unit_table = '' -- suppress error message and prevent processing of output unit | in_unit_table = '' -- suppress error message and prevent processing of output unit | ||
end | end | ||
− | in_unit_table = setmetatable({ symbol = in_unit, name2 = in_unit, | + | in_unit_table = setmetatable({ |
− | default = | + | symbol = in_unit, name2 = in_unit, utype = in_unit, |
− | + | scale = 1, default = '', defkey = '', linkey = '', | |
+ | bad_mcode = in_unit_table }, unit_mt) | ||
end | end | ||
+ | in_unit_table.valinfo = valinfo | ||
end | end | ||
if parms.test == 'msg' then | if parms.test == 'msg' then | ||
Dòng 1.952: | Dòng 2.161: | ||
end | end | ||
end | end | ||
− | |||
in_unit_table.inout = 'in' -- this is an input unit | in_unit_table.inout = 'in' -- this is an input unit | ||
if not parms.range then | if not parms.range then | ||
− | local success, inext, composite_unit = get_composite(parms, i | + | local success, inext, composite_unit = get_composite(parms, i, in_unit_table) |
if not success then return false, inext end | if not success then return false, inext end | ||
if composite_unit then | if composite_unit then | ||
Dòng 1.973: | Dòng 2.181: | ||
end | end | ||
end | end | ||
− | local | + | local word = strip(parms[i]) |
i = i + 1 | i = i + 1 | ||
local precision, is_bad_precision | local precision, is_bad_precision | ||
Dòng 1.988: | Dòng 2.196: | ||
end | end | ||
end | end | ||
− | if not set_precision( | + | if word and not set_precision(word) then |
− | parms.out_unit = | + | parms.out_unit = parms.out_unit or word |
if set_precision(strip(parms[i])) then | if set_precision(strip(parms[i])) then | ||
i = i + 1 | i = i + 1 | ||
Dòng 1.995: | Dòng 2.203: | ||
end | end | ||
if parms.opt_adj_mid then | if parms.opt_adj_mid then | ||
− | + | word = parms[i] | |
i = i + 1 | i = i + 1 | ||
− | if | + | if word then -- mid-text words |
− | if | + | if word:sub(1, 1) == '-' then |
− | parms.mid = | + | parms.mid = word |
else | else | ||
− | parms.mid = ' ' .. | + | parms.mid = ' ' .. word |
end | end | ||
end | end | ||
Dòng 2.041: | Dòng 2.249: | ||
parms.precision = precision | parms.precision = precision | ||
end | end | ||
− | return true | + | for j = i, i + 3 do |
+ | local parm = parms[j] -- warn if find a non-empty extraneous parameter | ||
+ | if parm and parm:match('%S') then | ||
+ | add_warning(parms, 1, 'cvt_unknown_option', parm) | ||
+ | break | ||
+ | end | ||
+ | end | ||
+ | return true, in_unit_table | ||
end | end | ||
Dòng 2.077: | Dòng 2.292: | ||
local fudge = 1e-14 -- {{Order of magnitude}} adds this, so we do too | local fudge = 1e-14 -- {{Order of magnitude}} adds this, so we do too | ||
local prec, minprec, adjust | local prec, minprec, adjust | ||
− | |||
local subunit_ignore_trailing_zero | local subunit_ignore_trailing_zero | ||
local subunit_more_precision -- kludge for "in" used in input like "|2|ft|6|in" | local subunit_more_precision -- kludge for "in" used in input like "|2|ft|6|in" | ||
Dòng 2.108: | Dòng 2.322: | ||
end | end | ||
if in_current.istemperature and out_current.istemperature then | if in_current.istemperature and out_current.istemperature then | ||
− | -- Converting between common temperatures (°C, °F, °R, K); not keVT | + | -- Converting between common temperatures (°C, °F, °R, K); not keVT. |
-- Kelvin value can be almost zero, or small but negative due to precision problems. | -- Kelvin value can be almost zero, or small but negative due to precision problems. | ||
-- Also, an input value like -300 C (below absolute zero) gives negative kelvins. | -- Also, an input value like -300 C (below absolute zero) gives negative kelvins. | ||
Dòng 2.245: | Dòng 2.459: | ||
end | end | ||
− | local | + | local function user_style(parms, i) |
+ | -- Return text for a user-specified style for a table cell, or '' if none, | ||
+ | -- given i = 1 (input style) or 2 (output style). | ||
+ | local style = parms[(i == 1) and 'stylein' or 'styleout'] | ||
+ | if style then | ||
+ | style = style:gsub('"', '') | ||
+ | if style ~= '' then | ||
+ | if style:sub(-1) ~= ';' then | ||
+ | style = style .. ';' | ||
+ | end | ||
+ | return style | ||
+ | end | ||
+ | end | ||
+ | return '' | ||
+ | end | ||
− | local function cvtround(parms, info, in_current, out_current) | + | local function make_table_or_sort(parms, invalue, info, in_current, scaled_top) |
+ | -- Set options to handle output for a table or a sort key, or both. | ||
+ | -- The text sort key is based on the value resulting from converting | ||
+ | -- the input to a fake base unit with scale = 1, and other properties | ||
+ | -- required for a conversion derived from the input unit. | ||
+ | -- For other modules, return the sort key in a hidden span element, and | ||
+ | -- the scaled value used to generate the sort key. | ||
+ | -- If scaled_top is set, it is the scaled value of the numerator of a per unit | ||
+ | -- to be combined with this unit (the denominator) to make the sort key. | ||
+ | -- Scaling only works with units that convert with a factor (not temperature). | ||
+ | local sortkey, scaled_value | ||
+ | if parms.opt_sortable_on then | ||
+ | local base = { -- a fake unit with enough fields for a valid convert | ||
+ | scale = 1, | ||
+ | invert = in_current.invert and 1, | ||
+ | iscomplex = in_current.iscomplex, | ||
+ | offset = in_current.offset and 0, | ||
+ | } | ||
+ | local outvalue, extra = convert(parms, invalue, info, in_current, base) | ||
+ | if extra then | ||
+ | outvalue = extra.outvalue | ||
+ | end | ||
+ | if in_current.istemperature then | ||
+ | -- Have converted to kelvin; assume numbers close to zero have a | ||
+ | -- rounding error and should be zero. | ||
+ | if abs(outvalue) < 1e-12 then | ||
+ | outvalue = 0 | ||
+ | end | ||
+ | end | ||
+ | if scaled_top and outvalue ~= 0 then | ||
+ | outvalue = scaled_top / outvalue | ||
+ | end | ||
+ | scaled_value = outvalue | ||
+ | if not valid_number(outvalue) then | ||
+ | if outvalue < 0 then | ||
+ | sortkey = '1000000000000000000' | ||
+ | else | ||
+ | sortkey = '9000000000000000000' | ||
+ | end | ||
+ | elseif outvalue == 0 then | ||
+ | sortkey = '5000000000000000000' | ||
+ | else | ||
+ | local mag = floor(log10(abs(outvalue)) + 1e-14) | ||
+ | local prefix | ||
+ | if outvalue > 0 then | ||
+ | prefix = 7000 + mag | ||
+ | else | ||
+ | prefix = 2999 - mag | ||
+ | outvalue = outvalue + 10^(mag+1) | ||
+ | end | ||
+ | sortkey = format('%d', prefix) .. format('%015.0f', floor(outvalue * 10^(14-mag))) | ||
+ | end | ||
+ | end | ||
+ | local sortspan | ||
+ | if sortkey and not parms.table_align then | ||
+ | sortspan = parms.opt_sortable_debug and | ||
+ | '<span data-sort-value="' .. sortkey .. '♠"><span style="border:1px solid">' .. sortkey .. '♠</span></span>' or | ||
+ | '<span data-sort-value="' .. sortkey .. '♠"></span>' | ||
+ | parms.join_before = sortspan | ||
+ | end | ||
+ | if parms.table_align then | ||
+ | local sort | ||
+ | if sortkey then | ||
+ | sort = ' data-sort-value="' .. sortkey .. '"' | ||
+ | if parms.opt_sortable_debug then | ||
+ | parms.join_before = '<span style="border:1px solid">' .. sortkey .. '</span>' | ||
+ | end | ||
+ | else | ||
+ | sort = '' | ||
+ | end | ||
+ | local style = 'style="text-align:' .. parms.table_align .. ';' | ||
+ | local joins = {} | ||
+ | for i = 1, 2 do | ||
+ | joins[i] = (i == 1 and '' or '\n|') .. style .. user_style(parms, i) .. '"' .. sort .. '|' | ||
+ | end | ||
+ | parms.table_joins = joins | ||
+ | end | ||
+ | return sortspan, scaled_value | ||
+ | end | ||
+ | |||
+ | local cvt_to_hand | ||
+ | |||
+ | local function cvtround(parms, info, in_current, out_current) | ||
-- Return true, t where t is a table with the conversion results; fields: | -- Return true, t where t is a table with the conversion results; fields: | ||
-- show = rounded, formatted string with the result of converting value in info, | -- show = rounded, formatted string with the result of converting value in info, | ||
-- using the rounding specified in parms. | -- using the rounding specified in parms. | ||
− | -- singular = true if result | + | -- singular = true if result (after rounding and ignoring any negative sign) |
− | -- is "1", or like "1.00"; | + | -- is "1", or like "1.00", or is a fraction with value < 1; |
-- (and more fields shown below, and a calculated 'absvalue' field). | -- (and more fields shown below, and a calculated 'absvalue' field). | ||
− | |||
-- or return false, t where t is an error message table. | -- or return false, t where t is an error message table. | ||
-- Input info.clean uses en digits (it has been translated, if necessary). | -- Input info.clean uses en digits (it has been translated, if necessary). | ||
-- Output show uses en or non-en digits as appropriate, or can be spelled. | -- Output show uses en or non-en digits as appropriate, or can be spelled. | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
if out_current.builtin == 'hand' then | if out_current.builtin == 'hand' then | ||
return cvt_to_hand(parms, info, in_current, out_current) | return cvt_to_hand(parms, info, in_current, out_current) | ||
end | end | ||
+ | local invalue = in_current.builtin == 'hand' and info.altvalue or info.value | ||
local outvalue, extra = convert(parms, invalue, info, in_current, out_current) | local outvalue, extra = convert(parms, invalue, info, in_current, out_current) | ||
+ | if parms.need_table_or_sort then | ||
+ | parms.need_table_or_sort = nil -- process using first input value only | ||
+ | make_table_or_sort(parms, invalue, info, in_current) | ||
+ | end | ||
if extra then | if extra then | ||
if not outvalue then return false, extra end | if not outvalue then return false, extra end | ||
Dòng 2.285: | Dòng 2.589: | ||
outvalue = -outvalue | outvalue = -outvalue | ||
end | end | ||
− | local | + | local precision, show, exponent |
local denominator = out_current.frac | local denominator = out_current.frac | ||
if denominator then | if denominator then | ||
Dòng 2.292: | Dòng 2.596: | ||
precision = parms.precision | precision = parms.precision | ||
if not precision then | if not precision then | ||
− | + | if parms.sigfig then | |
− | + | show, exponent = make_sigfig(outvalue, parms.sigfig) | |
− | show, exponent = make_sigfig(outvalue, sigfig) | + | elseif parms.opt_round then |
− | elseif parms. | + | local n = parms.opt_round |
− | local n = parms. | + | if n == 0.5 then |
− | show = format('%.0f', floor((outvalue / n) + 0.5) * n) | + | local integer, fracpart = math.modf(floor(2 * outvalue + 0.5) / 2) |
+ | if fracpart == 0 then | ||
+ | show = format('%.0f', integer) | ||
+ | else | ||
+ | show = format('%.1f', integer + fracpart) | ||
+ | end | ||
+ | else | ||
+ | show = format('%.0f', floor((outvalue / n) + 0.5) * n) | ||
+ | end | ||
else | else | ||
local inclean = info.clean | local inclean = info.clean | ||
Dòng 2.339: | Dòng 2.651: | ||
end | end | ||
local t = format_number(parms, show, exponent, isnegative) | local t = format_number(parms, show, exponent, isnegative) | ||
− | -- Set singular using match because on some systems 0.99999999999999999 is 1.0. | + | if type(show) == 'string' then |
− | + | -- Set singular using match because on some systems 0.99999999999999999 is 1.0. | |
− | t.fraction_table = ( | + | if exponent then |
+ | t.singular = (exponent == 1 and show:match('^10*$')) | ||
+ | else | ||
+ | t.singular = (show == '1' or show:match('^1%.0*$')) | ||
+ | end | ||
+ | else | ||
+ | t.fraction_table = show | ||
+ | t.singular = (outvalue <= 1) -- cannot have 'fraction == 1', but if it were possible it would be singular | ||
+ | end | ||
t.raw_absvalue = outvalue -- absolute value before rounding | t.raw_absvalue = outvalue -- absolute value before rounding | ||
return true, setmetatable(t, { | return true, setmetatable(t, { | ||
Dòng 2.466: | Dòng 2.786: | ||
-- 'smallsuffix' if (value < 120), or 'bigsuffix' otherwise. | -- 'smallsuffix' if (value < 120), or 'bigsuffix' otherwise. | ||
-- Input must use en digits and '.' decimal mark. | -- Input must use en digits and '.' decimal mark. | ||
− | local default = default_exceptions[unit_table.defkey or unit_table.symbol] or unit_table.default | + | local default = data_code.default_exceptions[unit_table.defkey or unit_table.symbol] or unit_table.default |
if not default then | if not default then | ||
+ | local per = unit_table.per | ||
+ | if per then | ||
+ | local function a_default(v, u) | ||
+ | local success, ucode = get_default(v, u) | ||
+ | if not success then | ||
+ | return '?' -- an unlikely error has occurred; will cause lookup of default to fail | ||
+ | end | ||
+ | -- Attempt to use only the first unit if a combination or output multiple. | ||
+ | -- This is not bulletproof but should work for most cases. | ||
+ | -- Where it does not work, the convert will need to specify the wanted output unit. | ||
+ | local t = all_units[ucode] | ||
+ | if t then | ||
+ | local combo = t.combination | ||
+ | if combo then | ||
+ | -- For a multiple like ftin, the "first" unit (ft) is last in the combination. | ||
+ | local i = t.multiple and table_len(combo) or 1 | ||
+ | ucode = combo[i] | ||
+ | end | ||
+ | else | ||
+ | -- Try for an automatically generated combination. | ||
+ | local item = ucode:match('^(.-)%+') or ucode:match('^(%S+)%s') | ||
+ | if all_units[item] then | ||
+ | return item | ||
+ | end | ||
+ | end | ||
+ | return ucode | ||
+ | end | ||
+ | local unit1, unit2 = per[1], per[2] | ||
+ | local def1 = (unit1 and a_default(value, unit1) or unit_table.vprefix or '') | ||
+ | local def2 = a_default(1, unit2) -- 1 because per unit of denominator | ||
+ | return true, def1 .. '/' .. def2 | ||
+ | end | ||
return false, { 'cvt_no_default', unit_table.symbol } | return false, { 'cvt_no_default', unit_table.symbol } | ||
end | end | ||
Dòng 2.489: | Dòng 2.841: | ||
local linked_pages -- to record linked pages so will not link to the same page more than once | local linked_pages -- to record linked pages so will not link to the same page more than once | ||
− | local function make_link(link, id, | + | local function unlink(unit_table) |
+ | -- Forget that the given unit has previously been linked (if it has). | ||
+ | -- That is needed when processing a range of inputs or outputs when an id | ||
+ | -- for the first range value may have been evaluated, but only an id for | ||
+ | -- the last value is displayed, and that id may need to be linked. | ||
+ | linked_pages[unit_table.unitcode or unit_table] = nil | ||
+ | end | ||
+ | |||
+ | local function make_link(link, id, unit_table) | ||
-- Return wikilink "[[link|id]]", possibly abbreviated as in examples: | -- Return wikilink "[[link|id]]", possibly abbreviated as in examples: | ||
-- [[Mile|mile]] --> [[mile]] | -- [[Mile|mile]] --> [[mile]] | ||
Dòng 2.496: | Dòng 2.856: | ||
-- * no link given (so caller does not need to check if a link was defined); or | -- * no link given (so caller does not need to check if a link was defined); or | ||
-- * link has previously been used during the current convert (to avoid overlinking). | -- * link has previously been used during the current convert (to avoid overlinking). | ||
− | + | local link_key | |
− | + | if unit_table then | |
− | + | link_key = unit_table.unitcode or unit_table | |
− | + | else | |
− | + | link_key = link | |
+ | end | ||
if not link or link == '' or linked_pages[link_key] then | if not link or link == '' or linked_pages[link_key] then | ||
return id | return id | ||
Dòng 2.544: | Dòng 2.905: | ||
else | else | ||
i = 3 | i = 3 | ||
+ | end | ||
+ | if i > 1 and varname == 'pl' then | ||
+ | i = i - 1 | ||
end | end | ||
vname = split(unit_table.varname, '!')[i] | vname = split(unit_table.varname, '!')[i] | ||
Dòng 2.560: | Dòng 2.924: | ||
end | end | ||
− | local function linked_id(unit_table, key_id, want_link, clean) | + | local function linked_id(parms, unit_table, key_id, want_link, clean) |
-- Return final unit id (symbol or name), optionally with a wikilink, | -- Return final unit id (symbol or name), optionally with a wikilink, | ||
-- and update unit_table.sep if required. | -- and update unit_table.sep if required. | ||
Dòng 2.574: | Dòng 2.938: | ||
local per = unit_table.per | local per = unit_table.per | ||
if per then | if per then | ||
+ | local paren1, paren2 = '', '' -- possible parentheses around bottom unit | ||
local unit1 = per[1] -- top unit_table, or nil | local unit1 = per[1] -- top unit_table, or nil | ||
local unit2 = per[2] -- bottom unit_table | local unit2 = per[2] -- bottom unit_table | ||
Dòng 2.585: | Dòng 2.950: | ||
return symbol -- for exceptions that have the symbol built-in | return symbol -- for exceptions that have the symbol built-in | ||
end | end | ||
+ | end | ||
+ | if (unit2.symbol):find('⋅', 1, true) then | ||
+ | paren1, paren2 = '(', ')' | ||
end | end | ||
end | end | ||
Dòng 2.607: | Dòng 2.975: | ||
if want_link and unit_table.link then | if want_link and unit_table.link then | ||
if abbr_on or not varname then | if abbr_on or not varname then | ||
− | result = (unit1 and unit1 | + | result = (unit1 and linked_id(parms, unit1, key_id, false, clean) or '') .. result .. linked_id(parms, unit2, key_id2, false, '1') |
else | else | ||
result = (unit1 and variable_name(clean, unit1) or '') .. result .. variable_name('1', unit2) | result = (unit1 and variable_name(clean, unit1) or '') .. result .. variable_name('1', unit2) | ||
end | end | ||
− | if | + | if omit_separator(result) then |
unit_table.sep = '' | unit_table.sep = '' | ||
end | end | ||
Dòng 2.617: | Dòng 2.985: | ||
end | end | ||
if unit1 then | if unit1 then | ||
− | result = linked_id(unit1, key_id, want_link, clean) .. result | + | result = linked_id(parms, unit1, key_id, want_link, clean) .. result |
if unit1.sep then | if unit1.sep then | ||
unit_table.sep = unit1.sep | unit_table.sep = unit1.sep | ||
Dòng 2.624: | Dòng 2.992: | ||
unit_table.sep = '' | unit_table.sep = '' | ||
end | end | ||
− | return result .. linked_id(unit2, key_id2, want_link, '1') | + | return result .. paren1 .. linked_id(parms, unit2, key_id2, want_link, '1') .. paren2 |
end | end | ||
if multiplier then | if multiplier then | ||
Dòng 2.643: | Dòng 3.011: | ||
end | end | ||
local id = unit_table.fixed_name or ((varname and not abbr_on) and variable_name(clean, unit_table) or unit_table[key_id]) | local id = unit_table.fixed_name or ((varname and not abbr_on) and variable_name(clean, unit_table) or unit_table[key_id]) | ||
− | if | + | if omit_separator(id) then |
unit_table.sep = '' | unit_table.sep = '' | ||
end | end | ||
if want_link then | if want_link then | ||
− | local link = link_exceptions[unit_table.linkey or unit_table.symbol] or unit_table.link | + | local link = data_code.link_exceptions[unit_table.linkey or unit_table.symbol] or unit_table.link |
if link then | if link then | ||
local before = '' | local before = '' | ||
local i = unit_table.customary | local i = unit_table.customary | ||
− | if i == 1 and | + | if i == 1 and parms.opt_sp_us then |
i = 2 -- show "U.S." not "US" | i = 2 -- show "U.S." not "US" | ||
end | end | ||
Dòng 2.673: | Dòng 3.041: | ||
end | end | ||
-- Omit any "US"/"U.S."/"imp"/"imperial" from start of id since that will be inserted. | -- Omit any "US"/"U.S."/"imp"/"imperial" from start of id since that will be inserted. | ||
− | local removes = (i < 3) and { ' | + | local removes = (i < 3) and { 'US ', 'US ', 'U.S. ', 'U.S. ' } or { 'imp ', 'imp ', 'imperial ' } |
for _, prefix in ipairs(removes) do | for _, prefix in ipairs(removes) do | ||
− | id = | + | local plen = #prefix |
+ | if id:sub(1, plen) == prefix then | ||
+ | id = id:sub(plen + 1) | ||
+ | break | ||
+ | end | ||
end | end | ||
before = pertext .. make_link(customary.link, customary[1]) .. ' ' | before = pertext .. make_link(customary.link, customary[1]) .. ' ' | ||
Dòng 2.689: | Dòng 3.061: | ||
-- id = unit name or symbol, possibly modified | -- id = unit name or symbol, possibly modified | ||
-- f = true if id is a name, or false if id is a symbol | -- f = true if id is a name, or false if id is a symbol | ||
− | -- using | + | -- using the value for index 'which', and for 'in' or 'out' (unit_table.inout). |
-- Result is '' if no symbol/name is to be used. | -- Result is '' if no symbol/name is to be used. | ||
-- In addition, set unit_table.sep = ' ' or ' ' or '' | -- In addition, set unit_table.sep = ' ' or ' ' or '' | ||
Dòng 2.701: | Dòng 3.073: | ||
local abbr_org = parms.abbr_org | local abbr_org = parms.abbr_org | ||
local adjectival = parms.opt_adjectival | local adjectival = parms.opt_adjectival | ||
− | |||
local lk = parms.lk | local lk = parms.lk | ||
local want_link = (lk == 'on' or lk == inout) | local want_link = (lk == 'on' or lk == inout) | ||
local usename = unit_table.usename | local usename = unit_table.usename | ||
local singular = info.singular | local singular = info.singular | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
local want_name | local want_name | ||
if usename then | if usename then | ||
Dòng 2.733: | Dòng 3.082: | ||
else | else | ||
if abbr_org == nil then | if abbr_org == nil then | ||
− | if | + | if parms.wantname then |
want_name = true | want_name = true | ||
end | end | ||
Dòng 2.771: | Dòng 3.120: | ||
end | end | ||
end | end | ||
− | if unit_table.engscale | + | if unit_table.engscale then |
-- engscale: so "|1|e3kg" gives "1 thousand kilograms" (plural) | -- engscale: so "|1|e3kg" gives "1 thousand kilograms" (plural) | ||
− | |||
singular = false | singular = false | ||
end | end | ||
key = (adjectival or singular) and 'name1' or 'name2' | key = (adjectival or singular) and 'name1' or 'name2' | ||
− | if | + | if parms.opt_sp_us then |
key = key .. '_us' | key = key .. '_us' | ||
end | end | ||
Dòng 2.787: | Dòng 3.135: | ||
end | end | ||
unit_table.sep = ' ' | unit_table.sep = ' ' | ||
− | key = | + | key = parms.opt_sp_us and 'sym_us' or 'symbol' |
end | end | ||
− | return linked_id(unit_table, key, want_link, info.clean), want_name | + | return linked_id(parms, unit_table, key, want_link, info.clean), want_name |
end | end | ||
− | local function decorate_value(parms, unit_table, which) | + | local function decorate_value(parms, unit_table, which, number_word) |
-- If needed, update unit_table so values will be shown with extra information. | -- If needed, update unit_table so values will be shown with extra information. | ||
-- For consistency with the old template (but different from fmtpower), | -- For consistency with the old template (but different from fmtpower), | ||
Dòng 2.806: | Dòng 3.154: | ||
end | end | ||
info.decorated = true | info.decorated = true | ||
− | + | if engscale then | |
− | + | local inout = unit_table.inout | |
− | + | local abbr = parms.abbr | |
− | + | if (abbr == 'on' or abbr == inout) and not parms.number_word then | |
− | + | info.show = info.show .. | |
− | + | '<span style="margin-left:0.2em">×<span style="margin-left:0.1em">' .. | |
− | + | from_en('10') .. | |
− | + | '</span></span><s style="display:none">^</s><sup>' .. | |
− | + | from_en(tostring(engscale.exponent)) .. '</sup>' | |
− | + | elseif number_word then | |
− | + | local number_id | |
− | + | local lk = parms.lk | |
− | + | if lk == 'on' or lk == inout then | |
− | + | number_id = make_link(engscale.link, engscale[1]) | |
− | + | else | |
− | + | number_id = engscale[1] | |
− | + | end | |
+ | -- WP:NUMERAL recommends " " in values like "12 million". | ||
+ | info.show = info.show .. (parms.opt_adjectival and '-' or ' ') .. number_id | ||
end | end | ||
− | |||
− | |||
end | end | ||
− | + | if prefix then | |
− | + | info.show = prefix .. info.show | |
− | + | end | |
end | end | ||
end | end | ||
Dòng 2.871: | Dòng 3.219: | ||
return preunit .. id1 | return preunit .. id1 | ||
end | end | ||
− | if parms.opt_also_symbol and not composite then | + | if parms.opt_also_symbol and not composite and not parms.opt_flip then |
local join1 = parms.joins[1] | local join1 = parms.joins[1] | ||
if join1 == ' (' or join1 == ' [' then | if join1 == ' (' or join1 == ' [' then | ||
− | parms.joins = { | + | parms.joins = { ' [' .. first_unit[parms.opt_sp_us and 'sym_us' or 'symbol'] .. ']' .. join1 , parms.joins[2] } |
end | end | ||
end | end | ||
Dòng 2.885: | Dòng 3.233: | ||
-- For simplicity and because more not needed, handle one range item only. | -- For simplicity and because more not needed, handle one range item only. | ||
local prefix2 = make_id(parms, 2, first_unit) .. ' ' | local prefix2 = make_id(parms, 2, first_unit) .. ' ' | ||
− | result = range_text(range[1], want_name, parms, result, prefix2 .. valinfo[2].show) | + | result = range_text(range[1], want_name, parms, result, prefix2 .. valinfo[2].show, 'in') |
end | end | ||
return preunit .. result | return preunit .. result | ||
Dòng 2.911: | Dòng 3.259: | ||
return table.concat(parts, sep2) .. mid | return table.concat(parts, sep2) .. mid | ||
end | end | ||
− | local | + | local add_unit = (parms.abbr == 'mos') or |
− | + | parms[parms.opt_flip and 'out_range_x' or 'in_range_x'] or | |
+ | (not want_name and parms.abbr_range_x) | ||
local range = parms.range | local range = parms.range | ||
− | if range then | + | if range and not add_unit then |
− | + | unlink(first_unit) | |
− | |||
− | |||
− | |||
end | end | ||
− | local id = | + | local id = range and make_id(parms, range.n + 1, first_unit) or id1 |
local extra, was_hyphenated = hyphenated_maybe(parms, want_name, sep, id, 'in') | local extra, was_hyphenated = hyphenated_maybe(parms, want_name, sep, id, 'in') | ||
− | if | + | if was_hyphenated then |
− | + | add_unit = false | |
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
+ | local result | ||
local valinfo = first_unit.valinfo | local valinfo = first_unit.valinfo | ||
if range then | if range then | ||
− | + | for i = 0, range.n do | |
− | + | local number_word | |
− | + | if i == range.n then | |
− | + | add_unit = false | |
− | + | number_word = true | |
− | + | end | |
− | + | decorate_value(parms, first_unit, i+1, number_word) | |
− | + | local show = valinfo[i+1].show | |
− | + | if add_unit then | |
− | + | show = show .. first_unit.sep .. (i == 0 and id1 or make_id(parms, i+1, first_unit)) | |
− | + | end | |
− | + | if i == 0 then | |
− | + | result = show | |
− | result = | ||
else | else | ||
− | + | result = range_text(range[i], want_name, parms, result, show, 'in') | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
end | end | ||
else | else | ||
− | decorate_value(parms, first_unit, 1) | + | decorate_value(parms, first_unit, 1, true) |
result = valinfo[1].show | result = valinfo[1].show | ||
end | end | ||
Dòng 2.973: | Dòng 3.301: | ||
-- Processing required for each output unit. | -- Processing required for each output unit. | ||
-- Return block of text to represent output (value/unit). | -- Return block of text to represent output (value/unit). | ||
+ | local inout = out_current.inout -- normally 'out' but can be 'in' for order=out | ||
local id1, want_name = make_id(parms, 1, out_current) | local id1, want_name = make_id(parms, 1, out_current) | ||
local sep = out_current.sep -- set by make_id | local sep = out_current.sep -- set by make_id | ||
Dòng 2.994: | Dòng 3.323: | ||
if range then | if range then | ||
-- For simplicity and because more not needed, handle one range item only. | -- For simplicity and because more not needed, handle one range item only. | ||
− | result = range_text(range[1], want_name, parms, result, prefix .. valinfo[2].show) | + | result = range_text(range[1], want_name, parms, result, prefix .. valinfo[2].show, inout) |
end | end | ||
return preunit .. result | return preunit .. result | ||
end | end | ||
− | local | + | local add_unit = (parms[parms.opt_flip and 'in_range_x' or 'out_range_x'] or |
+ | (not want_name and parms.abbr_range_x)) and | ||
+ | not parms.opt_output_number_only | ||
local range = parms.range | local range = parms.range | ||
− | if range then | + | if range and not add_unit then |
− | + | unlink(out_current) | |
− | + | end | |
− | + | local id = range and make_id(parms, range.n + 1, out_current) or id1 | |
+ | local extra, was_hyphenated = hyphenated_maybe(parms, want_name, sep, id, inout) | ||
+ | if was_hyphenated then | ||
+ | add_unit = false | ||
end | end | ||
− | local | + | local result |
− | |||
local valinfo = out_current.valinfo | local valinfo = out_current.valinfo | ||
if range then | if range then | ||
− | + | for i = 0, range.n do | |
− | local | + | local number_word |
− | + | if i == range.n then | |
− | + | add_unit = false | |
− | + | number_word = true | |
− | + | end | |
− | + | decorate_value(parms, out_current, i+1, number_word) | |
− | + | local show = valinfo[i+1].show | |
− | + | if add_unit then | |
+ | show = show .. out_current.sep .. (i == 0 and id1 or make_id(parms, i+1, out_current)) | ||
+ | end | ||
+ | if i == 0 then | ||
+ | result = show | ||
else | else | ||
− | + | result = range_text(range[i], want_name, parms, result, show, inout) | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
end | end | ||
else | else | ||
− | decorate_value(parms, out_current, 1) | + | decorate_value(parms, out_current, 1, true) |
result = valinfo[1].show | result = valinfo[1].show | ||
end | end | ||
Dòng 3.049: | Dòng 3.373: | ||
-- for a single output (which is not a combination or a multiple); | -- for a single output (which is not a combination or a multiple); | ||
-- or return false, t where t is an error message table. | -- or return false, t where t is an error message table. | ||
− | out_unit_table.valinfo = collection() | + | if parms.opt_order_out and in_unit_table.unitcode == out_unit_table.unitcode then |
− | + | out_unit_table.valinfo = in_unit_table.valinfo | |
− | + | else | |
− | + | out_unit_table.valinfo = collection() | |
− | + | for _, v in ipairs(in_unit_table.valinfo) do | |
− | + | local success, info = cvtround(parms, v, in_unit_table, out_unit_table) | |
+ | if not success then return false, info end | ||
+ | out_unit_table.valinfo:add(info) | ||
+ | end | ||
end | end | ||
return true, process_one_output(parms, out_unit_table) | return true, process_one_output(parms, out_unit_table) | ||
Dòng 3.063: | Dòng 3.390: | ||
-- for an output which is a multiple (like 'ftin'); | -- for an output which is a multiple (like 'ftin'); | ||
-- or return false, t where t is an error message table. | -- or return false, t where t is an error message table. | ||
+ | local inout = out_unit_table.inout -- normally 'out' but can be 'in' for order=out | ||
local multiple = out_unit_table.multiple -- table of scaling factors (will not be nil) | local multiple = out_unit_table.multiple -- table of scaling factors (will not be nil) | ||
local combos = out_unit_table.combination -- table of unit tables (will not be nil) | local combos = out_unit_table.combination -- table of unit tables (will not be nil) | ||
Dòng 3.069: | Dòng 3.397: | ||
local disp = parms.disp | local disp = parms.disp | ||
local want_name = (abbr_org == nil and (disp == 'or' or disp == 'slash')) or | local want_name = (abbr_org == nil and (disp == 'or' or disp == 'slash')) or | ||
− | not (abbr == 'on' or abbr == | + | not (abbr == 'on' or abbr == inout or abbr == 'mos') |
− | local want_link = (parms.lk == 'on' or parms.lk == | + | local want_link = (parms.lk == 'on' or parms.lk == inout) |
local mid = parms.opt_flip and parms.mid or '' | local mid = parms.opt_flip and parms.mid or '' | ||
local sep1 = ' ' | local sep1 = ' ' | ||
Dòng 3.086: | Dòng 3.414: | ||
local tfrac, thisvalue, strforce | local tfrac, thisvalue, strforce | ||
local out_current = combos[i] | local out_current = combos[i] | ||
− | out_current.inout = | + | out_current.inout = inout |
local scale = multiple[i] | local scale = multiple[i] | ||
if i == 1 then -- least significant unit ('in' from 'ftin') | if i == 1 then -- least significant unit ('in' from 'ftin') | ||
Dòng 3.156: | Dòng 3.484: | ||
id = out_current['symbol'] | id = out_current['symbol'] | ||
end | end | ||
− | if | + | if i == 1 and omit_separator(id) then |
-- Testing the id of the least significant unit should be sufficient. | -- Testing the id of the least significant unit should be sufficient. | ||
sep1 = '' | sep1 = '' | ||
Dòng 3.168: | Dòng 3.496: | ||
end | end | ||
local strval | local strval | ||
− | local | + | local spell_inout = (i == #combos or outvalue == 0) and inout or '' -- trick so the last value processed (first displayed) has uppercase, if requested |
if strforce and outvalue == 0 then | if strforce and outvalue == 0 then | ||
sign = '' -- any sign is in strforce | sign = '' -- any sign is in strforce | ||
Dòng 3.174: | Dòng 3.502: | ||
elseif tfrac then | elseif tfrac then | ||
local wholestr = (thisvalue > 0) and tostring(thisvalue) or nil | local wholestr = (thisvalue > 0) and tostring(thisvalue) or nil | ||
− | strval = format_fraction(parms, | + | strval = format_fraction(parms, spell_inout, false, wholestr, tfrac.numstr, tfrac.denstr, do_spell) |
else | else | ||
strval = (thisvalue == 0) and from_en('0') or with_separator(parms, format(fmt, thisvalue)) | strval = (thisvalue == 0) and from_en('0') or with_separator(parms, format(fmt, thisvalue)) | ||
if do_spell then | if do_spell then | ||
− | strval = spell_number(parms, | + | strval = spell_number(parms, spell_inout, strval) or strval |
end | end | ||
end | end | ||
Dòng 3.201: | Dòng 3.529: | ||
local success, result2 = make_result(valinfo[i+1]) | local success, result2 = make_result(valinfo[i+1]) | ||
if not success then return false, result2 end | if not success then return false, result2 end | ||
− | result = range_text(range[i], want_name, parms, result, result2) | + | result = range_text(range[i], want_name, parms, result, result2, inout) |
end | end | ||
end | end | ||
Dòng 3.208: | Dòng 3.536: | ||
local function process(parms, in_unit_table, out_unit_table) | local function process(parms, in_unit_table, out_unit_table) | ||
− | -- Return true, s where s = final wikitext result, | + | -- Return true, s, outunit where s = final wikitext result, |
-- or return false, t where t is an error message table. | -- or return false, t where t is an error message table. | ||
linked_pages = {} | linked_pages = {} | ||
− | local success, bad_output | + | local success, bad_output |
− | local bad_input_mcode = in_unit_table.bad_mcode -- | + | local bad_input_mcode = in_unit_table.bad_mcode -- nil if input unit is a valid convert unit |
− | |||
local out_unit = parms.out_unit | local out_unit = parms.out_unit | ||
− | if out_unit == nil or out_unit == '' then | + | if out_unit == nil or out_unit == '' or type(out_unit) == 'function' then |
− | if bad_input_mcode then | + | if bad_input_mcode or parms.opt_input_unit_only then |
bad_output = '' | bad_output = '' | ||
else | else | ||
− | success, out_unit = | + | local getdef = type(out_unit) == 'function' and out_unit or get_default |
+ | success, out_unit = getdef(in_unit_table.valinfo[1].value, in_unit_table) | ||
parms.out_unit = out_unit | parms.out_unit = out_unit | ||
if not success then | if not success then | ||
Dòng 3.227: | Dòng 3.555: | ||
end | end | ||
if not bad_output and not out_unit_table then | if not bad_output and not out_unit_table then | ||
− | success, out_unit_table = lookup(out_unit | + | success, out_unit_table = lookup(parms, out_unit, 'any_combination') |
if success then | if success then | ||
local mismatch = check_mismatch(in_unit_table, out_unit_table) | local mismatch = check_mismatch(in_unit_table, out_unit_table) | ||
Dòng 3.237: | Dòng 3.565: | ||
end | end | ||
end | end | ||
+ | local lhs, rhs | ||
local flipped = parms.opt_flip and not bad_input_mcode | local flipped = parms.opt_flip and not bad_input_mcode | ||
− | + | if bad_output then | |
− | + | rhs = (bad_output == '') and '' or message(parms, bad_output) | |
− | + | elseif parms.opt_input_unit_only then | |
− | + | rhs = '' | |
− | + | else | |
− | + | local combos -- nil (for 'ft' or 'ftin'), or table of unit tables (for 'm ft') | |
− | + | if not out_unit_table.multiple then -- nil/false ('ft' or 'm ft'), or table of factors ('ftin') | |
− | + | combos = out_unit_table.combination | |
− | + | end | |
− | + | local frac = parms.frac -- nil or denominator of fraction for output values | |
− | + | if frac then | |
− | + | -- Apply fraction to the unit (if only one), or to non-SI units (if a combination), | |
− | + | -- except that if a precision is also specified, the fraction only applies to | |
− | + | -- the hand unit; that allows the following result: | |
− | + | -- {{convert|156|cm|in hand|1|frac=2}} → 156 centimetres (61.4 in; 15.1½ hands) | |
− | + | -- However, the following is handled elsewhere as a special case: | |
− | + | -- {{convert|156|cm|hand in|1|frac=2}} → 156 centimetres (15.1½ hands; 61½ in) | |
− | + | if combos then | |
− | + | local precision = parms.precision | |
− | + | for _, unit in ipairs(combos) do | |
− | + | if unit.builtin == 'hand' or (not precision and not unit.prefixes) then | |
− | + | unit.frac = frac | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
− | |||
− | |||
end | end | ||
+ | else | ||
+ | out_unit_table.frac = frac | ||
end | end | ||
− | + | end | |
− | for i = 1, imax do | + | local outputs = {} |
− | + | local imax = combos and #combos or 1 -- 1 (single unit) or number of unit tables | |
− | + | if imax == 1 then | |
− | + | parms.opt_order_out = nil -- only useful with an output combination | |
− | + | end | |
− | + | if not flipped and not parms.opt_order_out then | |
− | + | -- Process left side first so any duplicate links (from lk=on) are suppressed | |
− | + | -- on right. Example: {{convert|28|e9pc|e9ly|abbr=off|lk=on}} | |
− | + | lhs = process_input(parms, in_unit_table) | |
+ | end | ||
+ | for i = 1, imax do | ||
+ | local success, item | ||
+ | local out_current = combos and combos[i] or out_unit_table | ||
+ | out_current.inout = 'out' | ||
+ | if i == 1 then | ||
+ | if imax > 1 and out_current.builtin == 'hand' then | ||
+ | out_current.out_next = combos[2] -- built-in hand can influence next unit in a combination | ||
end | end | ||
− | if | + | if parms.opt_order_out then |
− | + | out_current.inout = 'in' | |
− | |||
− | |||
end | end | ||
− | |||
− | |||
end | end | ||
− | + | if out_current.multiple then | |
− | + | success, item = make_output_multiple(parms, in_unit_table, out_current) | |
+ | else | ||
+ | success, item = make_output_single(parms, in_unit_table, out_current) | ||
+ | end | ||
+ | if not success then return false, item end | ||
+ | outputs[i] = item | ||
+ | end | ||
+ | if parms.opt_order_out then | ||
+ | lhs = outputs[1] | ||
+ | table.remove(outputs, 1) | ||
end | end | ||
+ | local sep = parms.table_joins and parms.table_joins[2] or parms.join_between | ||
+ | rhs = table.concat(outputs, sep) | ||
end | end | ||
− | if | + | if flipped or not lhs then |
− | local | + | local input = process_input(parms, in_unit_table) |
− | if | + | if flipped then |
− | + | lhs = rhs | |
+ | rhs = input | ||
else | else | ||
− | + | lhs = input | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
− | + | end | |
+ | if parms.join_before then | ||
+ | lhs = parms.join_before .. lhs | ||
end | end | ||
local wikitext | local wikitext | ||
if bad_input_mcode then | if bad_input_mcode then | ||
if bad_input_mcode == '' then | if bad_input_mcode == '' then | ||
− | wikitext = | + | wikitext = lhs |
else | else | ||
− | wikitext = | + | wikitext = lhs .. message(parms, bad_input_mcode) |
end | end | ||
elseif parms.table_joins then | elseif parms.table_joins then | ||
− | wikitext = parms.table_joins[1] .. | + | wikitext = parms.table_joins[1] .. lhs .. parms.table_joins[2] .. rhs |
else | else | ||
− | wikitext = | + | wikitext = lhs .. parms.joins[1] .. rhs .. parms.joins[2] |
end | end | ||
if parms.warnings and not bad_input_mcode then | if parms.warnings and not bad_input_mcode then | ||
Dòng 3.333: | Dòng 3.664: | ||
local function main_convert(frame) | local function main_convert(frame) | ||
-- Do convert, and if needed, do it again with higher default precision. | -- Do convert, and if needed, do it again with higher default precision. | ||
− | set_config(frame) | + | local parms = { frame = frame } -- will hold template arguments, after translation |
− | + | set_config(frame.args) | |
− | local success, | + | local success, result = get_parms(parms, frame:getParent().args) |
if success then | if success then | ||
− | for | + | if type(result) ~= 'table' then |
+ | return tostring(result) | ||
+ | end | ||
+ | local in_unit_table = result | ||
+ | local out_unit_table | ||
+ | for _ = 1, 2 do -- use counter so cannot get stuck repeating convert | ||
success, result, out_unit_table = process(parms, in_unit_table, out_unit_table) | success, result, out_unit_table = process(parms, in_unit_table, out_unit_table) | ||
if success and parms.do_convert_again then | if success and parms.do_convert_again then | ||
Dòng 3.345: | Dòng 3.681: | ||
end | end | ||
end | end | ||
− | |||
− | |||
end | end | ||
− | if success then | + | -- If input=x gives a problem, the result should be just the user input |
− | return result | + | -- (if x is a property like P123 it has been replaced with ''). |
+ | -- An unknown input unit would display the input and an error message | ||
+ | -- with success == true at this point. | ||
+ | -- Also, can have success == false with a message that outputs an empty string. | ||
+ | if parms.input_text then | ||
+ | if success and not parms.have_problem then | ||
+ | return result | ||
+ | end | ||
+ | local cat | ||
+ | if parms.tracking then | ||
+ | -- Add a tracking category using the given text as the category sort key. | ||
+ | -- There is currently only one type of tracking, but in principle multiple | ||
+ | -- items could be tracked, using different sort keys for convenience. | ||
+ | cat = wanted_category('tracking', parms.tracking) | ||
+ | end | ||
+ | return parms.input_text .. (cat or '') | ||
+ | end | ||
+ | return success and result or message(parms, result) | ||
+ | end | ||
+ | |||
+ | local function _unit(unitcode, options) | ||
+ | -- Helper function for Module:Val to look up a unit. | ||
+ | -- Parameter unitcode must be a string to identify the wanted unit. | ||
+ | -- Parameter options must be nil or a table with optional fields: | ||
+ | -- value = number (for sort key; default value is 1) | ||
+ | -- scaled_top = nil for a normal unit, or a number for a unit which is | ||
+ | -- the denominator of a per unit (for sort key) | ||
+ | -- si = { 'symbol', 'link' } | ||
+ | -- (a table with two strings) to make an SI unit | ||
+ | -- that will be used for the look up | ||
+ | -- link = true if result should be [[linked]] | ||
+ | -- sort = 'on' or 'debug' if result should include a sort key in a | ||
+ | -- span element ('debug' makes the key visible) | ||
+ | -- name = true for the name of the unit instead of the symbol | ||
+ | -- us = true for the US spelling of the unit, if any | ||
+ | -- Return nil if unitcode is not a non-empty string. | ||
+ | -- Otherwise return a table with fields: | ||
+ | -- text = requested symbol or name of unit, optionally linked | ||
+ | -- scaled_value = input value adjusted by unit scale; used for sort key | ||
+ | -- sortspan = span element with sort key like that provided by {{ntsh}}, | ||
+ | -- calculated from the result of converting value | ||
+ | -- to a base unit with scale 1. | ||
+ | -- unknown = true if the unitcode was not known | ||
+ | unitcode = strip(unitcode) | ||
+ | if unitcode == nil or unitcode == '' then | ||
+ | return nil | ||
+ | end | ||
+ | set_config({}) | ||
+ | linked_pages = {} | ||
+ | options = options or {} | ||
+ | local parms = { | ||
+ | abbr = options.name and 'off' or 'on', | ||
+ | lk = options.link and 'on' or nil, | ||
+ | opt_sp_us = options.us and true or nil, | ||
+ | opt_ignore_error = true, -- do not add pages using this function to 'what links here' for Module:Convert/extra | ||
+ | opt_sortable_on = options.sort == 'on' or options.sort == 'debug', | ||
+ | opt_sortable_debug = options.sort == 'debug', | ||
+ | } | ||
+ | if options.si then | ||
+ | -- Make a dummy table of units (just one unit) for lookup to use. | ||
+ | -- This makes lookup recognize any SI prefix in the unitcode. | ||
+ | local symbol = options.si[1] or '?' | ||
+ | parms.unittable = { [symbol] = { | ||
+ | _name1 = symbol, | ||
+ | _name2 = symbol, | ||
+ | _symbol = symbol, | ||
+ | utype = symbol, | ||
+ | scale = symbol == 'g' and 0.001 or 1, | ||
+ | prefixes = 1, | ||
+ | default = symbol, | ||
+ | link = options.si[2], | ||
+ | }} | ||
+ | end | ||
+ | local success, unit_table = lookup(parms, unitcode, 'no_combination') | ||
+ | if not success then | ||
+ | unit_table = setmetatable({ | ||
+ | symbol = unitcode, name2 = unitcode, utype = unitcode, | ||
+ | scale = 1, default = '', defkey = '', linkey = '' }, unit_mt) | ||
+ | end | ||
+ | local value = tonumber(options.value) or 1 | ||
+ | local clean = tostring(abs(value)) | ||
+ | local info = { | ||
+ | value = value, | ||
+ | altvalue = value, | ||
+ | singular = (clean == '1'), | ||
+ | clean = clean, | ||
+ | show = clean, | ||
+ | } | ||
+ | unit_table.inout = 'in' | ||
+ | unit_table.valinfo = { info } | ||
+ | local sortspan, scaled_value | ||
+ | if options.sort then | ||
+ | sortspan, scaled_value = make_table_or_sort(parms, value, info, unit_table, options.scaled_top) | ||
end | end | ||
− | return | + | return { |
+ | text = make_id(parms, 1, unit_table), | ||
+ | sortspan = sortspan, | ||
+ | scaled_value = scaled_value, | ||
+ | unknown = not success and true or nil, | ||
+ | } | ||
end | end | ||
− | return { convert = main_convert } | + | return { convert = main_convert, _unit = _unit } |