WIP: xlua: switch NEQP script to county-based helper
1 files changed, 179 insertions(+), 125 deletions(-)

M xlua/scripts/contests/NEQP.lua
M xlua/scripts/contests/NEQP.lua +179 -125
@@ 28,127 28,66 @@ local allowed_bands = {
 	[10000] = true,
 }
 
--- DXCC entities that use states (+counties) instead of "DX"
-local uscan_dxcc = {
-	[1] = true, -- Canada
-	[6] = true, -- Alaska
-	[110] = true, -- Hawaii
-	[291] = true, -- CONUS
+local mode_points = {
+	["PH"] = 1,
+	["CW"] = 2,
+	["DG"] = 2,
 }
 
 local raw_county_data = {
 	-- NB: these are the CT multiplies starting 2024; previous years
 	-- used counties instead of Regional Councils of Government
-	{ 'CT', 'GBR', 'Greater Bridgeport' },	{ 'CT', 'LCR', 'Lower CT River' },
-	{ 'CT', 'NAU', 'Naugatuck Valley' },	{ 'CT', 'NOE', 'Northeastern' },
-	{ 'CT', 'NOW', 'Northwest Hills' },	{ 'CT', 'SOE', 'Southeastern' },
-	{ 'CT', 'SOC', 'South Central' },	{ 'CT', 'WES', 'Western' },
+	{ 291, 'CT', 'CTGBR', 'Greater Bridgeport' },	{ 291, 'CT', 'CTLCR', 'Lower CT River' },
+	{ 291, 'CT', 'CTNAU', 'Naugatuck Valley' },	{ 291, 'CT', 'CTNOE', 'Northeastern' },
+	{ 291, 'CT', 'CTNOW', 'Northwest Hills' },	{ 291, 'CT', 'CTSOE', 'Southeastern' },
+	{ 291, 'CT', 'CTSOC', 'South Central' },	{ 291, 'CT', 'CTWES', 'Western' },
 
-	{ 'MA', 'BAR', 'Barnstable' },		{ 'MA', 'BER', 'Berkshire' },
- 	{ 'MA', 'BRI', 'Bristol' },		{ 'MA', 'DUK', 'Dukes' },
- 	{ 'MA', 'ESS', 'Essex' },		{ 'MA', 'FRA', 'Franklin' },
- 	{ 'MA', 'HMD', 'Hampden' },		{ 'MA', 'HMP', 'Hampshire' },
- 	{ 'MA', 'MID', 'Middlesex' },		{ 'MA', 'NAN', 'Nantucket' },
- 	{ 'MA', 'NOR', 'Norfolk' },		{ 'MA', 'PLY', 'Plymouth' },
- 	{ 'MA', 'SUF', 'Suffolk' },		{ 'MA', 'WOR', 'Worcester' },
+	{ 291, 'MA', 'MABAR', 'Barnstable' },		{ 291, 'MA', 'MABER', 'Berkshire' },
+ 	{ 291, 'MA', 'MABRI', 'Bristol' },		{ 291, 'MA', 'MADUK', 'Dukes' },
+ 	{ 291, 'MA', 'MAESS', 'Essex' },		{ 291, 'MA', 'MAFRA', 'Franklin' },
+ 	{ 291, 'MA', 'MAHMD', 'Hampden' },		{ 291, 'MA', 'MAHMP', 'Hampshire' },
+ 	{ 291, 'MA', 'MAMID', 'Middlesex' },		{ 291, 'MA', 'MANAN', 'Nantucket' },
+ 	{ 291, 'MA', 'MANOR', 'Norfolk' },		{ 291, 'MA', 'MAPLY', 'Plymouth' },
+ 	{ 291, 'MA', 'MASUF', 'Suffolk' },		{ 291, 'MA', 'MAWOR', 'Worcester' },
 
-	{ 'ME', 'AND', 'Androscoggin' },	{ 'ME', 'ARO', 'Aroostook' },
- 	{ 'ME', 'CUM', 'Cumberland' },		{ 'ME', 'FRA', 'Franklin' },
- 	{ 'ME', 'HAN', 'Hancock' },		{ 'ME', 'KEN', 'Kennebec' },
- 	{ 'ME', 'KNO', 'Knox' },		{ 'ME', 'LIN', 'Lincoln' },
- 	{ 'ME', 'OXF', 'Oxford' },		{ 'ME', 'PEN', 'Penobscot' },
- 	{ 'ME', 'PIS', 'Piscataquis' },		{ 'ME', 'SAG', 'Sagadahoc' },
- 	{ 'ME', 'SOM', 'Somerset' },		{ 'ME', 'WAL', 'Waldo' },
- 	{ 'ME', 'WAS', 'Washington' },		{ 'ME', 'YOR', 'York' },
+	{ 291, 'ME', 'MEAND', 'Androscoggin' },		{ 291, 'ME', 'MEARO', 'Aroostook' },
+ 	{ 291, 'ME', 'MECUM', 'Cumberland' },		{ 291, 'ME', 'MEFRA', 'Franklin' },
+ 	{ 291, 'ME', 'MEHAN', 'Hancock' },		{ 291, 'ME', 'MEKEN', 'Kennebec' },
+ 	{ 291, 'ME', 'MEKNO', 'Knox' },			{ 291, 'ME', 'MELIN', 'Lincoln' },
+ 	{ 291, 'ME', 'MEOXF', 'Oxford' },		{ 291, 'ME', 'MEPEN', 'Penobscot' },
+ 	{ 291, 'ME', 'MEPIS', 'Piscataquis' },		{ 291, 'ME', 'MESAG', 'Sagadahoc' },
+ 	{ 291, 'ME', 'MESOM', 'Somerset' },		{ 291, 'ME', 'MEWAL', 'Waldo' },
+ 	{ 291, 'ME', 'MEWAS', 'Washington' },		{ 291, 'ME', 'MEYOR', 'York' },
 
-	{ 'NH', 'BEL', 'Belknap' },		{ 'NH', 'CAR', 'Carroll' },
- 	{ 'NH', 'CHE', 'Cheshire' },		{ 'NH', 'COO', 'Coos' },
- 	{ 'NH', 'GRA', 'Grafton' },		{ 'NH', 'HIL', 'Hillsborough' },
- 	{ 'NH', 'MER', 'Merrimack' },		{ 'NH', 'ROC', 'Rockingham' },
- 	{ 'NH', 'STR', 'Strafford' },		{ 'NH', 'SUL', 'Sullivan' },
+	{ 291, 'NH', 'NHBEL', 'Belknap' },		{ 291, 'NH', 'NHCAR', 'Carroll' },
+ 	{ 291, 'NH', 'NHCHE', 'Cheshire' },		{ 291, 'NH', 'NHCOO', 'Coos' },
+ 	{ 291, 'NH', 'NHGRA', 'Grafton' },		{ 291, 'NH', 'NHHIL', 'Hillsborough' },
+ 	{ 291, 'NH', 'NHMER', 'Merrimack' },		{ 291, 'NH', 'NHROC', 'Rockingham' },
+ 	{ 291, 'NH', 'NHSTR', 'Strafford' },		{ 291, 'NH', 'NHSUL', 'Sullivan' },
 
-	{ 'RI', 'BRI', 'Bristol' },		{ 'RI', 'KEN', 'Kent' },
- 	{ 'RI', 'NEW', 'Newport' },		{ 'RI', 'PRO', 'Providence' },
- 	{ 'RI', 'WAS', 'Washington' },
+	{ 291, 'RI', 'RIBRI', 'Bristol' },		{ 291, 'RI', 'RIKEN', 'Kent' },
+ 	{ 291, 'RI', 'RINEW', 'Newport' },		{ 291, 'RI', 'RIPRO', 'Providence' },
+ 	{ 291, 'RI', 'RIWAS', 'Washington' },
 
-	{ 'VT', 'ADD', 'Addison' },		{ 'VT', 'BEN', 'Bennington' },
- 	{ 'VT', 'CAL', 'Caledonia' },		{ 'VT', 'CHI', 'Chittenden' },
- 	{ 'VT', 'ESS', 'Essex' },		{ 'VT', 'FRA', 'Franklin' },
- 	{ 'VT', 'GRA', 'Grand Isle' },		{ 'VT', 'LAM', 'Lamoille' },
- 	{ 'VT', 'ORA', 'Orange' },		{ 'VT', 'ORL', 'Orleans' },
- 	{ 'VT', 'RUT', 'Rutland' },		{ 'VT', 'WAS', 'Washington' },
- 	{ 'VT', 'WNH', 'Windham' },		{ 'VT', 'WND', 'Windsor' },
+	{ 291, 'VT', 'VTADD', 'Addison' },		{ 291, 'VT', 'VTBEN', 'Bennington' },
+ 	{ 291, 'VT', 'VTCAL', 'Caledonia' },		{ 291, 'VT', 'VTCHI', 'Chittenden' },
+ 	{ 291, 'VT', 'VTESS', 'Essex' },		{ 291, 'VT', 'VTFRA', 'Franklin' },
+ 	{ 291, 'VT', 'VTGRA', 'Grand Isle' },		{ 291, 'VT', 'VTLAM', 'Lamoille' },
+ 	{ 291, 'VT', 'VTORA', 'Orange' },		{ 291, 'VT', 'VTORL', 'Orleans' },
+ 	{ 291, 'VT', 'VTRUT', 'Rutland' },		{ 291, 'VT', 'VTWAS', 'Washington' },
+ 	{ 291, 'VT', 'VTWNH', 'Windham' },		{ 291, 'VT', 'VTWND', 'Windsor' },
 }
 
--- generate views of the county data
-local map_county_name2abbrev = {}
-local map_county_abbrev2name = {}
-local new_england_states = {}
-
--- initialize county_{name2abbrev,abbrev2name}
-for _, info in pairs(raw_county_data) do
-	local a2023 = info[2] .. info[1]
-	local a = info[1] .. info[2]
-	local n = info[1] .. info[3]
-	local s = info[1]
-
-	assert(map_county_name2abbrev[n] == nil)
-	map_county_name2abbrev[n] = a
-
-	-- old (2023) -style abbrevs for easier entry
-	assert(map_county_abbrev2name[a2023] == nil)
-	map_county_abbrev2name[a2023] = info
-	assert(map_county_abbrev2name[a] == nil)
-	map_county_abbrev2name[a] = info
-
-	new_england_states[s] = true
-end
-
--- dup checking
-local dups = {}
-
-local function mkhash(qso)
+local function duphash(qso, tx, rx)
 	return string.format("%s-%u-%s-%s-%s",
 			     qso.rx.station_call,
 			     qso.tx.band,
 			     qso.tx.mode,
-			     qso.additional['contest-tx'],
-			     qso.additional['contest-rx'])
+			     tx,
+			     rx)
 end
 
