Zum Inhalt springen

„Modul:AufEinWortDatumKennung“ – Versionsunterschied

aus AufEinWort, der freien Forschung
KKeine Bearbeitungszusammenfassung
KKeine Bearbeitungszusammenfassung
 
Zeile 267: Zeile 267:
     local poolSuffix = LABELS[token] or token
     local poolSuffix = LABELS[token] or token
     if not poolSuffix or poolSuffix == "" then poolSuffix = "erst." end
     if not poolSuffix or poolSuffix == "" then poolSuffix = "erst." end
     local pool = (args.star_cat and trim(args.star_cat) ~= "" and args.star_cat) or (" Auswahl " .. poolSuffix)
--     local pool = (args.star_cat and trim(args.star_cat) ~= "" and args.star_cat) or (" Auswahl " .. poolSuffix)
    local pool = (args.star_cat and trim(args.star_cat) ~= "" and args.star_cat) or (poolSuffix .. " Auswahl")
     table.insert(out, string.format("[[Kategorie:%s|%s%s %s]]", pool, sp, code, token))
     table.insert(out, string.format("[[Kategorie:%s|%s%s %s]]", pool, sp, code, token))
   end
   end

Aktuelle Version vom 18. August 2026, 09:33 Uhr


-- Modul:AufEinWortDatumKennung
-- Zweck: Auf Kategorieseiten automatisch DEFAULTSORT + Eltern (Monat/Jahr) + Jahrhundert setzen
-- Ermittlung primär aus dem **Seitentitel** (lesbares Datum + Suffix), optional Override über Parameter
-- Unterstützte Token/Suffixe: ESD=erst., BAD=bear., UWD=hrsg., GBD=gebo., STD=gest., EED=eegt.

local p = {}

-- =========================
-- Konfiguration / Labels
-- =========================
local LABELS = {
  ESD = "erst.",
  BAD = "bear.",
  UWD = "hrsg.",
  GBD = "gebo.",
  STD = "gest.",
  EED = "eegt."
}
local LABEL_TO_TOKEN = {}
for k,v in pairs(LABELS) do LABEL_TO_TOKEN[v] = k end

local monatsNamen = {"Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"}
local monate = {
  januar = 1, februar = 2,
  maerz  = 3, ["märz"] = 3,
  april  = 4, mai = 5,
  juni = 6, juli = 7,
  august = 8, september = 9,
  oktober = 10, november = 11,
  dezember = 12
}

-- =========================
-- Hilfsfunktionen
-- =========================
local function trim(s)
  if not s then return s end
  s = mw.ustring.gsub(s, mw.ustring.char(160), " ") -- NBSP → Space
  s = mw.ustring.gsub(s, "%s+", " ")
  s = mw.ustring.match(s, "^%s*(.-)%s*$") or s
  return s
end

local function detectEra(s)
  if not s or s == "" then return 1 end
 if s:find("v. Chr.", 1, true) or s:find("v.Chr.", 1, true) then return 0 end  -- literal search
  
  return 1
end

local function month_to_num(name)
  if not name or name == "" then return 0 end
  local asNum = tonumber(name)
  if asNum and asNum >= 1 and asNum <= 12 then return asNum end
  local key = mw.ustring.lower(name)
  if key == "märz" then key = "maerz" end
  return monate[key] or 0
end

-- parse lesbare Datumsformen aus Titel (ohne Suffix)
-- liefert y, m (0 wenn unbekannt), d (0 wenn unbekannt), era (1 n. Chr., 0 v. Chr.)
local function parse_pretty_date(s)
  s = trim(s or "")
  local era = detectEra(s)

  -- Tag. Monat Jahr  (z. B. "12. März 2025 n. Chr." / "12. Maerz 425 v. Chr.")
  local d, mname, y = mw.ustring.match(s, "^(%d%d?)%.%s*(%S+)%s+(%d+)%s+[nv]%.%s*Chr%.?$")
  if d and mname and y then
    return tonumber(y), month_to_num(mname), tonumber(d), era
  end

  -- Monat Jahr  (z. B. "März 2025 n. Chr.")
  local mname2, y2  = mw.ustring.match(s, "^(%S+)%s+(%d+)%s+[nv]%.%s*Chr%.?$")
  if mname2 and y2 then
    return tonumber(y2), month_to_num(mname2), 0, era
  end

  -- Jahr  (z. B. "2025 n. Chr." / "425 v. Chr.")
  local y3          = mw.ustring.match(s, "^(%d+)%s+[nv]%.%s*Chr%.?$")
  if y3 then
    return tonumber(y3), 0, 0, era
  end

  return nil, nil, nil, era
