WIP: xlua: grid-based contests
5 files changed, 165 insertions(+), 0 deletions(-)

M docs/lua-hlog-module.md
M xlua/CMakeLists.txt
M xlua/scripts/.hgignore
A => xlua/scripts/helper-grid.lua
M xlua/xlua.c
M docs/lua-hlog-module.md +3 -0
@@ 58,6 58,9 @@ hlog.contest_helper
 ### `county(...)`
 TODO
 
+### `grid(...}`
+TODO
+
 
 hlog.country
 ------------

          
M xlua/CMakeLists.txt +1 -0
@@ 31,6 31,7 @@ set(LUA_SCRIPTS
 	contests/NHQP
 	contests/POTA
 	helper-county
+	helper-grid
 	prompt
 	set
 	startup-stats

          
M xlua/scripts/.hgignore +1 -0
@@ 8,6 8,7 @@ contests/NEQP.{luac,c}
 contests/NHQP.{luac,c}
 contests/POTA.{luac,c}
 helper-county.{luac,c}
+helper-grid.{luac,c}
 prompt.{luac,c}
 set.{luac,c}
 startup-stats.{luac,c}

          
A => xlua/scripts/helper-grid.lua +159 -0
@@ 0,0 1,159 @@ 
+--
+-- Copyright (c) 2024-2025 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a copy
+-- of this software and associated documentation files (the "Software"), to deal
+-- in the Software without restriction, including without limitation the rights
+-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+-- copies of the Software, and to permit persons to whom the Software is
+-- furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+-- SOFTWARE.
+--
+
+local mt = { }
+
+local function grid(name, year, duphash)
+	local out = { }
+
+	out['name'] = name -- contest name
+	out['year'] = year -- contest year
+	out['dups'] = hlog.set.new() -- set of dup hashes
+
+	-- set up an object with the data
+	local obj = {
+		data = out,
+		duphash = duphash,
+	}
+
+	setmetatable(obj, mt)
+
+	return obj
+end
+
+local function qso_load(self, qso, tx, rx)
+	local map = self.data
+
+	tx = tx ~= nil and tx or qso.tx.grid
+	rx = rx ~= nil and rx or qso.rx.grid
+
+	map['dups']:add(self.duphash(qso, tx, rx))
+end
+
+local function qso_done(self, qso, tx, rx)
+	local map = self.data
+
+	qso.additional['contest-id'] = map['name']
+	qso.additional['contest-year'] = map['year']
+
+	self:qso_load(qso, tx, rx)
+end
+
+-- is the passed in qso a duplicate with a previous qso?
+local function is_dup(self, qso, tx, rx)
+	local map = self.data
+
+	tx = tx ~= nil and tx or qso.tx.grid
+	rx = rx ~= nil and rx or qso.rx.grid
+
+	return map['dups']:has(self.duphash(qso, tx, rx))
+end
+
+local function export_prep(self, id, year, qpoints, qmult, qexchange)
+	local function do_add_entry(tx, rx, args)
+		local qp = args[1]
+		local all = args[2]
+		local ops = args[3]
+		local points = args[4]
+		local mults = args[5]
+		local qso = args[6]
+
+		local tx_exch, tx_exch_ok = qexchange(qso.tx, tx)
+		local rx_exch, rx_exch_ok = qexchange(qso.rx, rx)
+
+		-- accumulate points
+		if not self:is_dup(qso, tx, rx) and tx_exch_ok and rx_exch_ok then
+			points[1] = points[1] + qpoints(qso, tx, rx)
+		end
+
+		-- add the mult
+		local m = qmult(qso, tx, rx)
+		if m ~= nil then
+			mults:add(m)
+		end
+
+		-- add to list of all QSOs
+		table.insert(all, { qso, tx_exch, rx_exch, tx_exch_ok and rx_exch_ok })
+
+		-- stash for future dup detection
+		self:qso_done(qso, tx, rx)
+
+		-- keep track of the operators
+		ops:add(qso.tx.operator_call)
+	end
+
+	local function add_entry(id, year, station, all, ops, points, mults, qso)
+		if qso.additional['contest-id'] ~= id or
+		   qso.additional['contest-year'] ~= year then
+			return
+		end
+
+		if qso.tx.band == nil then
+			print(string.format("Error: %s QSO lacks band info", qso.uuid))
+			return
+		end
+
+		assert(qso.tx.station_call ~= nil)
+		station:add(qso.tx.station_call)
+
+		hlog.utils.each_with_each(qso.tx.grid,
+					  qso.rx.grid,
+					  "+", do_add_entry,
+					  { self, all, ops, points, mults, qso })
+	end
+
+	local station = hlog.set.new()
+	local all = {}
+	local ops = hlog.set.new()
+	local points = { 0 }
+	local mults = hlog.set.new()
+
+	for qso in hlog.index.history(true) do
+		add_entry(id, year, station, all, ops, points, mults, qso)
+	end
+
+	if station:count_items() ~= 1 then
+		error(string.format("Error: more than one station callsign used: %s",
+				    station:tostring()))
+	end
+
+	station = hlog.utils.join_ipairs(station:as_array(), ",") -- should be only 1 element
+	points = points[1]
+
+	return all, station, ops, points, mults
+end
+
+local function tostring(self)
+	return "Grid{" .. self.data:tostring() .. "}"
+end
+
+mt.__index = {
+	qso_load = qso_load,
+	qso_done = qso_done,
+	is_dup = is_dup,
+	export_prep = export_prep,
+	tostring = tostring,
+}
+
+return {
+	grid = grid,
+}

          
M xlua/xlua.c +1 -0
@@ 68,6 68,7 @@ static void xlua_pushlib_contest_helper(
 {
 	lua_newtable(L);
 	xlua_merge_lua_funcs(L, "helper-county.lua");
+	xlua_merge_lua_funcs(L, "helper-grid.lua");
 }
 
 static void xlua_register(lua_State *L)