Jump to content
Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:Accession descriptors

From Landrace.wiki

Documentation for this module may be created at Module:Accession descriptors/doc

--[[
Map an accession's plant descriptors to the wiki's descriptor vocabulary: the character names and
state words of UPOV TG/276/2 (2025), the hemp and cannabis test guideline, with in-house states where
no standard defines one. The wiki uses the words only. A value here is an observation of a population,
never a UPOV note or a trial result.

Template:Accession passes each descriptor parameter through this module twice: once for the value
Semantic MediaWiki stores, once for the infobox. The page source is never changed. A page that says
"Christmas tree" stores and shows the plant shape "Conical"; "Intermediate" leaflets store "Medium".

Every value of a parameter falls in one of five kinds (the tables below):
* a state of the parameter's own character, stored and shown ("Intermediate" -> leaflet width Medium);
* a state of another character, stored and shown there ("Purples" in leaf_color -> leaf anthocyanin
  colour Purple; "Foxtailed" in flower_structure -> flower cluster shape Foxtailed);
* not a state at all (a condition, a growth stage, a texture): shown in plain words under
  "Also noted" and not stored ("Woody" stem colour -> "woody stem");
* recorded in the right character but not a standard state: shown as written, not stored;
* a placeholder ("Unknown"): nothing.
A value the tables do not list is passed through as written, stored and shown in its own character,
so SMW checks it against the property's allowed values exactly as before.

Functions (named arguments are the template's parameters, by name; pass every value by name):
  store    |<character>|<param>=… -> the stored states, separated by ";" (use with #set |+sep=;)
  display  |<character>|<param>=… -> the infobox text for one character
  noted    |<param>=…|<param>=…   -> the infobox text for "Also noted"
  formLabel|1=<value>             -> the label Form:Accession shows for a stored state
]]
local p = {}

-- The characters: the property each one stores, and the parameters that feed it, in display order.
-- A parameter named here but not in its own FIELDS entry feeds the character through its table.
local CHARACTERS = {
	sex_expression = { 'sexual_characteristics' },
	photoperiod = { 'photoperiod_response' },
	chemotype = { 'chemotype' },
	plant_shape = { 'growth_pattern' },
	branching = { 'branching' },
	leaflet_width = { 'leaf_shape' },
	leaf_green = { 'leaf_color' },
	leaf_anthocyanin = { 'leaf_color', 'leaf_anthocyanin' },
	stem_colour = { 'stem_color' },
	stigma_colour = { 'stigma_color' },
	leaflet_serration = { 'serration' },
	flower_density = { 'flower_structure', 'flower_density' },
	flower_cluster_size = { 'flower_structure', 'flower_cluster_size' },
	flower_cluster_shape = { 'flower_structure', 'flower_cluster_shape' },
	terpenes = { 'terpene_profile' },
	aroma = { 'aroma', 'terpene_profile' },
	seed_viability = { 'seed_viability' },
}

--[[
FIELDS[param]: own = the parameter's own character (nil: its unlisted values go to "Also noted");
multi = the parameter holds a comma-separated list (otherwise the whole value is one value);
map = lower-cased value -> one of:
  'State'                          a state of the own character
  { 'character', 'State' }         a state of another character
  { note = 'plain words' }         not a state: "Also noted", not stored
  { asis = true }                  shown as written in the own character, not stored
  { 'character', 'State', show = 'text' }   stored as State, shown as text
  false                            a placeholder: nothing
The standard states map to themselves, so a value written in the vocabulary's own words keeps working.
]]
local FIELDS = {
	sexual_characteristics = { own = 'sex_expression', map = {
		dioecious = 'Dioecious', monoecious = 'Monoecious',
		unknown = false,
	} },
	photoperiod_response = { own = 'photoperiod', map = {
		['short-day'] = 'Short-day', ['day-neutral'] = 'Day-neutral', other = 'Other',
		['semi-autoflowering'] = { 'photoperiod', 'Other', show = 'Other ("semi-autoflowering")' },
		unknown = false,
	} },
	chemotype = { own = 'chemotype', map = {
		['thc-predominant'] = 'THC-predominant', ['mixed thc and cbd'] = 'Mixed THC and CBD',
		['cbd-predominant'] = 'CBD-predominant', ['cbg-predominant'] = 'CBG-predominant',
		['low cannabinoid content'] = 'Low cannabinoid content', other = 'Other', segregating = 'Segregating',
		['thc-dominant'] = 'THC-predominant', ['cbd-dominant'] = 'CBD-predominant',
		['mixed (thc-dominant, cbd-dominant, thcv)'] = 'Segregating',
		['high thc'] = { note = 'high THC' },
		unknown = false,
	} },
	growth_pattern = { own = 'plant_shape', multi = true, map = {
		conical = 'Conical', columnar = 'Columnar', bushy = 'Bushy', candelabra = 'Candelabra',
		spreading = 'Spreading', sprawling = 'Sprawling',
		['christmas tree'] = 'Conical', menorah = 'Candelabra', creeper = 'Sprawling', ['vine-like'] = 'Sprawling',
		unknown = false,
	} },
	branching = { own = 'branching', map = {
		weak = 'Weak', medium = 'Medium', strong = 'Strong',
		minimal = 'Weak', moderate = 'Medium', extensive = 'Strong',
		['apical dominant'] = { note = 'apical dominance' },
		unknown = false,
	} },
	leaf_shape = { own = 'leaflet_width', map = {
		narrow = 'Narrow', medium = 'Medium', broad = 'Broad',
		intermediate = 'Medium', ['skinny leaf'] = 'Narrow',
		unknown = false,
	} },
	leaf_color = { own = 'leaf_green', multi = true, map = {
		light = 'Light', medium = 'Medium', dark = 'Dark',
		['light greens'] = 'Light', ['dark greens'] = 'Dark',
		purples = { 'leaf_anthocyanin', 'Purple' }, reds = { 'leaf_anthocyanin', 'Red' },
		blacks = { 'leaf_anthocyanin', 'Very dark purple' },
		purple = { 'leaf_anthocyanin', 'Purple' }, red = { 'leaf_anthocyanin', 'Red' },
		['very dark purple'] = { 'leaf_anthocyanin', 'Very dark purple' },
		yellowing = { note = 'yellowing leaves' }, browning = { note = 'browning leaves' },
		bruised = { note = 'bruised leaves' }, variegated = { note = 'variegated leaves' },
		unknown = false,
	} },
	leaf_anthocyanin = { own = 'leaf_anthocyanin', multi = true, map = {
		purple = 'Purple', red = 'Red', ['very dark purple'] = 'Very dark purple',
		unknown = false,
	} },
	stem_color = { own = 'stem_colour', multi = true, map = {
		yellow = 'Yellow', ['medium green'] = 'Medium green', ['dark green'] = 'Dark green', purple = 'Purple',
		['light greens'] = 'Medium green', ['dark greens'] = 'Dark green', reds = 'Purple', purples = 'Purple',
		woody = { note = 'woody stem' }, striped = { note = 'striped stem' },
		yellowing = { note = 'yellowing stem' },
		unknown = false,
	} },
	stigma_color = { own = 'stigma_colour', multi = true, map = {
		white = 'White', pink = 'Pink', red = 'Red', orange = 'Orange', brown = 'Brown',
		['pink hairs'] = 'Pink', ['red hairs'] = 'Red', ['orange hairs'] = 'Orange', ['brown hairs'] = 'Brown',
		immature = { note = 'immature stigmas' },
		unknown = false,
	} },
	serration = { own = 'leaflet_serration', map = {
		shallow = 'Shallow', medium = 'Medium', deep = 'Deep',
		smooth = { asis = true },
		sharp = { note = 'sharp leaflet teeth' },
		other = false, unknown = false,
	} },
	flower_structure = { multi = true, map = {
		loose = { 'flower_density', 'Loose' }, airy = { 'flower_density', 'Loose' },
		fluffy = { 'flower_density', 'Loose' }, dense = { 'flower_density', 'Dense' },
		small = { 'flower_cluster_size', 'Small' }, big = { 'flower_cluster_size', 'Large' },
		large = { 'flower_cluster_size', 'Large' },
		foxtailed = { 'flower_cluster_shape', 'Foxtailed' }, running = { 'flower_cluster_shape', 'Elongated' },
		elongated = { 'flower_cluster_shape', 'Elongated' }, compact = { 'flower_cluster_shape', 'Compact' },
		thick = { note = 'thick flower clusters' }, thin = { note = 'thin flower clusters' },
		unknown = false,
	} },
	flower_density = { own = 'flower_density', multi = true, map = {
		loose = 'Loose', medium = 'Medium', dense = 'Dense', unknown = false,
	} },
	flower_cluster_size = { own = 'flower_cluster_size', multi = true, map = {
		small = 'Small', medium = 'Medium', large = 'Large', unknown = false,
	} },
	flower_cluster_shape = { own = 'flower_cluster_shape', multi = true, map = {
		compact = 'Compact', elongated = 'Elongated', foxtailed = 'Foxtailed', unknown = false,
	} },
	-- Named terpenes, from a test. A compound named with no test behind it is shown as written and
	-- not stored; aroma words written here are shown and stored as aroma.
	terpene_profile = { own = 'terpenes', map = {
		unknown = false,
		['myrcene dominant'] = { asis = true },
		['earthy spice, herbal musk, tropical fruit undertones'] = { 'aroma', 'Earthy spice, herbal musk, tropical fruit undertones' },
		['earthy spice, herbal musk, woody undertones'] = { 'aroma', 'Earthy spice, herbal musk, woody undertones' },
		['earthy spice, herbal musk, woody undertones, bright sweet fruit, mango'] = { 'aroma', 'Earthy spice, herbal musk, woody undertones, bright sweet fruit, mango' },
		['earthy spice, herbal musk, woody undertones, sweet fruit, mango'] = { 'aroma', 'Earthy spice, herbal musk, woody undertones, sweet fruit, mango' },
		['earthy spice, herbal musk, mango, tropical fruit undertones'] = { 'aroma', 'Earthy spice, herbal musk, mango, tropical fruit undertones' },
		['mango, tropical fruit, floral, earthy spice, herbal musk'] = { 'aroma', 'Mango, tropical fruit, floral, earthy spice, herbal musk' },
		['earthy spice, herbal musk, woody undertones, mango, subtle herbal notes'] = { 'aroma', 'Earthy spice, herbal musk, woody undertones, mango, subtle herbal notes' },
		['earthy spice, herbal musk, woody undertones, sweet tropical fruit, mango'] = { 'aroma', 'Earthy spice, herbal musk, woody undertones, sweet tropical fruit, mango' },
		['myrcene dominant, earthy spice, herbal musk, woody undertones, tropical fruit, mango'] = { 'aroma', 'Myrcene dominant, earthy spice, herbal musk, woody undertones, tropical fruit, mango' },
		['incense, bright lemon citrus'] = { 'aroma', 'Incense, bright lemon citrus' },
	} },
	aroma = { own = 'aroma', map = {} },
	seed_viability = { own = 'seed_viability', map = {
		viable = 'Viable', marginal = 'Marginal', ['not viable'] = 'Not viable',
		unknown = false, ['n/a'] = false,
	} },
}

-- What Form:Accession shows for a stored state, where the vocabulary's word is not the one growers use.
local FORM_LABELS = {
	Conical = 'Conical (Christmas tree)',
	Candelabra = 'Candelabra (menorah)',
	Sprawling = 'Sprawling (creeper, vine-like)',
	Elongated = 'Elongated (running)',
}

local function trim(s)
	return mw.text.trim(s or '')
end

local function split(value, multi)
	value = trim(value)
	if value == '' then
		return {}
	end
	if not multi then
		return { value }
	end
	local out = {}
	for _, v in ipairs(mw.text.split(value, ',', true)) do
		v = trim(v)
		if v ~= '' then
			table.insert(out, v)
		end
	end
	return out
end

-- One value of one parameter -> { char =, state =, show =, store =, note = } or nil (nothing).
local function classify(param, value)
	local f = FIELDS[param]
	if not f then
		return nil
	end
	local m = f.map[mw.ustring.lower(value)]
	if m == false then
		return nil
	end
	if m == nil then
		if f.own then
			return { char = f.own, state = value, show = value, store = true }
		end
		return { note = value }
	end
	if type(m) == 'string' then
		return { char = f.own, state = m, show = m, store = true }
	end
	if m.note then
		return { note = m.note }
	end
	if m.asis then
		return { char = f.own, show = value, store = false }
	end
	return { char = m[1], state = m[2], show = m.show or m[2], store = true }
end

-- All classified values of the parameters in args, in parameter then value order.
local function collect(args, params)
	local out = {}
	for _, param in ipairs(params) do
		local f = FIELDS[param]
		if f then
			for _, v in ipairs(split(args[param], f.multi)) do
				local c = classify(param, v)
				if c then
					table.insert(out, c)
				end
			end
		end
	end
	return out
end

local function addUnique(list, seen, s)
	if s and s ~= '' and not seen[s] then
		seen[s] = true
		table.insert(list, s)
	end
end

-- "Conical, bushy": a list in running text, capital only at the start. A word that starts with two
-- capitals (THC-predominant) keeps them.
local function sentence(list, sep)
	local out = {}
	for i, s in ipairs(list) do
		if i > 1 and not mw.ustring.match(s, '^%u%u') then
			s = mw.ustring.lower(mw.ustring.sub(s, 1, 1)) .. mw.ustring.sub(s, 2)
		end
		out[i] = s
	end
	local text = table.concat(out, sep or ', ')
	return mw.ustring.upper(mw.ustring.sub(text, 1, 1)) .. mw.ustring.sub(text, 2)
end

function p._store(char, args)
	local list, seen = {}, {}
	for _, c in ipairs(collect(args, CHARACTERS[char] or {})) do
		if c.char == char and c.store then
			addUnique(list, seen, c.state)
		end
	end
	return table.concat(list, ';')
end

function p._display(char, args)
	local list, seen = {}, {}
	for _, c in ipairs(collect(args, CHARACTERS[char] or {})) do
		if c.char == char then
			addUnique(list, seen, c.show)
		end
	end
	if #list == 0 then
		return ''
	end
	if char == 'aroma' or char == 'terpenes' then
		-- free text: each value is a description of its own, shown as written
		return table.concat(list, '; ')
	end
	return sentence(list)
end

local NOTED_ORDER = { 'growth_pattern', 'branching', 'leaf_shape', 'leaf_color', 'stem_color', 'stigma_color',
	'serration', 'flower_structure', 'chemotype' }

function p._noted(args)
	local list, seen = {}, {}
	for _, c in ipairs(collect(args, NOTED_ORDER)) do
		if c.note then
			addUnique(list, seen, c.note)
		end
	end
	if #list == 0 then
		return ''
	end
	return sentence(list)
end

function p._formLabel(value)
	value = trim(value)
	return FORM_LABELS[value] or value
end

local function namedArgs(frame)
	local args = {}
	for k, v in pairs(frame.args) do
		if type(k) == 'string' then
			args[k] = v
		end
	end
	return args
end

function p.store(frame)
	return p._store(trim(frame.args[1]), namedArgs(frame))
end

function p.display(frame)
	return p._display(trim(frame.args[1]), namedArgs(frame))
end

function p.noted(frame)
	return p._noted(namedArgs(frame))
end

function p.formLabel(frame)
	return p._formLabel(frame.args[1])
end

-- For checks: the tables, read-only.
p.CHARACTERS, p.FIELDS, p.FORM_LABELS = CHARACTERS, FIELDS, FORM_LABELS

return p