end

local function yearIndex(era, y)
  if y < 1 then y = 1 end
  if era == 0 then
    local idx = 5000 - y; if idx < 0 then idx = 0 end; return idx
  else
    local idx = 5000 + y; if idx > 9999 then return nil end; return idx
  end
end

local function kennung_parts_from(y, m, d, era, num)
  local yidx = yearIndex(era, y); if not yidx then return nil end
  local Y = string.format("%04d", yidx)
  local M = string.format("%02d", (m or 0))
  local D = string.format("%02d", (d or 0))
  local N = string.format("%02d", tonumber(num) or 0)
  return Y..M..D..N
end

local function infer_from_title(args)
  -- 1) Quelle: Overrides
  local datum = args.Datum
  local token = args.Token
  local nummer = args.Nummer or "00"

  if not datum or datum == "" then
    -- 2) Aus Seitentitel ermitteln
    local title = mw.title.getCurrentTitle().text or ""
    title = trim(title)
    -- Letztes Wort prüfen (Suffix?), z. B. "erst."
    local base, label = mw.ustring.match(title, "^(.-)%s+(%S+)$")
    if (not token or token == "") and label and LABEL_TO_TOKEN[label] then
      -- Erkanntes Suffix → Token setzen und **Suffix aus Datum entfernen**
      token = LABEL_TO_TOKEN[label]
      datum = trim(base or title)
    else
      -- **Kein** bekanntes Suffix → **vollen** Titel als Datum verwenden
      datum = title
    end
  end

  -- Fallback Token (nur wenn nirgends gesetzt)
  token = token or "ESD"

  return datum, token, nummer
end


local function granularity_of(y, m, d)
  -- returns "day" | "month" | "year"
  if (d or 0) > 0 and (m or 0) > 0 then return "day" end
  if (m or 0) > 0 then return "month" end
  return "year"
end

local function sort_prefix_for(args, g)
  local sp = args.sort_prefix
  if sp and sp ~= "" then return sp end
  if g == "day" then return "T"
  elseif g == "month" then return "M"
  else return "J" end
end
-- forward declaration, damit prettyDate in make_defaultsort_and_parents sichtbar ist
local prettyDate