-local function county_name2abbrev(dxcc, state, county)
-	-- outside of US or Canada: DX
-	if uscan_dxcc[dxcc] == nil then
-		return "DX"
-	end
-
-	-- outside of New England: state/province
-	if new_england_states[state] == nil then
-		return state
-	end
-
-	-- inside New England: <state><county> abbreviation
-	assert(county ~= nil)
-
-	return map_county_name2abbrev[state .. county]
-end
-
-local function county_abbrev2name(abbrev)
-	local info = map_county_abbrev2name[abbrev]
-
-	-- inside New England: return state & county
-	if info ~= nil then
-		return info[1], info[3]
-	end
-
-	-- outside of New England: state
-	if abbrev ~= nil and abbrev ~= "DX" then
-		return abbrev, nil
-	end
-
-	-- outside of US
-	return nil, nil
-end
+local neqp = hlog.contest_helper.county(raw_county_data, { 1, 291 }, duphash)
 
 local function startup_fill_template(template)
 	local done = false

          
@@ 159,19 98,30 @@ local function startup_fill_template(tem
 
 		template.tx.dxcc = io.stdin:read()
 
-		if uscan_dxcc[template.tx.dxcc] then
+		if neqp:dxcc_use_state(template.tx.dxcc) then
 			print("Enter state or province (e.g., MA):")
 
-			local tmp = io.stdin:read()
-			local state, county = county_abbrev2name(tmp)
+			local tmp = io.stdin:read():upper()
+			local state, county = neqp:abbrev2name(tmp)
 
-			if county ~= nil then
+			if tmp == "" or tmp == nil then
+				-- try again
+			elseif county ~= nil then
 				template.tx.state = state
 				template.tx.county = county
 				done = true
-			elseif new_england_states[tmp] then
+			elseif neqp:state_use_county(template.tx.dxcc, tmp) then
 				print("Enter county abbreviation (e.g., MID):")
-				state, county = county_abbrev2name(tmp .. io.stdin:read())
+
+				local tmp2 = io.stdin:read():upper()
+
+				state, county = neqp:abbrev2name(tmp .. tmp2)
+				if county == nil then
+					-- fall back to just trying the last
+					-- input in case the user entered the
+					-- whole abbreviation (e.g., MAMID)
+					state, county = neqp:abbrev2name(tmp2)
+				end
 
 				if county ~= nil then
 					template.tx.state = state

          
@@ 190,12 140,6 @@ local function startup_fill_template(tem
 	-- TODO: fill in qso.tx's {itu,cqz,country,continent}
 end
 
-local function qso_done(qso)
-	if allowed_bands[qso.tx.band] then
-		dups[mkhash(qso)] = true
-	end
-end
-
 -- copy country data into QSO
 local function sync_zones(qso)
 	local call = qso.rx.station_call

          
@@ 235,7 179,7 @@ local function field_changed(qso, fields
 		qso.additional['contest-rx'] = value
 
 		-- set state & county based on the exchange
-		local state, county = county_abbrev2name(value)
+		local state, county = neqp:abbrev2name(value)
 		qso.rx.state = state
 		qso.rx.county = county
 

          
@@ 248,11 192,115 @@ local function field_changed(qso, fields
 	end
 
 	return nil, {
-		["DUP"] = dups[mkhash(qso)] and "red" or "black",
+		["DUP"] = neqp:is_dup(qso) and "red" or "black",
 		["BAND"] = allowed_bands[qso.tx.band] and "black" or "red",
 	}
 end
 
+local function qexchange(side, ex)
+	local ok = true
+	local out = ""
+
+	if side.rst == nil then
+		ok = false
+		out = out .. "59"
+	else
+		out = out .. side.rst
+	end
+
+	if ex == nil then
+		ok = false
+		out = out .. " ??"
+	else
+		out = out .. " " .. ex
+	end
+
+	return out, ok
+end
+
+local function qpoints(qso, tx, rx)
+	assert(qso.tx.mode ~= nil)
+
+	return mode_points[qso.tx.cabrillo_mode]
+end
+
+local function qmult(qso, tx, rx)
+	local function in_ne(side)
+		if not neqp:dxcc_use_county(side.dxcc) and
+		   not neqp:dxcc_use_state(side.dxcc) then
+			return false -- DX
+		end
+
+		if not neqp:state_use_county(side.dxcc, side.state) then
+			return false -- state/province outside of New England
+		end
+
+		return true -- inside New England
+	end
+
+	if in_ne(qso.tx) then
+		-- We are in New England; we get to work anyone anywhere.
+	elseif in_ne(qso.rx) then
+		-- We are outside of New England and other station is in New
+		-- England, we get to work them.
+	else
+		-- We are outside of New England and the other station is
+		-- also outside of New England.
+		return nil
+	end
+
+	-- At this point, we know that this contact involves at least one
+	-- New England station.
+
+	if neqp:state_use_county(qso.rx.dxcc, qso.rx.state) then
+		-- Other station should have given us state & county.
+		local state, county = neqp:abbrev2name(rx)
+
+		if county == nil then
+			return nil -- no/bad state & county code given
+		end
+
+		return rx -- state & county code is valid
+	elseif neqp:dxcc_use_state(qso.rx.dxcc) then
+		-- Other station should have given us a state/province.
+		return rx -- TODO: sanity check state/province
+	else
+		-- Other station should have given us "DX".
+		if rx ~= "DX" then
+			return nil -- no/bad exchange
+		end
+
+		return qso.rx.dxcc -- use DXCC as multiplier
+	end
+end
+
+local function export(outfname)
+	local all
+	local station
+	local ops
+	local points
+	local mults
+
+	all, station, ops, points, mults = neqp:export_prep(contest.id, contest.year,
+							    qpoints, qmult, qexchange)
+
+	hlog.cabrillo.print_header(contest.id,
+				   station,
+				   ops:as_array(),
+				   points * mults:count_items())
+
+	for _, rec in ipairs(all) do
+		local qso = rec[1]
+		local tx = rec[2]
+		local rx = rec[3]
+		local xqso = not rec[4]
+
+		hlog.cabrillo.print_qso(qso, tx, rx, xqso)
+	end
+
+	hlog.cabrillo.print_footer()
+end
+
 return {
 	labels = {
 		{ 0,  0, "St. Call" },

          
@@ 276,6 324,8 @@ return {
 	},
 
 	events = {
+		export = export,
+
 		-- called once on startup (after config's fill_template)
 		startup = function(template)
 			print("NEQP contest script")

          
@@ 286,7 336,7 @@ return {
 				if qso.additional['contest-id'] == "NEQP" and
 				   qso.additional['contest-year'] == contest.year and
 				   allowed_bands[qso.tx.band] then
-					dups[mkhash(qso)] = true
+					neqp:qso_done(qso)
 				end
 			end
 		end,

          
@@ 299,23 349,27 @@ return {
 			local dxcc = qso.tx.dxcc
 			local state = qso.tx.state
 			local county = qso.tx.county
-			local cs = county_name2abbrev(dxcc, state, county)
+			local exch = neqp:name2abbrev(dxcc, state, county)
 
 			-- qso.tx.rst = "59" -- we always send 59
 			-- qso.rx.rst = "59" -- we expect a 59
-			qso.additional['contest-id'] = 'NEQP'
-			qso.additional['contest-year'] = contest.year
 
-			if cs == nil then
+			if exch == nil then
 				contest.lcd("?")
 			else
-				qso.additional['contest-tx'] = cs
-				contest.lcd(cs)
+				qso.additional['contest-id'] = 'NEQP'
+				qso.additional['contest-year'] = contest.year
+				qso.additional['contest-tx'] = exch
+				contest.lcd(exch)
 			end
 		end,
 
 		-- called when QSO is complete & saved, just prior to a reset
-		qso_done = qso_done,
+		qso_done = function(qso)
+			if allowed_bands[qso.tx.band] then
+				neqp:qso_done(qso)
+			end
+		end,
 
 		-- called whenever a contest field changes
 		field_changed = field_changed,