وحدة:Tabular data
| This module is rated as alpha. It is ready for third-party input, and may be used on a few pages to see if problems arise, but should be watched. Suggestions for new features or changes in their input and output mechanisms are welcome. |
This module provides basic functions for interacting with tabular data on Wikimedia Commons.
cell
Returns the value of the cell at the given row index and column name.
Usage: {{#invoke:Tabular data|cell|Page name.tab|output_row=Index of row to output|output_column=Name of column to output}}
A row index of 1 refers to the first row in the table. A row index of -1 refers to the last row in the table. It is an error to specify a row index of 0.
أمثلة
Latest death toll in c:Data:COVID-19 cases in Santa Clara County, California.tab (regardless of when the table was last updated):
lookup
Returns the value of the cell(s) in one or more output columns of the row matching the search key and column.
This function is reminiscent of LOOKUP() macros in popular spreadsheet applications, except that the search key must match exactly. (On the other hand, this means the table does not need to be sorted.)
Usage: {{#invoke:Tabular data|lookup|Page name.tab|search_value=Value to find in column|search_column=Name of column to search in|output_column=Name of column to output|output_column2=Name of another column to output|output_columnn=…|output_format=String format to format the output}}
If multiple columns are output without an explicit string format, this function formats the output as a human-readable list.
Some may find {{Tabular query}} (which uses this module) an intuitive way to obtain cell data as it resembles a simple SQL query.
Parameters
|1=- Page name on Commons with extension but no namespace
|search_value=or|search_pattern=- Value to find or pattern to match in column
|search_column=- Name of column to search in
|occurrence=- Index of the match to output in case of multiple matching rows. A row index of
1refers to the first matching row. A row index of-1refers to the last matching row. It is an error to specify a row index of0. |output_column=or|output_column1=,|output_column2=, ...- Names of columns to output
|output_format=- String format to format the output
wikitable
Returns the entire data table as a (rather plain) table.
Usage: {{#invoke:Tabular data|wikitable|Page name.tab}}
Examples
COVID-19 statistics in Santa Clara County, California
|
|---|
{{#invoke:Tabular data|wikitable
|COVID-19 cases in Santa Clara County, California.tab}} |
Implementation notes
The implementation of this function incorporates {{n/a}} (to represent null values), {{yes}} (true), and {{no}} (false). The templates themselves cannot be reused because they are incompatible with the mw.html library, which builds the table using an HTML DOM instead of pure wikitext.
Internationalization
You can most likely port this template to a wiki in another language without making major modifications. The wikitable function automatically localizes the table's description, column titles, and license name into the wiki's content language. It also formats numbers according to the content language. However, you should localize the cells representing true, false, and null by changing the values in the messages, bgColors, and colors variables to match the wiki's own {{yes}}, {{no}}, and {{n/a}} templates, respectively.
See also
- {{NUMBEROF}}
- {{Last tab}} and {{Date tab}}
-- Module:Tabular data (self-contained, ASCII-only, Scottish-locality aware)
local p = {}
local navbar = require("Module:Navbar")
local lang = mw.getContentLanguage()
-- Messages
local messages = { ["true"]="نعم", ["false"]="لا", null="غير متاح" }
local bgColors = { ["true"]="#9f9", ["false"]="#f99", null="#ececec" }
local colors = { ["true"]="#2c2c2c", ["false"]="#2c2c2c", null="#2c2c2c" }
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
-- Convert localized digits to ASCII; strip Arabic/ASCII thousands; map Arabic decimal to dot;
-- strip Arabic comma and NBSP/NNBSP.
local function asciiDigits(s)
if s == nil then return s end
s = tostring(s)
local map = {
["٠"]="0",["١"]="1",["٢"]="2",["٣"]="3",["٤"]="4",["٥"]="5",["٦"]="6",["٧"]="7",["٨"]="8",["٩"]="9",
["۰"]="0",["۱"]="1",["۲"]="2",["۳"]="3",["۴"]="4",["۵"]="5",["۶"]="6",["۷"]="7",["۸"]="8",["۹"]="9",
["٬"]="", -- U+066C Arabic thousands sep
[","]="", -- ASCII thousands sep
["٫"]=".", -- U+066B Arabic decimal sep -> dot
["،"]="", -- U+060C Arabic comma
["\194\160"]="", -- NBSP
["\226\128\175"]="", -- NNBSP
}
s = mw.ustring.gsub(s, ".", map)
s = mw.ustring.gsub(s, "%.%.+", ".")
return s
end
-- Normalize Data: title; accept with/without "Data:" and ".tab"
local function normalizeDataTitle(name)
if not name or name == "" then return nil end
name = asciiDigits(mw.text.trim(name))
if not mw.ustring.match(name, "^[Dd]ata:") then
name = "Data:" .. name
end
-- If '...tab' without dot, fix to '.tab'
name = mw.ustring.gsub(name, "([^.])tab$", "%1.tab")
if not mw.ustring.match(name, "%.tab$") then
name = name .. ".tab"
end
return name
end
-- Try fetching via JsonConfig
local function tryGetTabular(title)
local ok, res = pcall(function() return mw.ext.data.get(title) end)
if not ok then return nil, 'Failed to load tabular data from [['..title..']]: '..tostring(res) end
if res == false or res == nil then return nil, 'Tabular data [['..title..']] could not be loaded (mw.ext.data.get returned false).' end
if type(res) ~= 'table' then return nil, 'Tabular data [['..title..']] is not a table (got '..type(res)..').' end
if type(res.schema) ~= 'table' or type(res.schema.fields) ~= 'table' then
return nil, 'Unexpected tabular schema: missing schema.fields.'
end
if type(res.data) ~= 'table' then
if type(res.rows) == 'table' then
res.data = res.rows
elseif type(res.entries) == 'table' then
res.data = res.entries
else
return nil, 'Unexpected tabular schema: missing data/rows/entries table.'
end
end
res.__title = title
return res
end
-- Safe loader: fetch first (works even if local .exists=false due to Commons)
local function safeGetData(pageName)
local title = normalizeDataTitle(pageName)
if not title then return nil, 'No data page specified.' end
local ds, err = tryGetTabular(title)
if ds then return ds, nil end
-- Optional existence hint
local exists
pcall(function()
local t = mw.title.new(title)
exists = t and t.exists
end)
if not exists then
return nil, 'Tabular data page [[' .. title .. ']] does not exist or is not accessible.'
end
return nil, err or 'Unable to load tabular data.'
end
-- Truthy helper
local function truthy(v)
if type(v) == 'boolean' then return v end
if v == nil then return false end
v = mw.ustring.lower(tostring(v))
return v == '1' or v == 'yes' or v == 'y' or v == 'true' or v == 'on'
end
-- Fallback for silent returns
local function fallback(args) return args.default or "" end
-- List field names
local function joinFieldNames(dataset)
local names = {}
if dataset and dataset.schema and dataset.schema.fields then
for _, f in ipairs(dataset.schema.fields) do table.insert(names, f.name) end
end
return table.concat(names, ", ")
end
-- Try a list of candidate page names; return first dataset that loads
local function tryCandidates(cands)
for _, c in ipairs(cands) do
local ds = select(1, safeGetData(c))
if ds then return ds end
end
return nil
end
-- Resolve dataset from args (table / data=string / page=|src=|1=)
-- If nothing provided and POP= is present (Scottish template), try Scottish default series.
local function resolveDataset(args)
if type(args.data) == 'table' then
return args.data, nil
end
if type(args.data) == 'string' and args.data ~= '' then
return safeGetData(args.data)
end
local raw = args.src or args.page or args[1]
-- If caller provided something, use it
if raw and raw ~= "" then
return safeGetData(raw)
end
-- Heuristic: Scottish locality population template often omits page
-- Try Data:Scottish localities/<year>.tab with fallbacks
if args.POP or args.Pop or args.pop then
local y = asciiDigits(lang:formatDate('Y'))
local ds = tryCandidates({
"Scottish localities/"..y,
"Scottish localities/2022",
"Scottish localities/2021",
"Scottish localities/2020",
})
if ds then return ds, nil end
end
return nil, 'No dataset source was provided and no default matched.'
end
-- Write a cell's text (ASCII-normalized); or render null look
local function writeCellText(cell, value)
if value == nil then
cell
:addClass("mw-tabular-value-null")
:addClass("table-na")
:css({ background=bgColors.null, color=colors.null, ["vertical-align"]="middle", ["text-align"]="center" })
:wikitext(messages.null)
else
cell:wikitext(asciiDigits(value))
end
end
--------------------------------------------------------------------------------
-- Core
--------------------------------------------------------------------------------
-- Internal: return value(s) from a specific row and column(s)
function p._cell(args)
-- Special compatibility: {{Scottish locality population|name|POP=Dumbarton}}
if (args[1] and mw.ustring.lower(args[1]) == "name") and (args.POP or args.Pop or args.pop) then
local a = mw.clone(args)
a.search_column = "Name"
a.search_value = args.POP or args.Pop or args.pop
a.output_column = a.output_column or "Population"
return p._lookup(a)
end
local dataset, err = resolveDataset(args)
if not dataset then
return fallback(args) -- silent
end
local rowIdx = tonumber(asciiDigits(args.output_row))
if not rowIdx then
return fallback(args)
end
local outputFormat = args.output_format
-- Collect requested output columns
local outputColumnNames = { args.output_column1 or args.output_column }
while args["output_column" .. #outputColumnNames + 1] do
table.insert(outputColumnNames, args["output_column" .. #outputColumnNames + 1])
end
if not outputColumnNames[1] or outputColumnNames[1] == "" then
return fallback(args)
end
-- Map column name -> index
local outputColumnIdxs, numFound = {}, 0
for i, field in ipairs(dataset.schema.fields) do
for _, wanted in ipairs(outputColumnNames) do
if field.name == wanted and not outputColumnIdxs[wanted] then
outputColumnIdxs[wanted] = i
numFound = numFound + 1
end
end
if numFound == #outputColumnNames then break end
end
if numFound < #outputColumnNames then
return fallback(args)
end
-- Normalize row index (allow negative)
local nrows = #dataset.data
if nrows == 0 then return fallback(args) end
if rowIdx > 0 then
rowIdx = (rowIdx - 1) % nrows + 1
elseif rowIdx < 0 then
rowIdx = rowIdx % nrows + 1
else
return fallback(args)
end
local record = dataset.data[rowIdx]
if not record then return fallback(args) end
-- Output columns
if outputFormat or #outputColumnNames > 1 then
local values = {}
for _, name in ipairs(outputColumnNames) do
table.insert(values, record[ outputColumnIdxs[name] ])
end
return asciiDigits(outputFormat and mw.ustring.format(outputFormat, unpack(values))
or mw.text.listToText(values))
else
return asciiDigits(record[ outputColumnIdxs[ outputColumnNames[1] ] ] or fallback(args))
end
end
--- Public
function p.cell(frame) return p._cell(frame.args) end
-- Internal: lookup row by value/pattern in a column, then reuse _cell to pick columns
function p._lookup(args)
-- If template-like call omitted search_column but gave POP=, map it
if (args.POP or args.Pop or args.pop) and (not args.search_value) then
args.search_value = args.POP or args.Pop or args.pop
end
if not args.search_column and args[1] and mw.ustring.lower(args[1]) == "name" then
args.search_column = "Name"
end
if not args.output_column then
args.output_column = "Population"
end
local dataset, err = resolveDataset(args)
if not dataset then
return fallback(args)
end
local searchColumn = args.search_column
if not searchColumn or searchColumn == "" then
return fallback(args)
end
-- Find search column index
local searchColumnIdx
for i, field in ipairs(dataset.schema.fields) do
if field.name == searchColumn then searchColumnIdx = i; break end
end
if not searchColumnIdx then
return fallback(args)
end
local occurrence = tonumber(asciiDigits(args.occurrence)) or 1
local startIdx, endIdx, step = 1, #dataset.data, 1
if occurrence < 0 then startIdx, endIdx, step = #dataset.data, 1, -1 end
local searchValue, searchPattern = args.search_value, args.search_pattern
local numMatches = 0
for i = startIdx, endIdx, step do
local rec = dataset.data[i]
local cellVal = rec and rec[searchColumnIdx]
if (searchValue and cellVal == searchValue)
or (searchPattern and mw.ustring.match(tostring(cellVal), searchPattern)) then
numMatches = numMatches + 1
if numMatches == math.abs(occurrence) then
local a = mw.clone(args)
a.data, a.output_row = dataset, i
return p._cell(a)
end
end
end
return fallback(args)
end
--- Public
function p.lookup(frame) return p._lookup(frame.args) end
-- Columns lister to help debugging/templates (optional to use)
function p.columns(frame)
local dataset = select(1, resolveDataset(frame.args))
if not dataset then return "" end
return joinFieldNames(dataset)
end
-- Render a wikitable (keeps explicit error messages)
function p._wikitable(args)
local pageName = args[1] or args.page or args.src or args.data
local dataset, err = safeGetData(pageName)
if not dataset then
return string.format('<strong class="error">%s</strong>', err)
end
local datatypes = {}
local htmlTable = mw.html.create("table"):addClass("wikitable sortable")
local dataTitle = normalizeDataTitle(pageName) or ""
htmlTable
:tag("caption")
:wikitext(navbar.navbar({ template=dataTitle, mini="y", style="float: right;", "view","edit" }))
:wikitext(asciiDigits(dataset.description or ""))
-- Header
local headerRow = htmlTable:tag("tr")
for i, field in ipairs(dataset.schema.fields) do
datatypes[i] = field.type
headerRow
:tag("th")
:attr("scope", "col")
:attr("data-sort-type", (datatypes[i] == "text") and "string" or datatypes[i])
:wikitext(asciiDigits(field.title or field.name or ("col"..i)))
end
-- Rows
for _, record in ipairs(dataset.data) do
local row = htmlTable:tag("tr")
for j = 1, #dataset.schema.fields do
local cell = row:tag("td")
local val = record[j]
if val ~= nil then
local dtype = datatypes[j]
if dtype == "number" and type(val) == "number" then
cell:attr("align", "right"); writeCellText(cell, val)
elseif dtype == "boolean" and type(val) == "boolean" then
cell:addClass(val and "table-yes" or "table-no")
:css({ background = val and bgColors["true"] or bgColors["false"],
color = val and colors["true"] or colors["false"],
["vertical-align"]="middle", ["text-align"]="center" })
:wikitext(messages[val and "true" or "false"])
else
writeCellText(cell, val)
end
else
writeCellText(cell, nil)
end
end
end
-- Footer
local footer = htmlTable:tag("tr"):tag("td")
footer:addClass("sortbottom"):attr("colspan", #dataset.schema.fields)
if dataset.sources then footer:wikitext(asciiDigits(dataset.sources)):tag("br") end
if dataset.license and dataset.license.url and dataset.license.text then
local licenseText = mw.message.new("Jsonconfig-license",
mw.ustring.format("[%s %s]", dataset.license.url, dataset.license.text))
footer:tag("i"):wikitext(tostring(licenseText))
end
return tostring(htmlTable)
end
function p.wikitable(frame) return p._wikitable(frame.args) end
--------------------------------------------------------------------------------
return p