-- Eltern (Monat → Jahr) + unsuffigierte Tages-Kategorie (Sammelpunkt)
local function make_defaultsort_and_parents(args)
  -- 1) Datum/Token/Nummer aus Parametern bzw. Seitentitel ermitteln
  local datum, token, nummer = infer_from_title(args)

  -- 2) Lesbares Datum parsen → y,m,d,era
  local y, m, d, era = parse_pretty_date(datum)
  if not y then
    return ""  -- Titel nicht erkennbar
  end

  -- 3) Kennung (YIDXMMDDNN) + Granularität + Präfix
  local code = kennung_parts_from(y, m or 0, d or 0, era, nummer)
  if not code then return "" end
  local g  = granularity_of(y, m or 0, d or 0)
  local sp = sort_prefix_for(args, g)

  -- 3a) Prüfen, ob der aktuelle Titel ein bekanntes Suffix am Ende hat
  local titleText = trim(mw.title.getCurrentTitle().text or "")
  local _base, endLabel = mw.ustring.match(titleText, "^(.-)%s+(%S+)$")
  local hasSuffix = (args.Token and args.Token ~= "") or (endLabel and LABEL_TO_TOKEN[endLabel] ~= nil)

  -- 4) DEFAULTSORT setzen (per Parserfunktion)
  local out   = {}
  local frame = mw.getCurrentFrame()
  -- Bei unsuffigierten Seiten kein Token in den Sortkey stringen
  local defaultsortKey = sp .. code .. (hasSuffix and (" " .. token) or "")
  table.insert(out, frame:callParserFunction('DEFAULTSORT', defaultsortKey) or "")

  -- 5) Eltern (chain vs. all)
  local parentsMode = tostring(args.parents or "chain")
  local eraLabel    = (era == 0) and " v. Chr." or " n. Chr."
  local suffix      = LABELS[token] or token
  local tokForKeys  = hasSuffix and token or "DT"

  -- Falls unsuffigierter Tag: chain → all (Tag soll direkt Monat + Jahr bekommen)
  local effectiveMode = parentsMode
  if parentsMode == "chain" and g == "day" and not hasSuffix then
    effectiveMode = "all"
  end

  if effectiveMode ~= "0" then
    if effectiveMode == "all" then
      -- ALL: Day → (unsuffigierter Tag, falls suffigiert) + Monat + Jahr
      if g == "day" then
        if hasSuffix then
          -- suffigierter Tag → Parent: unsuffigierter Tag (Sammelknoten)
          local dayTitle = prettyDate(era, y, m, d)         -- ohne Suffix
          local dayKey   = string.format("%s%s %s", sp, code, token)
          table.insert(out, string.format("[[Kategorie:%s|%s]]", dayTitle, dayKey))
        end
        if m and m >= 1 and m <= 12 then
          -- Monatstitel: suffigiert nur, wenn die Ausgangsseite suffigiert war
          local monthTitle = hasSuffix
            and string.format("%s %d%s %s", monatsNamen[m], y, eraLabel, suffix)
            or  string.format("%s %d%s",      monatsNamen[m], y, eraLabel)
          local monthKey   = string.format("%s%s%02d00 %s", sp, string.sub(code,1,4), m, tokForKeys)
          table.insert(out, string.format("[[Kategorie:%s|%s]]", monthTitle, monthKey))
        end
        -- Jahrstitel: suffigiert nur, wenn die Ausgangsseite suffigiert war
        local yearTitle = hasSuffix
          and string.format("%d%s %s", y, eraLabel, suffix)
          or  string.format("%d%s",      y, eraLabel)
        local yearKey   = string.format("%s%s0000 %s",  sp, string.sub(code,1,4), tokForKeys)
        table.insert(out, string.format("[[Kategorie:%s|%s]]", yearTitle, yearKey))

      elseif g == "month" then
        local yearTitle = string.format("%d%s %s", y, eraLabel, suffix)
        local yearKey   = string.format("%s%s0000 %s", sp, string.sub(code,1,4), tokForKeys)
        table.insert(out, string.format("[[Kategorie:%s|%s]]", yearTitle, yearKey))
      else
        -- g == "year": keine Eltern (Jh. kommt unten)
      end

    else
      -- CHAIN (Standard):
      if g == "day" then
        if hasSuffix then
          -- suffigierter Tag → Eltern: unsuffigierte Tages-Kategorie
          local dayTitle = prettyDate(era, y, m, d)         -- ohne Suffix
          local dayKey   = string.format("%s%s %s", sp, code, token)
          table.insert(out, string.format("[[Kategorie:%s|%s]]", dayTitle, dayKey))
        else
          -- unsuffigierter Tag → Eltern: Monat
          if m and m >= 1 and m <= 12 then
            local monthTitle = string.format("%s %d%s", monatsNamen[m], y, eraLabel) -- ohne Suffix
            local monthKey   = string.format("%s%s%02d00 %s", sp, string.sub(code,1,4), m, tokForKeys)
            table.insert(out, string.format("[[Kategorie:%s|%s]]", monthTitle, monthKey))
          end
        end

      elseif g == "month" then
        -- Monat → Jahr (wie gehabt)
        local yearTitle = string.format("%d%s %s", y, eraLabel, suffix)
        local yearKey   = string.format("%s%s0000 %s", sp, string.sub(code,1,4), tokForKeys)
        table.insert(out, string.format("[[Kategorie:%s|%s]]", yearTitle, yearKey))
      else
        -- g == "year": kein Parent (Jh. kommt unten)
      end
    end
  end

  -- 6) Jahrhundert (nur bei Granularität = Jahr)
  local wantJh = tostring(args.jh or "1") ~= "0"
  if wantJh and g == "year" then
    local form   = args.form or "kurz"
    local term   = (form == "lang") and "Jahrhundert" or "Jh."
    local ce     = (era ~= 0)
    local century= math.floor((y - 1) / 100) + 1
    local label  = tostring(century)..". "..term.." "..(ce and "n. Chr." or "v. Chr.")
    table.insert(out, string.format("[[Kategorie:%s|%s%s %s]]", label, sp, code, tokForKeys))
  end

  -- 7) Token-Pool ("Stern") – nur bei suffigierten Seiten sinnvoll
  local star = trim(tostring(args.star or "all"))  -- Default: aktiv
  if star ~= "0" and hasSuffix then
    local poolSuffix = LABELS[token] or token
    if not poolSuffix or poolSuffix == "" then poolSuffix = "erst." end
