Module:ProjectCounts

From BOINC Projects
Revision as of 20:44, 30 September 2026 by Al Piskun (talk | contribs)
Jump to navigation Jump to search

Module:ProjectCounts computes the figures shown in the statistics block on the Main Page (projects listed, screensavers archived, etc.) directly from the wikitext of the relevant pages, so the numbers stay current as tables and lists are edited. It is also used for section row counts on the BOINC projects page itself.

The module reads the source wikitext of target pages at parse time. Each page it reads is registered as a parser dependency, so any page using these invokes re-renders automatically when the target page changes. No manual purging or updating of figures is needed.

Live test

The following invokes run against live data. Expected values as of October 2026 — 38, 6, 293, 337, 71, 101 — will drift upward as the wiki grows; a sudden 0 or a red "Script error" means something broke (check section heading names first).

Active: 37

Private: 6

Completed: 294

Listed: 337

Recordings: Script error: The function "recordings" does not exist.

Documented: 101

Functions

count

Counts the data rows in the wikitable(s) of one section of a page.

Usage:

{{#invoke:ProjectCounts|count|Section name|page=Page title}}
  • 1 (required) — the section heading, exactly as written on the page (without the equals signs; any heading level works).
  • page (optional) — page to read; defaults to BOINC projects.

Examples:

Call Returns
{{#invoke:ProjectCounts|count|Active BOINC Projects}} 38
{{#invoke:ProjectCounts|count|Completed BOINC Projects}} 293
{{#invoke:ProjectCounts|count|Screensaver Gallery|page=BOINC project screensavers}} 35 (project rows; see recordings below)

Return values recorded October 2026; they change as the tables grow.

total

Sums the data rows of several sections of one page. This produces the "Projects listed" figure on the Main Page.

Usage:

{{#invoke:ProjectCounts|total|Active BOINC Projects|Private BOINC Projects|Completed BOINC Projects}}

Returns 337 (38 + 6 + 293 as of October 2026). page again defaults to BOINC projects.

recordings

Counts unique YouTube recordings in a section, by deduplicating video IDs found in watch?v= links, youtu.be/ short links, and img.youtube.com/vi/ thumbnail references. Sections where one table row holds several videos are counted correctly — this measures recordings, not rows.

Usage:

{{#invoke:ProjectCounts|recordings|Screensaver Gallery|page=BOINC project screensavers}}
{{#invoke:ProjectCounts|recordings}}  

Returns 71 as of October 2026.

documented

Counts distinct existing mainspace articles linked from a section — link targets must exist (redlinks are skipped) and namespaced links (File:, Category:, etc.) are ignored. Defaults to the curated project list on the Main Page.

Usage:

{{#invoke:ProjectCounts|documented|Local BOINC project wiki pages|page=Main Page}}
{{#invoke:ProjectCounts|documented}}  

Returns 102 as of October 2026 (101 project articles plus the link to the BOINC projects master table itself).

Note: the Main Page "Projects documented" stat currently uses {{PAGESINCATEGORY:BOINC projects|pages}} rather than this function. Infobox software automatically adds Category:BOINC projects to every article containing it, so the category count reflects documented project pages without relying on the curated list. documented remains available for counting any curated link list per se.

How the counting works

Section slicing
The named heading is located at any heading level; the section runs until the next heading of the same or higher level (or end of page). Sub-sections are therefore included, but siblings are not. The heading name is matched exactly — a == Screensaver Gallery == invoke will no longer match if the heading is later renamed, e.g. to "Gallery". If a function suddenly returns 0 after an edit, check the heading text first.
Row counting (count, total)
Data rows = number of |- row separators at line start, minus one per table for its header separator. Assumes each table has exactly one header row and that every data row begins with |- on its own line — the format used by all project tables on this wiki.

Assumptions and limitations

  • Table-format dependent in the manner described above. Free-form or multi-header tables will mis-count.
  • Correct results depend on section headings staying constant; consider them part of the interface.
  • Lua cannot enumerate category members or template transclusions. For "pages in a category" use {{PAGESINCATEGORY:Category name|pages}}; for "pages using a template" see Special:WhatLinksHere/Template:Infobox software.

Where the module is used

Statistic Location Function used
Projects listed Main Page total over the three sections of BOINC projects
Screensavers archived Main Page recordings on BOINC project screensavers
Section row counts BOINC projects section headers count per section
Projects documented Main Page (category-based: {{PAGESINCATEGORY:BOINC projects|pages}}, powered by Template:Infobox software)

Maintenance

An off-wiki audit script (wiki_reconcile.py, pure Python, no dependencies) cross-checks the curated Main Page list against pages transcluding the infobox; it reports documented-but-unlisted pages, listed pages lacking an infobox, and redlinks. Run it after large re-organizations of the project listings.


-- Module:ProjectCounts
local p = {}

local function esc(s) return (s:gsub('(%W)', '%%%1')) end

local function getContent(page)
	local t = mw.title.new(page)
	if not t then return nil end
	return t:getContent()
end

-- Extract the wikitext of one section: from its heading (any level)
-- to the next heading of the same or higher level.
local function getSection(content, section)
	local hStart, hEnd, marks = content:find('\n(=+)%s*' .. esc(section) .. '%s*=*')
	if not hStart then return nil end
	local depth = #marks
	local secEnd = #content + 1
	local pos = hEnd
	while true do
		local nStart, nEnd, nMarks = content:find('\n(=+)', pos)
		if not nStart then break end
		if #nMarks <= depth then secEnd = nStart break end
		pos = nEnd
	end
	return '\n' .. content:sub(hEnd + 1, secEnd - 1)
end

-- Data rows = row separators minus one header separator per table.
local function countRows(body)
	local _, tables = body:gsub('\n{|', '')
	local _, seps   = body:gsub('\n|%-', '')
	return math.max(seps - tables, 0)
end

local function countSection(page, section)
	local content = getContent(page)
	if not content then return 0 end
	local body = getSection(content, mw.text.trim(section))
	if not body then return 0 end
	return countRows(body)
end

-- {{#invoke:ProjectCounts|count|Section name|page=Page title}}
function p.count(frame)
	return countSection(frame.args.page or 'BOINC projects', frame.args[1] or '')
end

-- {{#invoke:ProjectCounts|total|Section A|Section B|...|page=Page title}}
function p.total(frame)
	local page = frame.args.page or 'BOINC projects'
	local content = getContent(page)
	if not content then return 0 end
	local sum = 0
	for _, s in ipairs(frame.args) do
		local body = getSection(content, mw.text.trim(s))
		if body then sum = sum + countRows(body) end
	end
	return sum
end

-- {{#invoke:ProjectCounts|documented|Local BOINC project wiki pages|page=Main Page}}
-- Counts distinct mainspace article links in a section, skipping redlinks.
function p.documented(frame)
	local page = frame.args.page or 'Main Page'
	local section = mw.text.trim(frame.args[1] or 'Local BOINC project wiki pages')
	local content = getContent(page)
	if not content then return 0 end
	local body = getSection(content, section)
	if not body then return 0 end
	local seen, n = {}, 0
	for link in body:gmatch('%[%[([^|%]%[#:]+)') do
		link = mw.text.trim(link)
		if link ~= '' and not seen[link] then
			seen[link] = true
			local t = mw.title.new(link)
			if t and t.namespace == 0 and t.exists then
				n = n + 1
			end
		end
	end
	return n
end

return p