Modulo:ChronoSpine/group

Da Wikitrek.
Vai alla navigazione Vai alla ricerca

La documentazione per questo modulo può essere creata in Modulo:ChronoSpine/group/man

--[[
Module:ChronoSpine/group
==========================

Purpose
-------
Takes the flat, unsorted list of normalised entries produced by any
adapter (`/manual` today; `/smw`, `/wikibase` in later steps) and
turns it into the ordered list of groups the renderer (Step 4) will
walk: sorting entries (per Module:ChronoSpine/entry's M.compare) and
bucketing them into year / month / day / season groups, with the
"Data incerta" group always last regardless of bucketing mode.

Like /manual, this module is `tempo`-independent: nothing here reads
or branches on `tempo` (reale/narrativo) -- bucketing by date fields
or by the free-text `gruppo` field works identically either way.

What a group looks like
-------------------------
```lua
{
  key     = { anno = 2257 },  -- the bucket's key fields (shape
                               -- depends on `raggruppa`; see below)
  kind    = "normal",          -- or "incerta" for the one, always-last,
                               -- uncertain-dates group
  entries = { <entry>, ... },  -- this bucket's entries, already in
                               -- chronological (sorted) order
}
```

This module only returns `key`'s raw field values -- never a display
label ("2257", "Maggio 2022", "Stagione 3", "Data incerta"). Turning a
key into text is /i18n's and the renderer's job (Step 4), since the
label format is inherently about display and (for the "Data incerta"
case) language, neither of which belongs in this module.

Also explicitly out of scope here, for the same "this module only
decides *what the groups are*, not *how to display them*" reason:
  * `comprimi` (collapsing long runs of a group's entries in display)
  * synthesising placeholder empty-year groups to fill gaps in an
    otherwise-continuous range
Both are display decisions the renderer makes about an already-built
group list, not something this module's output needs to anticipate.

Grouping modes (`raggruppa`)
------------------------------
  "anno"     (default, and the fallback for anything unrecognised)
             -- bucket key: { anno }
  "mese"     -- bucket key: { anno, mese }. An entry with a year but
                no month gets its own bucket within that year (all
                such entries in the same year share ONE bucket,
                positioned as if `mese` were earlier than any actual
                month -- see Module:ChronoSpine/entry's
                compare_dates_only, which this mirrors).
  "giorno"   -- bucket key: { anno, mese, giorno }, with the same
                cascading idea one level deeper.
  "stagione" -- bucket key: { gruppo } (the entry's own free-text
                field). Entries with no `gruppo` at all share one
                "ungrouped" bucket.

Why bucket by key, not by contiguous runs
--------------------------------------------
For "anno"/"mese"/"giorno", entries sharing a bucket key are always
contiguous once sorted by date -- but that is NOT guaranteed for
"stagione": in a combined view, one season's entries could in
principle be interleaved with another group's. So grouping here works
by looking each entry's key up in a map: if the key has been seen
before, the entry joins that existing group wherever it already is in
the output; otherwise a new group is created. A group's position in
the final output is simply wherever its first (chronologically
earliest) member appeared. This gives correct, non-fragmented results
in every mode, including the "stagione" interleaving case.
--]]

local M = {}

local entry = require("Module:ChronoSpine/entry")

--[[
build_key(e, raggruppa)
-------------------------
Private helper. Computes both forms of one entry's bucket key for the
given `raggruppa` mode: a plain table (for the group's public `.key`
field) and a string (used only internally, as a map lookup key, since
Lua tables cannot be compared or hashed by value).

An unrecognised `raggruppa` (including `nil`) falls through to the
same behaviour as "anno" -- deliberately not an error: the project
briefing treats this as a template-level concern (Step 5's
TemplateData can restrict the parameter to a fixed set of valid
values), not something worth its own error code the way per-entry
data quality is in Module:ChronoSpine/manual.

Parameters:
  e         (table)       -- one normalised entry.
  raggruppa (string|nil)  -- "anno", "mese", "giorno", "stagione", or
                             anything else (treated as "anno").

Returns:
  key        (table)  -- the fields to expose on the group.
  key_string (string) -- a string uniquely identifying this key, safe
                         to use as a table index.
--]]
local function build_key(e, raggruppa)
	if raggruppa == "stagione" then
		return { gruppo = e.gruppo }, "gruppo:" .. tostring(e.gruppo)
	elseif raggruppa == "giorno" then
		return { anno = e.anno, mese = e.mese, giorno = e.giorno },
			"anno:" .. tostring(e.anno) .. "|mese:" .. tostring(e.mese) .. "|giorno:" .. tostring(e.giorno)
	elseif raggruppa == "mese" then
		return { anno = e.anno, mese = e.mese },
			"anno:" .. tostring(e.anno) .. "|mese:" .. tostring(e.mese)
	else
		-- "anno", or anything unrecognised: default behaviour.
		return { anno = e.anno }, "anno:" .. tostring(e.anno)
	end
end

--[[
M.group(entries, raggruppa)
------------------------------
Sorts `entries` and buckets them into groups.

Parameters:
  entries   (table)      -- a flat list of normalised entries (see
                             Module:ChronoSpine/entry), in any order.
  raggruppa (string|nil) -- one of "anno" (default), "mese", "giorno",
                             "stagione".

Returns:
  (table) -- an ordered list of groups, each
             { key = {...}, kind = "normal"|"incerta", entries = {...} },
             with the "incerta" group (if any uncertain entries exist)
             always last. Never mutates `entries` itself: sorting
             happens on a shallow copy.
--]]
function M.group(entries, raggruppa)
	-- Shallow-copy before sorting: table.sort mutates in place, and
	-- the caller's list is not ours to reorder.
	local sorted = {}
	for _, e in ipairs(entries) do
		table.insert(sorted, e)
	end
	table.sort(sorted, entry.compare)

	-- "Data incerta" entries never take part in key-based bucketing,
	-- regardless of `raggruppa`: pull them out first, keeping the
	-- relative order M.compare already gave them (which, for two
	-- uncertain entries, is their input order via `ordine`).
	local normal_entries = {}
	local incerta_entries = {}
	for _, e in ipairs(sorted) do
		if e.incerta then
			table.insert(incerta_entries, e)
		else
			table.insert(normal_entries, e)
		end
	end

	-- Bucket the normal entries by key, tracking first-seen order.
	local group_order = {}      -- list of key_strings, in first-seen order
	local groups_by_key = {}    -- key_string -> group table

	for _, e in ipairs(normal_entries) do
		local key, key_string = build_key(e, raggruppa)
		local group = groups_by_key[key_string]
		if not group then
			group = { key = key, kind = "normal", entries = {} }
			groups_by_key[key_string] = group
			table.insert(group_order, key_string)
		end
		table.insert(group.entries, e)
	end

	local groups = {}
	for _, key_string in ipairs(group_order) do
		table.insert(groups, groups_by_key[key_string])
	end

	if #incerta_entries > 0 then
		table.insert(groups, { key = {}, kind = "incerta", entries = incerta_entries })
	end

	return groups
end

return M