--     local pool = (args.star_cat and trim(args.star_cat) ~= "" and args.star_cat) or (" Auswahl " .. poolSuffix)
    local pool = (args.star_cat and trim(args.star_cat) ~= "" and args.star_cat) or (poolSuffix .. " Auswahl")
    table.insert(out, string.format("[[Kategorie:%s|%s%s %s]]", pool, sp, code, token))
  end

  return table.concat(out, "\n")
end



-- =========================
-- Öffentliche API
-- =========================
function p.setup(frame)
  local args = frame.args or (frame.getParent and frame:getParent().args) or {}

  -- robuste Defaults (falls Template-Defaults mal nicht greifen)
  if not args.parents or args.parents == "" then args.parents = "chain" end
  if not args.jh or args.jh == "" then args.jh = "1" end
  if not args.form or args.form == "" then args.form = "kurz" end
  if not args.star or args.star == "" then args.star = "all" end  -- Standard: Stern aktiv

  return make_defaultsort_and_parents(args)
end

-- ===== Zusätzliche Parser/Helfer für Werkseiten =====

local function replace_umlauts(s)
  if not s or s == "" then return s end
  local m = { ["Ä"]="Ae", ["ä"]="ae", ["Ö"]="Oe", ["ö"]="oe", ["Ü"]="Ue", ["ü"]="ue", ["ß"]="ss" }
  for k,v in pairs(m) do s = mw.ustring.gsub(s, k, v) end
  return s
end

-- Era erkennen (Minuszeichen vorn, v chr, vor christus, bce, bc); Rückgabe 1=n. Chr., 0=v. Chr.
local function detectEraAndCleanRaw(s)
  if not s or s == "" then return 1, s end
  local era = 1
  local sl = mw.ustring.lower(s)
  sl = mw.ustring.gsub(sl, "%.", " ")
  sl = trim(sl)
  if mw.ustring.match(sl, "^%-")
     or mw.ustring.find(sl, " v chr")
     or mw.ustring.find(sl, " vor christus")
     or mw.ustring.find(sl, " bce")
     or mw.ustring.find(sl, " bc") then
    era = 0
  end
  sl = mw.ustring.gsub(sl, "^%-", "")
  sl = mw.ustring.gsub(sl, " v chr", "")
  sl = mw.ustring.gsub(sl, " vor christus", "")
  sl = mw.ustring.gsub(sl, " bce", "")
  sl = mw.ustring.gsub(sl, " bc", "")
  sl = trim(sl)
  sl = mw.ustring.gsub(sl, " n chr", "")
  sl = mw.ustring.gsub(sl, " nach christus", "")
  sl = mw.ustring.gsub(sl, " ce", "")
  return era, sl
end

