WIP: xlua: generalize state/county -type contests
5 files changed, 267 insertions(+), 0 deletions(-)

M docs/lua-hlog-module.md
M xlua/CMakeLists.txt
M xlua/scripts/.hgignore
A => xlua/scripts/helper-county.lua
M xlua/xlua.c
M docs/lua-hlog-module.md +7 -0
@@ 52,6 52,13 @@ prefixes the printed qso line with "X-",
 shouldn't be counted toward the total.
 
 
+hlog.contest_helper
+-------------------
+
+### `county(...)`
+TODO
+
+
 hlog.country
 ------------
 

          
M xlua/CMakeLists.txt +1 -0
@@ 29,6 29,7 @@ set(LUA_SCRIPTS
 	contests/K1USN-SST
 	contests/NEQP
 	contests/POTA
+	helper-county
 	prompt
 	set
 	startup-stats

          
M xlua/scripts/.hgignore +1 -0
@@ 6,6 6,7 @@ contests/CQ-WW-VHF.{luac,c}
 contests/K1USN-SST.{luac,c}
 contests/NEQP.{luac,c}
 contests/POTA.{luac,c}
+helper-county.{luac,c}
 prompt.{luac,c}
 set.{luac,c}
 startup-stats.{luac,c}

          
A => xlua/scripts/helper-county.lua +248 -0
@@ 0,0 1,248 @@ 
+--
+-- Copyright (c) 2022-2024 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 county(counties, state_dxccs, duphash)
+	local out = { }
+
+	local tmp = hlog.set.new()
+	tmp:add(state_dxccs)
+	state_dxccs = tmp
+
+	local county_dxccs = hlog.set.new()
+	local states = hlog.set.new()
+	local name2abbrev = { }
+	local abbrev2name = { }
+	for _, info in ipairs(counties) do
+		local dxcc = info[1]
+		local a = info[3]
+		local n = info[2] .. info[4]
+		local s = info[2]
+
+		state_dxccs:add(dxcc)
+		county_dxccs:add(dxcc)
+		states:add(s)
+
+		assert(name2abbrev[n] == nil)
+		name2abbrev[n] = a
+
+		assert(abbrev2name[a] == nil)
+		abbrev2name[a] = info
+	end
+
+	-- NOTE: dxcc-state is a superset of dxcc-county
+	out['dxcc-county'] = county_dxccs -- set of dxccs that may use counties
+	out['dxcc-state'] = state_dxccs -- set of dxccs that may use states
+	out['states-county'] = states -- set of states that use counties
+	out['county'] = counties -- the raw data from the user
+	out['n2a'] = name2abbrev -- map from <state,name> to abbrev
+	out['a2n'] = abbrev2name -- map from abbrev to info
+	out['dups'] = hlog.set.new() -- set of dup hashes
+
+	-- set up an object with the data
+	local obj = {
+		duphash = duphash,
+		data = out,
+	}
+
+	setmetatable(obj, mt)
+
+	return obj
+end
+
+local function name2abbrev(self, dxcc, state, county)
+	local map = self.data
+
+	-- outside of DXCCs that use states or counties
+	if not map['dxcc-state']:has(dxcc) and not map['dxcc-county']:has(dxcc) then
+		return "DX"
+	end
+
+	-- inside the 'states' but outside the 'counties' area
+	if not map['states-county']:has(state) then
+		return state
+	end
+
+	-- inside the 'counties' area
+	assert(county ~= nil)
+
+	return map['n2a'][state .. county]
+end
+
+local function abbrev2name(self, abbrev)
+	local map = self.data
+
+	local info = map['a2n'][abbrev]
+
+	-- inside 'counties' area: return state & county
+	if info ~= nil then
+		return info[2], info[4]
+	end
+
+	-- outside 'counties' area, but inside 'states' area: return state
+	if abbrev ~= nil and abbrev ~= "DX" then
+		return abbrev, nil
+	end
+
+	-- outside 'counties' and 'states' area
+	return nil, nil
+end
+
+local function dxcc_use_state(self, dxcc)
+	if dxcc == nil then
+		return false
+	end
+
+	return self.data['dxcc-state']:has(dxcc)
+end
+
+local function dxcc_use_county(self, dxcc)
+	if dxcc == nil then
+		return false
+	end
+
+	return self.data['dxcc-county']:has(dxcc)
+end
+
+local function state_use_county(self, dxcc, state)
+	local map = self.data
+
+	if dxcc == nil or state == nil then
+		return false
+	end
+
+	return map['dxcc-county']:has(dxcc) and map['states-county']:has(state)
+end
+
+local function qso_done(self, qso, tx, rx)
+	local map = self.data
+
+	tx = tx ~= nil and tx or qso.additional['contest-tx']
+	rx = rx ~= nil and rx or qso.additional['contest-rx']
+
+	map['dups']:add(self.duphash(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.additional['contest-tx']
+	rx = rx ~= nil and rx or qso.additional['contest-rx']
+
+	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.additional['contest-tx'],
+					  qso.additional['contest-rx'],
+					  "+", 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:to_string()))
+	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 "County{" .. self.data:tostring() .. "}"
+end
+
+mt.__index = {
+	name2abbrev = name2abbrev,
+	abbrev2name = abbrev2name,
+	dxcc_use_state = dxcc_use_state,
+	dxcc_use_county = dxcc_use_county,
+	state_use_county = state_use_county,
+	qso_done = qso_done,
+	is_dup = is_dup,
+	export_prep = export_prep,
+	tostring = tostring,
+}
+
+return {
+	county = county,
+}

          
M xlua/xlua.c +10 -0
@@ 64,6 64,12 @@ static const luaL_Reg xlua_funcs[] = {
 	{ NULL, NULL },
 };
 
+static void xlua_pushlib_contest_helper(lua_State *L)
+{
+	lua_newtable(L);
+	xlua_merge_lua_funcs(L, "helper-county.lua");
+}
+
 static void xlua_register(lua_State *L)
 {
 	lua_newtable(L);

          
@@ 76,6 82,10 @@ static void xlua_register(lua_State *L)
 	xlua_pushlib_cabrillo(L);
 	lua_settable(L, -3);
 
+	lua_pushliteral(L, "contest_helper");
+	xlua_pushlib_contest_helper(L);
+	lua_settable(L, -3);
+
 	lua_pushliteral(L, "country");
 	xlua_pushlib_country(L);
 	lua_settable(L, -3);