-- Flexibles Datums-Parsing (dd.mm.yyyy | yyyy-mm-dd | "Tag Monat Jahr" | "Monat Jahr" | "Jahr")
local function parse_date_flexible_raw(s)
  -- dd.mm.yyyy
  local d, m, y = mw.ustring.match(s, "^(%d%d?)%s*%.%s*(%d%d?)%s*%.%s*(%d+)$")
  if d and m and y then return d, m, y end
  -- yyyy-mm-dd
  local y2, m2, d2 = mw.ustring.match(s, "^(%d+)%-(%d%d?)%-(%d%d?)$")
  if y2 and m2 and d2 then return d2, m2, y2 end
  -- Tag Monat Jahr
  local dd, mname, yy = mw.ustring.match(s, "^(%d+)%s*%.?%s*(%S+)%s+(%d+)$")
  if dd and mname and yy then return dd, mname, yy end
  -- Monat Jahr
  local mname2, yy2 = mw.ustring.match(s, "^(%S+)%s+(%d+)$")
  if mname2 and yy2 then return nil, mname2, yy2 end
  -- Jahr
  local yy3 = mw.ustring.match(s, "^(%d+)$")
  if yy3 then return nil, nil, yy3 end
  return nil, nil, nil
end

-- Komponenten aus frei eingegebenem Datum gewinnen
local function parse_flexible_input(datum)
  local era, cleaned = detectEraAndCleanRaw(datum or "")
  local normalized = replace_umlauts(cleaned)
  local d, mname, y = parse_date_flexible_raw(normalized)
  if not y then return nil end
  local ynum = tonumber(y) or 0
  local mnum = 0
  if mname then
    local asNum = tonumber(mname)
    mnum = asNum or month_to_num(mname) or 0
  end
  local dnum = d and tonumber(d) or 0
  return era, ynum, mnum, dnum
end

function prettyDate(era, y, m, d)
  local eraLabel = (era == 0) and " v. Chr." or " n. Chr."
  if (d or 0) > 0 and (m or 0) >= 1 and m <= 12 then
    return string.format("%d. %s %d%s", d, monatsNamen[m], y, eraLabel)
  elseif (m or 0) >= 1 and m <= 12 then
    return string.format("%s %d%s", monatsNamen[m], y, eraLabel)
  else
    return string.format("%d%s", y, eraLabel)
  end
end

-- Kennung aus Datumsstring (liefert code, era, y, m, d)
local function makeKennungFromDateString(datum, nummer)
  local era, y, m, d = parse_flexible_input(datum or "")
  if not era or not y or y == 0 then return "Fehler: Ungültiges Datumsformat" end
  local code = kennung_parts_from(y, m or 0, d or 0, era, nummer or "00")
  if not code then return "Fehler: Ungültige Jahreszahl" end
  return code, era, y, m, d
end

-- ===== Öffentliche API für Werkseiten =====

-- Liefert die numerische Kennung (YIDXMMDDNN)
function p.kennung(frame)
  local a = frame.args or (frame.getParent and frame:getParent().args) or {}
  local datum = a.Datum or a.Erstelldatum or a.Bearbeitdatum or a.Urwerkdatum or a.Geburtdatum or a.Sterbedatum or a.Ereignisdatum
  local nummer = a.Nummer or "00"
  local code = makeKennungFromDateString(datum, nummer)
  if type(code) == "table" then code = code[1] end
  return code or ""
end

-- Liefert den lesbaren Kategorietitel inkl. Suffix (z. B. "12. März 2025 n. Chr. erst.")
function p.titel(frame)
  local a = frame.args or (frame.getParent and frame:getParent().args) or {}
  local token = a.Token or "ESD"
  local datum = a.Datum or a.Erstelldatum or a.Bearbeitdatum or a.Urwerkdatum or a.Geburtdatum or a.Sterbedatum or a.Ereignisdatum
  local code, era, y, m, d = makeKennungFromDateString(datum, 0)
  if not code or (type(code)=="string" and code:sub(1,1)=="F") then return "unbekannt" end
  local suffix = LABELS[token] or token
  return prettyDate(era, y, m, d) .. " " .. suffix
end

function p.titelMonat(frame)
  local a = frame.args or (frame.getParent and frame:getParent().args) or {}
  local datum = a.Datum or a.Erstelldatum or a.Bearbeitdatum or a.Urwerkdatum or a.Geburtdatum or a.Sterbedatum or a.Ereignisdatum
  local code, era, y, m = makeKennungFromDateString(datum, 0)
  if not code or (type(code)=="string" and code:sub(1,1)=="F") or not m or m==0 then return "" end
  return prettyDate(era, y, m, 0)
end

function p.titelJahr(frame)
  local a = frame.args or (frame.getParent and frame:getParent().args) or {}
  local datum = a.Datum or a.Erstelldatum or a.Bearbeitdatum or a.Urwerkdatum or a.Geburtdatum or a.Sterbedatum or a.Ereignisdatum
  local code, era, y = makeKennungFromDateString(datum, 0)
  if not code or (type(code)=="string" and code:sub(1,1)=="F") or not y or y==0 then return "" end
  return prettyDate(era, y, 0, 0)
end

-- Baut eine komplette SMW-Annotation mit Kategoriewert und Anzeigetext in EINEM Aufruf.
-- Beispiel: [[Urwerkdatum Kategorie::Kategorie:12. März 2025 n. Chr. hrsg.|12. März 2025 n. Chr. hrsg.]]
function p.annotateCat(frame)
  local a = frame.args or (frame.getParent and frame:getParent().args) or {}

  -- Property-Name (SMW-Attribut)
  local prop  = a.Property or "Urwerkdatum Kategorie"

  -- Token: ESD|BAD|UWD|GBD|STD → Suffix kommt aus LABELS via p.titel
  local token = a.Token or "UWD"

  -- Datumseingabe flexibel wie in p.titel
  local title = p.titel{
    args = {
      Token          = token,
      Datum          = a.Datum,
      Urwerkdatum    = a.Urwerkdatum,
      Erstelldatum   = a.Erstelldatum,
      Bearbeitdatum  = a.Bearbeitdatum,   -- dein aktueller Parametername
      Geburtdatum    = a.Geburtdatum,
      Sterbedatum    = a.Sterbedatum,
      Ereignisdatum    = a.Ereignisdatum
    }
  }

  if not title or title == "" then
    return ""
  end

  return string.format("[[%s::Kategorie:%s|%s]]", prop, title, title)
end


local function catlink_for(datum, nummer, token)
  local code, era, y, m, d = makeKennungFromDateString(datum, nummer)
  if not code or (type(code)=="string" and code:sub(1,1)=="F") then return "" end
  local title = prettyDate(era, y, m, d) .. " " .. (LABELS[token] or token)
  local g = granularity_of(y or 0, m or 0, d or 0)
  local sp = sort_prefix_for({}, g)  -- T für Tag, M für Monat, J für Jahr
  local key = sp .. code .. " " .. token
  return string.format("[[Kategorie:%s|%s]]", title, key)
end

local function _catWerk(frame, which)
  local a = frame.args or (frame.getParent and frame:getParent().args) or {}
  local datum = a.Datum or a.Erstelldatum or a.Bearbeitdatum or a.Urwerkdatum or a.Geburtdatum or a.Sterbedatum or a.Ereignisdatum or ""
  local nummer = a.Nummer or "00"
  return catlink_for(datum, nummer, which)
end

function p.catESD(frame) return _catWerk(frame, "ESD") end
function p.catBAD(frame) return _catWerk(frame, "BAD") end
function p.catUWD(frame) return _catWerk(frame, "UWD") end
function p.catGBD(frame) return _catWerk(frame, "GBD") end
function p.catSTD(frame) return _catWerk(frame, "STD") end
function p.catEED(frame) return _catWerk(frame, "EED") end

-- === Kontext-Helfer für Vorlagen (auto, ohne Parameter) ===

-- "ESD"|"BAD"|"UWD"|"GBD"|"STD" oder "DT" (unsuffigierter Tag / kein Suffix)
function p.token(frame)
  -- Erkenne Suffix ausschließlich aus dem aktuellen Seitentitel
  local title = mw.title.getCurrentTitle().text or ""
  title = trim(title)
  local base, label = mw.ustring.match(title, "^(.-)%s+(%S+)$")
  if label and LABEL_TO_TOKEN[label] then
    return LABEL_TO_TOKEN[label]
  end
  return "DT"  -- kein Suffix → unsuffigierter Tag
end

-- unsuffigiertes, lesbares Basisdatum (z. B. "26. Juli 2020 n. Chr.")
function p.basis(frame)
  local a = frame and (frame.args or (frame.getParent and frame:getParent().args)) or {}
  local datum = infer_from_title(a)
  if type(datum) == "table" then datum = datum[1] end
  local y,m,d,era = parse_pretty_date(datum)
  if not y then return "" end
  return prettyDate(era, y, m, d)
end

-- "day"|"month"|"year"
function p.granular(frame)
  local a = frame and (frame.args or (frame.getParent and frame:getParent().args)) or {}
  local datum = infer_from_title(a)
  if type(datum) == "table" then datum = datum[1] end
  local y,m,d,era = parse_pretty_date(datum)
  if not y then return "" end
  return granularity_of(y, m or 0, d or 0)
end

-- === Ranges für Kennungen (Monat/Jahr) ===
local function bounds_from_title(gran)
  local title = mw.title.getCurrentTitle().text or ""
  local y,m,d,era = parse_pretty_date(title)
  if not y then return "", "" end
  local low, high
  if gran == "month" then
    low  = kennung_parts_from(y, m or 0,  0, era,  0)
    high = kennung_parts_from(y, m or 0, 31, era, 99)
  elseif gran == "year" then
    low  = kennung_parts_from(y, 0,       0, era,  0)
    high = kennung_parts_from(y, 12,     31, era, 99)
  else -- day (falls gebraucht)
    low  = kennung_parts_from(y, m or 0, d or 0, era,  0)
    high = kennung_parts_from(y, m or 0, d or 0, era, 99)
  end
  return tostring(low or ""), tostring(high or "")
end

function p.low(frame)
  local gran = (frame.args and frame.args.gran) or p.granular(frame) or "month"
  local low = bounds_from_title(gran)
  return (type(low)=="table") and (low[1] or "") or (low or "")
end

function p.high(frame)
  local gran = (frame.args and frame.args.gran) or p.granular(frame) or "month"
  local _, high = bounds_from_title(gran)
  return tostring(high or "")
end

-- Einzelteile eines Datums/Contexts für SMW-Properties
function p.teil(frame)
  local a = frame.args or (frame.getParent and frame:getParent().args) or {}
  -- Eingabe: Datum + optional Token (für ART)
  local datum = a.Datum or a.Erstelldatum or a.Bearbeitdatum or a.Urwerkdatum or a.Geburtdatum or a.Sterbedatum or a.Ereignisdatum
  local token = a.Token -- ESD/BAD/UWD/GBD/STD, optional
  local F = (a.F or a.f or ""):lower()  -- tag|monat|jahr|jh|chr|art

  -- Falls Token nicht übergeben: Aus aktuellem Titel-Ende ableiten (nur für ART sinnvoll)
  if (not token or token == "") then
    local title = trim(mw.title.getCurrentTitle().text or "")
    local _, label = mw.ustring.match(title, "^(.-)%s+(%S+)$")
    if label and LABEL_TO_TOKEN[label] then token = LABEL_TO_TOKEN[label] end
  end

  local era, y, m, d = parse_flexible_input(datum or "")
  if not era or not y or y == 0 then
    -- Keine gültigen Komponenten → leere Zeichenkette zurück
    return ""
  end

  if F == "tag" then
    return (d and d > 0) and (tostring(d) .. ".") or ""
  elseif F == "monat" then
    return (m and m >= 1 and m <= 12) and monatsNamen[m] or ""
  elseif F == "jahr" then
    return tostring(y)
  elseif F == "jh" or F == "jahrhundert" then
    local century = math.floor((y - 1) / 100) + 1
    return tostring(century) .. ". Jh."
  elseif F == "chr" then
    return (era == 0) and "v. Chr." or "n. Chr."
  elseif F == "art" then
    -- „Art“ kommt vom Token bzw. Seitensuffix
    local label = (token and LABELS[token]) or ""
    return label or ""
  else
    -- Unbekanntes Feld → leer
    return ""
  end
end

return p