Add complete specification document for the JSON-driven award calculation system. Documents all rule types, filter operators, QSO schema, and implementation guidance suitable for porting to any programming language. Co-Authored-By: Claude <noreply@anthropic.com>
1052 lines
30 KiB
Markdown
1052 lines
30 KiB
Markdown
# Award System Specification
|
||
|
||
## Overview
|
||
|
||
This specification describes a JSON-driven award calculation system for amateur radio QSO (contact) tracking applications. The system calculates progress toward awards based on QSO data stored in a database.
|
||
|
||
The award definitions are externalized as JSON files, allowing awards to be added, modified, or extended without changing application code.
|
||
|
||
---
|
||
|
||
## Architecture Overview
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ AWARD SYSTEM │
|
||
├─────────────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
|
||
│ │ Award Definitions│ <20>───> │ Rule Processor │ ──> │ QSO Database │ │
|
||
│ │ (JSON files) │ │ (Engine Core) │ │ │ │
|
||
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
|
||
│ │ │ │
|
||
│ │ │ │
|
||
│ v v │
|
||
│ ┌──────────────────┐ ┌──────────────┐ │
|
||
│ │ Award Metadata │ │ QSO Records │ │
|
||
│ │ - id, name │ │ - callsign │ │
|
||
│ │ - description │ │ - qsoDate │ │
|
||
│ │ - caption │ │ - band │ │
|
||
│ │ - category │ │ - mode │ │
|
||
│ │ - rules │ │ - entityId │ │
|
||
│ └──────────────────┘ │ - state │ │
|
||
│ │ - grid │ │
|
||
│ │ - darcDok │ │
|
||
│ │ - satName │ │
|
||
│ │ - lotw... │ │
|
||
│ │ - dclQsl... │ │
|
||
│ └──────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Award Definition JSON Schema
|
||
|
||
### Root Object Structure
|
||
|
||
Every award definition is a JSON object with the following structure:
|
||
|
||
```json
|
||
{
|
||
"id": "string (required, unique identifier)",
|
||
"name": "string (required, display name)",
|
||
"description": "string (required, short description)",
|
||
"caption": "string (required, detailed explanation)",
|
||
"category": "string (required, grouping category)",
|
||
"rules": {
|
||
"type": "string (required, rule type)",
|
||
"...": "rule-specific properties"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Root Properties
|
||
|
||
| Property | Type | Required | Description |
|
||
|----------|------|----------|-------------|
|
||
| `id` | string | Yes | Unique identifier for the award (e.g., "dxcc-mixed", "dld-80m") |
|
||
| `name` | string | Yes | Display name shown in UI (e.g., "DXCC Mixed Mode", "DLD 80m") |
|
||
| `description` | string | Yes | Short description for list views |
|
||
| `caption` | string | Yes | Detailed explanation of award requirements |
|
||
| `category` | string | Yes | Grouping category for UI organization (e.g., "dxcc", "darc", "vucc") |
|
||
| `rules` | object | Yes | Award calculation rules (see Rule Types below) |
|
||
|
||
---
|
||
|
||
## Rule Types
|
||
|
||
The system supports **5 rule types**. Each type defines a different calculation method.
|
||
|
||
### 1. Entity Rule Type (`"type": "entity"`)
|
||
|
||
Counts unique entities (DXCC countries, states, grid squares, callsigns) from QSOs.
|
||
|
||
#### Properties
|
||
|
||
| Property | Type | Required | Description |
|
||
|----------|------|----------|-------------|
|
||
| `type` | string | Yes | Must be `"entity"` |
|
||
| `entityType` | string | Yes | What to count: `"dxcc"`, `"state"`, `"grid"`, `"callsign"` |
|
||
| `target` | number | Yes | Number required to complete award |
|
||
| `displayField` | string | No | Field to display as entity name (defaults to entity value) |
|
||
| `filters` | object | No | Filter criteria (see Filters section) |
|
||
|
||
#### Entity Type Mappings
|
||
|
||
| `entityType` Value | Source Field | Notes |
|
||
|-------------------|--------------|-------|
|
||
| `"dxcc"` | `entityId` | Numeric DXCC entity ID |
|
||
| `"state"` | `state` | US state or other regional code |
|
||
| `"grid"` | `grid` | First 4 characters only (e.g., "FN31") |
|
||
| `"callsign"` | `callsign` | Station callsign |
|
||
|
||
#### Calculation Logic
|
||
|
||
```
|
||
worked = count of unique entities where QSO exists
|
||
confirmed = count of unique entities where lotwQslRstatus === 'Y'
|
||
```
|
||
|
||
#### Example: DXCC Mixed Mode
|
||
|
||
```json
|
||
{
|
||
"id": "dxcc-mixed",
|
||
"name": "DXCC Mixed Mode",
|
||
"description": "Confirm 100 DXCC entities on any band/mode",
|
||
"caption": "Contact and confirm 100 different DXCC entities. Any band and mode combination counts.",
|
||
"category": "dxcc",
|
||
"rules": {
|
||
"type": "entity",
|
||
"entityType": "dxcc",
|
||
"target": 100,
|
||
"displayField": "entity"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Example: VUCC Satellite (with filter)
|
||
|
||
```json
|
||
{
|
||
"id": "vucc-satellite",
|
||
"name": "VUCC Satellite",
|
||
"description": "Confirm 100 unique grid squares via satellite",
|
||
"caption": "Contact and confirm 100 unique 4-character grid squares via satellite.",
|
||
"category": "vucc",
|
||
"rules": {
|
||
"type": "entity",
|
||
"entityType": "grid",
|
||
"target": 100,
|
||
"displayField": "grid",
|
||
"filters": {
|
||
"operator": "AND",
|
||
"filters": [
|
||
{ "field": "satellite", "operator": "eq", "value": true }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. DOK Rule Type (`"type": "dok"`)
|
||
|
||
Counts unique DOK (DARC Ortsverband Kennung) combinations for German amateur radio awards.
|
||
|
||
#### Properties
|
||
|
||
| Property | Type | Required | Description |
|
||
|----------|------|----------|-------------|
|
||
| `type` | string | Yes | Must be `"dok"` |
|
||
| `target` | number | Yes | Number required to complete award |
|
||
| `confirmationType` | string | Yes | Must be `"dcl"` (DARC Community Logbook) |
|
||
| `displayField` | string | No | Field to display (typically `"darcDok"`) |
|
||
| `filters` | object | No | Filter criteria (band, mode, etc.) |
|
||
|
||
#### Calculation Logic
|
||
|
||
```
|
||
For each QSO with darcDok:
|
||
combination = darcDok + "/" + band + "/" + mode
|
||
Track unique combinations
|
||
|
||
worked = count of unique (DOK, band, mode) combinations
|
||
confirmed = count of combinations where dclQslRstatus === 'Y'
|
||
```
|
||
|
||
**Key Behavior**: Each unique DOK on each unique band/mode counts separately.
|
||
|
||
#### Example: DLD (Deutschland Diplom)
|
||
|
||
```json
|
||
{
|
||
"id": "dld",
|
||
"name": "DLD",
|
||
"description": "Deutschland Diplom - Confirm 100 unique DOKs on different bands/modes",
|
||
"caption": "Contact and confirm stations with 100 unique DOKs on different band/mode combinations.",
|
||
"category": "darc",
|
||
"rules": {
|
||
"type": "dok",
|
||
"target": 100,
|
||
"confirmationType": "dcl",
|
||
"displayField": "darcDok"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Example: DLD 80m (with filter)
|
||
|
||
```json
|
||
{
|
||
"id": "dld-80m",
|
||
"name": "DLD 80m",
|
||
"description": "Confirm 100 unique DOKs on 80m",
|
||
"caption": "Contact and confirm 100 unique DOKs on the 80m band.",
|
||
"category": "darc",
|
||
"rules": {
|
||
"type": "dok",
|
||
"target": 100,
|
||
"confirmationType": "dcl",
|
||
"displayField": "darcDok",
|
||
"filters": {
|
||
"operator": "AND",
|
||
"filters": [
|
||
{ "field": "band", "operator": "eq", "value": "80m" }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. Points Rule Type (`"type": "points"`)
|
||
|
||
Awards points for contacting specific stations, with three counting modes.
|
||
|
||
#### Properties
|
||
|
||
| Property | Type | Required | Description |
|
||
|----------|------|----------|-------------|
|
||
| `type` | string | Yes | Must be `"points"` |
|
||
| `target` | number | Yes | Points required to complete award |
|
||
| `countMode` | string | No | `"perStation"`, `"perBandMode"`, or `"perQso"` (default: `"perStation"`) |
|
||
| `stations` | array | Yes | Array of station definitions |
|
||
|
||
#### Station Array Format
|
||
|
||
```json
|
||
"stations": [
|
||
{ "callsign": "STATION1", "points": 10 },
|
||
{ "callsign": "STATION2", "points": 5 }
|
||
]
|
||
```
|
||
|
||
#### Count Modes
|
||
|
||
| Mode | Description |
|
||
|------|-------------|
|
||
| `"perStation"` | Each unique station earns points once (if confirmed) |
|
||
| `"perBandMode"` | Each unique (callsign, band, mode) combination earns points |
|
||
| `"perQso"` | Every confirmed QSO earns points |
|
||
|
||
#### Calculation Logic
|
||
|
||
```
|
||
For each QSO with callsign matching a station:
|
||
points = station.points for that callsign
|
||
|
||
if countMode === "perStation":
|
||
Count unique callsigns (if confirmed)
|
||
else if countMode === "perBandMode":
|
||
Count unique (callsign, band, mode) combinations (if confirmed)
|
||
else if countMode === "perQso":
|
||
Count every confirmed QSO
|
||
|
||
totalPoints = sum(points for confirmed entries)
|
||
```
|
||
|
||
#### Example: Wavelog Award (perBandMode)
|
||
|
||
```json
|
||
{
|
||
"id": "wavelog-award",
|
||
"name": "Wavelog Award",
|
||
"description": "Contact special stations on multiple bands and modes to earn points",
|
||
"caption": "Contact special stations to earn points. Points awarded for each unique band/mode combination.",
|
||
"category": "special",
|
||
"rules": {
|
||
"type": "points",
|
||
"target": 50,
|
||
"countMode": "perBandMode",
|
||
"stations": [
|
||
{ "callsign": "DF2ET", "points": 10 },
|
||
{ "callsign": "DJ7NT", "points": 10 },
|
||
{ "callsign": "HB9HIL", "points": 10 },
|
||
{ "callsign": "DB4SCW", "points": 5 }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4. Filtered Rule Type (`"type": "filtered"`)
|
||
|
||
Creates a filtered variant of another award by applying additional criteria.
|
||
|
||
#### Properties
|
||
|
||
| Property | Type | Required | Description |
|
||
|----------|------|----------|-------------|
|
||
| `type` | string | Yes | Must be `"filtered"` |
|
||
| `baseRule` | object | Yes | The base entity rule to filter |
|
||
| `filters` | object | Yes | Additional filters to apply |
|
||
|
||
#### Base Rule Format
|
||
|
||
```json
|
||
"baseRule": {
|
||
"type": "entity",
|
||
"entityType": "dxcc|state|grid|callsign",
|
||
"target": number,
|
||
"displayField": "string"
|
||
}
|
||
```
|
||
|
||
#### Example: DXCC CW
|
||
|
||
```json
|
||
{
|
||
"id": "dxcc-cw",
|
||
"name": "DXCC CW",
|
||
"description": "Confirm 100 DXCC entities using CW mode",
|
||
"caption": "Contact and confirm 100 different DXCC entities using CW mode only.",
|
||
"category": "dxcc",
|
||
"rules": {
|
||
"type": "filtered",
|
||
"baseRule": {
|
||
"type": "entity",
|
||
"entityType": "dxcc",
|
||
"target": 100,
|
||
"displayField": "entity"
|
||
},
|
||
"filters": {
|
||
"operator": "AND",
|
||
"filters": [
|
||
{ "field": "mode", "operator": "eq", "value": "CW" }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Note**: At runtime, this is internally converted to an `entity` rule with the filters applied.
|
||
|
||
---
|
||
|
||
### 5. Counter Rule Type (`"type": "counter"`)
|
||
|
||
Counts QSOs or unique callsigns matching filter criteria.
|
||
|
||
#### Properties
|
||
|
||
| Property | Type | Required | Description |
|
||
|----------|------|----------|-------------|
|
||
| `type` | string | Yes | Must be `"counter"` |
|
||
| `target` | number | Yes | Number required to complete award |
|
||
| `countBy` | string | Yes | `"qso"` (QSOs) or `"callsign"` (unique callsigns) |
|
||
| `displayField` | string | No | Field to display |
|
||
| `filters` | object | No | Filter criteria |
|
||
|
||
#### Example: RS-44 Satellite
|
||
|
||
```json
|
||
{
|
||
"id": "sat-rs44",
|
||
"name": "RS-44 Satellite",
|
||
"description": "Work 44 QSOs on satellite RS-44",
|
||
"caption": "Make 44 unique QSOs via the RS-44 satellite. Each QSO with a different callsign counts.",
|
||
"category": "custom",
|
||
"rules": {
|
||
"type": "counter",
|
||
"target": 44,
|
||
"countBy": "qso",
|
||
"displayField": "callsign",
|
||
"filters": {
|
||
"operator": "AND",
|
||
"filters": [
|
||
{ "field": "satName", "operator": "eq", "value": "RS-44" }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Note**: At runtime, counter rules are internally converted to `entity` rules with `entityType: "callsign"`.
|
||
|
||
---
|
||
|
||
## Filters
|
||
|
||
Filters allow awards to apply criteria based on QSO fields.
|
||
|
||
### Filter Structure
|
||
|
||
```json
|
||
"filters": {
|
||
"operator": "AND|OR",
|
||
"filters": [
|
||
{ "field": "fieldName", "operator": "operator", "value": "value" },
|
||
...
|
||
]
|
||
}
|
||
```
|
||
|
||
### Filter Operators
|
||
|
||
| Operator | Description | Example |
|
||
|----------|-------------|---------|
|
||
| `"eq"` | Equals | `{ "field": "band", "operator": "eq", "value": "20m" }` |
|
||
| `"ne"` | Not equals | `{ "field": "mode", "operator": "ne", "value": "FM" }` |
|
||
| `"in"` | In array | `{ "field": "band", "operator": "in", "value": ["20m", "40m"] }` |
|
||
| `"nin"` | Not in array | `{ "field": "mode", "operator": "nin", "value": ["FM", "AM"] }` |
|
||
| `"contains"` | Contains substring | `{ "field": "callsign", "operator": "contains", "value": "DL" }` |
|
||
|
||
### Available Filter Fields
|
||
|
||
Any QSO database field can be filtered:
|
||
|
||
| Field | Type | Example |
|
||
|-------|------|---------|
|
||
| `band` | string | `"80m"`, `"20m"`, `"2m"` |
|
||
| `mode` | string | `"CW"`, `"SSB"`, `"FT8"` |
|
||
| `callsign` | string | `"DJ7NT"` |
|
||
| `entity` | string | `"Germany"` |
|
||
| `entityId` | number | `230` |
|
||
| `state` | string | `"CA"`, `"NY"` |
|
||
| `grid` | string | `"FN31pr"` |
|
||
| `satName` | string | `"AO-73"`, `"RS-44"` |
|
||
| `satellite` | boolean | `true` (has satellite name) |
|
||
|
||
### Special Field: `satellite`
|
||
|
||
The `satellite` field is a computed field:
|
||
- Returns `true` if QSO has a non-empty `satName`
|
||
- Returns `false` otherwise
|
||
|
||
### Compound Filters
|
||
|
||
Multiple filters can be combined with `AND` or `OR`:
|
||
|
||
```json
|
||
"filters": {
|
||
"operator": "AND",
|
||
"filters": [
|
||
{ "field": "band", "operator": "eq", "value": "80m" },
|
||
{ "field": "mode", "operator": "eq", "value": "CW" }
|
||
]
|
||
}
|
||
```
|
||
|
||
This creates: DLD 80m CW (only QSOs on 80m band AND CW mode count).
|
||
|
||
---
|
||
|
||
## QSO Database Schema
|
||
|
||
The award system relies on QSO records with specific fields.
|
||
|
||
### Required QSO Fields
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `userId` | number | User/owner identifier |
|
||
| `id` | number | Unique QSO identifier |
|
||
| `callsign` | string | Station callsign contacted |
|
||
| `qsoDate` | string | QSO date (YYYY-MM-DD) |
|
||
| `timeOn` | string | QSO time (HH:MM) |
|
||
| `band` | string | Amateur band (e.g., "20m", "80m") |
|
||
| `mode` | string | Mode (e.g., "CW", "SSB", "FT8") |
|
||
|
||
### DXCC Fields
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `entityId` | number | DXCC entity ID (numeric) |
|
||
| `entity` | string | DXCC entity name (e.g., "Germany") |
|
||
|
||
### US State Fields
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `state` | string | US state code (e.g., "CA", "NY") |
|
||
|
||
### Grid Square Fields
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `grid` | string | Maidenhead grid square (e.g., "FN31pr") |
|
||
|
||
### DOK Fields (German Awards)
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `darcDok` | string | DARC Ortsverband Kennung (e.g., "F03", "P30") |
|
||
|
||
### Satellite Fields
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `satName` | string | Satellite name (e.g., "AO-73", "RS-44") |
|
||
|
||
### LoTW Confirmation Fields
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `lotwQslRstatus` | string | LoTW QSL received status (`"Y"` = confirmed) |
|
||
| `lotwQslRdate` | string | LoTW QSL received date (YYYY-MM-DD) |
|
||
|
||
### DCL Confirmation Fields
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `dclQslRstatus` | string | DCL QSL received status (`"Y"` = confirmed) |
|
||
| `dclQslRdate` | string | DCL QSL received date (YYYY-MM-DD) |
|
||
|
||
---
|
||
|
||
## Progress Calculation Result
|
||
|
||
The award calculation returns a standardized result object:
|
||
|
||
### Base Result Structure
|
||
|
||
```typescript
|
||
{
|
||
worked: number, // Count of worked entities
|
||
confirmed: number, // Count of confirmed entities
|
||
target: number, // Award target
|
||
percentage: number, // Progress percentage (0-100)
|
||
workedEntities: string[], // Array of worked entity identifiers
|
||
confirmedEntities: string[] // Array of confirmed entity identifiers
|
||
}
|
||
```
|
||
|
||
### Detail Result Structure (with `includeDetails: true`)
|
||
|
||
```typescript
|
||
{
|
||
// ... base fields ...
|
||
|
||
award: {
|
||
id: string,
|
||
name: string,
|
||
description: string,
|
||
caption: string,
|
||
target: number
|
||
},
|
||
|
||
entities: [
|
||
{
|
||
qsoId: number, // Reference QSO ID
|
||
entity: string, // Entity identifier
|
||
entityId: number, // DXCC entity ID (if applicable)
|
||
entityName: string, // Display name
|
||
worked: boolean,
|
||
confirmed: boolean,
|
||
qsoDate: string,
|
||
band: string,
|
||
mode: string,
|
||
callsign: string,
|
||
lotwQslRdate?: string, // Present if confirmed
|
||
dclQslRdate?: string, // Present if DCL confirmed
|
||
satName?: string, // For satellite awards
|
||
points?: number // For points-based awards
|
||
}
|
||
],
|
||
|
||
total: number, // Total entities
|
||
confirmed: number // Confirmed count
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Implementation Guide
|
||
|
||
### Core Algorithm
|
||
|
||
```
|
||
1. Load award definition from JSON file
|
||
2. Query QSOs for user from database
|
||
3. Apply filters (if defined in rules)
|
||
4. Based on rule type:
|
||
- entity: Count unique entity values
|
||
- dok: Count unique (DOK, band, mode) combinations
|
||
- points: Sum points by countMode
|
||
- filtered: Apply filters to base entity rule
|
||
- counter: Count QSOs or callsigns
|
||
5. Return progress with worked/confirmed counts
|
||
```
|
||
|
||
### Rule Type Resolution
|
||
|
||
```
|
||
1. If rules.type === "filtered":
|
||
Convert to entity rule with baseRule + filters
|
||
|
||
2. If rules.type === "counter":
|
||
Convert to entity rule with entityType = "callsign"
|
||
|
||
3. If rules.type === "dok":
|
||
Use dedicated DOK calculation
|
||
|
||
4. If rules.type === "points":
|
||
Use dedicated points calculation
|
||
|
||
5. If rules.type === "entity":
|
||
Use standard entity calculation
|
||
```
|
||
|
||
### Filter Application
|
||
|
||
```
|
||
function applyFilters(qsos, filters) {
|
||
if filters.operator === "AND":
|
||
return qsos where ALL filter conditions match
|
||
else if filters.operator === "OR":
|
||
return qsos where ANY filter condition matches
|
||
}
|
||
|
||
function matchesFilter(qso, filter) {
|
||
value = qso[filter.field]
|
||
|
||
switch filter.operator:
|
||
case "eq": return value === filter.value
|
||
case "ne": return value !== filter.value
|
||
case "in": return filter.value.includes(value)
|
||
case "nin": return !filter.value.includes(value)
|
||
case "contains": return value.includes(filter.value)
|
||
}
|
||
```
|
||
|
||
### Entity Value Extraction
|
||
|
||
```
|
||
function getEntityValue(qso, entityType) {
|
||
switch entityType:
|
||
case "dxcc": return qso.entityId
|
||
case "state": return qso.state
|
||
case "grid": return qso.grid.substring(0, 4) // First 4 chars
|
||
case "callsign": return qso.callsign
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Confirmation Systems
|
||
|
||
### LoTW (Logbook of The World)
|
||
|
||
- **Confirmation field**: `lotwQslRstatus === 'Y'`
|
||
- **Date field**: `lotwQslRdate`
|
||
- Used for: DXCC, WAS, VUCC, most entity-based awards
|
||
|
||
### DCL (DARC Community Logbook)
|
||
|
||
- **Confirmation field**: `dclQslRstatus === 'Y'`
|
||
- **Date field**: `dclQslRdate`
|
||
- Required for: DLD award
|
||
- Provides: DOK fields (`darcDok`)
|
||
|
||
---
|
||
|
||
## Award Variants
|
||
|
||
Using filters, you can create award variants from a base award:
|
||
|
||
| Base Award | Filter | Variant |
|
||
|------------|--------|---------|
|
||
| DLD | `band = "80m"` | DLD 80m |
|
||
| DLD | `mode = "CW"` | DLD CW |
|
||
| DLD | `band = "80m"` AND `mode = "CW"` | DLD 80m CW |
|
||
| DXCC | `mode = "CW"` | DXCC CW |
|
||
| DXCC | `band = "20m"` | DXCC 20m |
|
||
| DXCC | `satellite = true` | DXCC Satellite |
|
||
|
||
---
|
||
|
||
## Example Awards
|
||
|
||
### 1. DXCC Mixed Mode
|
||
|
||
```json
|
||
{
|
||
"id": "dxcc-mixed",
|
||
"name": "DXCC Mixed Mode",
|
||
"description": "Confirm 100 DXCC entities on any band/mode",
|
||
"caption": "Contact and confirm 100 different DXCC entities. Any band and mode combination counts.",
|
||
"category": "dxcc",
|
||
"rules": {
|
||
"type": "entity",
|
||
"entityType": "dxcc",
|
||
"target": 100,
|
||
"displayField": "entity"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. WAS (Worked All States)
|
||
|
||
```json
|
||
{
|
||
"id": "was-mixed",
|
||
"name": "WAS Mixed Mode",
|
||
"description": "Confirm all 50 US states",
|
||
"caption": "Contact and confirm all 50 US states.",
|
||
"category": "was",
|
||
"rules": {
|
||
"type": "entity",
|
||
"entityType": "state",
|
||
"target": 50,
|
||
"displayField": "state",
|
||
"filters": {
|
||
"operator": "AND",
|
||
"filters": [
|
||
{ "field": "entityId", "operator": "eq", "value": 291 }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. VUCC Satellite
|
||
|
||
```json
|
||
{
|
||
"id": "vucc-satellite",
|
||
"name": "VUCC Satellite",
|
||
"description": "Confirm 100 unique grid squares via satellite",
|
||
"caption": "Contact and confirm 100 unique 4-character grid squares via satellite.",
|
||
"category": "vucc",
|
||
"rules": {
|
||
"type": "entity",
|
||
"entityType": "grid",
|
||
"target": 100,
|
||
"displayField": "grid",
|
||
"filters": {
|
||
"operator": "AND",
|
||
"filters": [
|
||
{ "field": "satellite", "operator": "eq", "value": true }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. 73 on 73 (Satellite)
|
||
|
||
```json
|
||
{
|
||
"id": "73-on-73",
|
||
"name": "73 on 73",
|
||
"description": "Confirm 73 unique QSO partners on satellite AO-73",
|
||
"caption": "Contact and confirm 73 different stations via the AO-73 satellite.",
|
||
"category": "satellite",
|
||
"rules": {
|
||
"type": "entity",
|
||
"entityType": "callsign",
|
||
"target": 73,
|
||
"displayField": "callsign",
|
||
"filters": {
|
||
"operator": "AND",
|
||
"filters": [
|
||
{ "field": "satName", "operator": "eq", "value": "AO-73" }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5. DLD 80m CW (Combined Filters)
|
||
|
||
```json
|
||
{
|
||
"id": "dld-80m-cw",
|
||
"name": "DLD 80m CW",
|
||
"description": "Confirm 100 unique DOKs on 80m using CW",
|
||
"caption": "Contact and confirm 100 unique DOKs on 80m band using CW mode.",
|
||
"category": "darc",
|
||
"rules": {
|
||
"type": "dok",
|
||
"target": 100,
|
||
"confirmationType": "dcl",
|
||
"displayField": "darcDok",
|
||
"filters": {
|
||
"operator": "AND",
|
||
"filters": [
|
||
{ "field": "band", "operator": "eq", "value": "80m" },
|
||
{ "field": "mode", "operator": "eq", "value": "CW" }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Pseudocode Implementation
|
||
|
||
### Main Calculation Function
|
||
|
||
```
|
||
function calculateAwardProgress(userId, award, includeDetails = false):
|
||
rules = normalizeRules(award.rules)
|
||
|
||
// Get user's QSOs
|
||
qsos = database.getQSOs(userId)
|
||
|
||
// Apply filters
|
||
if rules.filters:
|
||
qsos = applyFilters(qsos, rules.filters)
|
||
|
||
// Calculate based on rule type
|
||
switch rules.type:
|
||
case "entity":
|
||
return calculateEntityProgress(qsos, rules, includeDetails)
|
||
case "dok":
|
||
return calculateDOKProgress(qsos, rules, includeDetails)
|
||
case "points":
|
||
return calculatePointsProgress(qsos, rules, includeDetails)
|
||
```
|
||
|
||
### Entity Progress Calculation
|
||
|
||
```
|
||
function calculateEntityProgress(qsos, rules, includeDetails):
|
||
workedEntities = new Set()
|
||
confirmedEntities = new Set()
|
||
entityDetails = []
|
||
|
||
for qso in qsos:
|
||
entity = getEntityValue(qso, rules.entityType)
|
||
if entity is null: continue
|
||
|
||
workedEntities.add(entity)
|
||
|
||
if qso.lotwQslRstatus === 'Y':
|
||
confirmedEntities.add(entity)
|
||
|
||
if includeDetails and entity not in entityDetails:
|
||
entityDetails.push({
|
||
qsoId: qso.id,
|
||
entity: entity,
|
||
entityName: getDisplayName(qso, rules.displayField),
|
||
worked: true,
|
||
confirmed: true,
|
||
qsoDate: qso.qsoDate,
|
||
band: qso.band,
|
||
mode: qso.mode,
|
||
callsign: qso.callsign
|
||
})
|
||
|
||
result = {
|
||
worked: workedEntities.size,
|
||
confirmed: confirmedEntities.size,
|
||
target: rules.target,
|
||
percentage: (confirmedEntities.size / rules.target) * 100
|
||
}
|
||
|
||
if includeDetails:
|
||
result.entities = entityDetails
|
||
result.total = entityDetails.length
|
||
|
||
return result
|
||
```
|
||
|
||
### DOK Progress Calculation
|
||
|
||
```
|
||
function calculateDOKProgress(qsos, rules, includeDetails):
|
||
combinations = new Map() // Key: "DOK/band/mode"
|
||
|
||
for qso in qsos:
|
||
dok = qso.darcDok
|
||
if dok is null: continue
|
||
|
||
band = qso.band || "Unknown"
|
||
mode = qso.mode || "Unknown"
|
||
key = `${dok}/${band}/${mode}`
|
||
|
||
if not combinations.has(key):
|
||
combinations.set(key, {
|
||
qsoId: qso.id,
|
||
entity: dok,
|
||
entityName: dok,
|
||
band: band,
|
||
mode: mode,
|
||
callsign: qso.callsign,
|
||
worked: false,
|
||
confirmed: false
|
||
})
|
||
|
||
detail = combinations.get(key)
|
||
detail.worked = true
|
||
|
||
if qso.dclQslRstatus === 'Y':
|
||
detail.confirmed = true
|
||
detail.dclQslRdate = qso.dclQslRdate
|
||
|
||
workedDOKs = new Set()
|
||
confirmedDOKs = new Set()
|
||
|
||
for [key, detail] in combinations:
|
||
workedDOKs.add(detail.entity)
|
||
if detail.confirmed:
|
||
confirmedDOKs.add(detail.entity)
|
||
|
||
result = {
|
||
worked: workedDOKs.size,
|
||
confirmed: confirmedDOKs.size,
|
||
target: rules.target,
|
||
percentage: (confirmedDOKs.size / rules.target) * 100
|
||
}
|
||
|
||
if includeDetails:
|
||
result.entities = Array.from(combinations.values())
|
||
result.total = result.entities.length
|
||
|
||
return result
|
||
```
|
||
|
||
### Points Progress Calculation
|
||
|
||
```
|
||
function calculatePointsProgress(qsos, rules, includeDetails):
|
||
stationPoints = new Map()
|
||
for station in rules.stations:
|
||
stationPoints.set(station.callsign.toUpperCase(), station.points)
|
||
|
||
workedStations = new Set()
|
||
totalPoints = 0
|
||
stationDetails = []
|
||
|
||
if rules.countMode === "perBandMode":
|
||
combinationMap = new Map()
|
||
|
||
for qso in qsos:
|
||
callsign = qso.callsign.toUpperCase()
|
||
points = stationPoints.get(callsign)
|
||
if not points: continue
|
||
|
||
band = qso.band || "Unknown"
|
||
mode = qso.mode || "Unknown"
|
||
key = `${callsign}/${band}/${mode}`
|
||
|
||
if not combinationMap.has(key):
|
||
combinationMap.set(key, {
|
||
qsoId: qso.id,
|
||
callsign: callsign,
|
||
band: band,
|
||
mode: mode,
|
||
points: points,
|
||
worked: true,
|
||
confirmed: false
|
||
})
|
||
|
||
if qso.lotwQslRstatus === 'Y':
|
||
detail = combinationMap.get(key)
|
||
if not detail.confirmed:
|
||
detail.confirmed = true
|
||
|
||
details = Array.from(combinationMap.values())
|
||
totalPoints = details.filter(d => d.confirmed).reduce(sum, d.points)
|
||
|
||
result = {
|
||
worked: workedStations.size,
|
||
totalPoints: totalPoints,
|
||
target: rules.target,
|
||
percentage: (totalPoints / rules.target) * 100
|
||
}
|
||
|
||
if includeDetails:
|
||
result.entities = details.map(formatEntity)
|
||
result.total = details.length
|
||
|
||
return result
|
||
```
|
||
|
||
---
|
||
|
||
## Language Implementation Notes
|
||
|
||
### Data Structure Recommendations
|
||
|
||
1. **Use Sets for uniqueness**: When counting unique entities, use hash sets for O(1) lookups
|
||
2. **Use Maps for combinations**: When tracking (DOK, band, mode) combinations, use composite keys
|
||
3. **Normalize early**: Convert filtered/counter rules to entity rules at load time
|
||
|
||
### Performance Considerations
|
||
|
||
1. **Cache award definitions**: Load once at startup, not per calculation
|
||
2. **Database queries**: Apply filters at database level when possible (SQL WHERE clauses)
|
||
3. **Pagination**: For large QSO datasets, process in batches
|
||
4. **Index fields**: Ensure database indexes on userId, entityId, band, mode, callsign
|
||
|
||
### Confirmation Field Handling
|
||
|
||
```typescript
|
||
// Helper function for unified confirmation checking
|
||
function isConfirmed(qso): boolean {
|
||
return qso.lotwQslRstatus === 'Y' || qso.dclQslRstatus === 'Y'
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Extension Points
|
||
|
||
### Adding New Rule Types
|
||
|
||
To add a new rule type:
|
||
|
||
1. Define the rule schema (properties, required fields)
|
||
2. Implement calculation function
|
||
3. Add case to main calculation switch
|
||
4. Add example award definition
|
||
|
||
### Adding New Entity Types
|
||
|
||
To add a new entity type:
|
||
|
||
1. Add to `entityType` enum in entity rule
|
||
2. Add case to `getEntityValue()` function
|
||
3. Ensure QSO database has the field
|
||
4. Add display field mapping if needed
|
||
|
||
### Adding New Filter Operators
|
||
|
||
To add a new filter operator:
|
||
|
||
1. Add operator name to filter schema
|
||
2. Implement comparison logic in `matchesFilter()`
|
||
3. Add documentation with examples
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
The award system is a flexible, JSON-driven framework for calculating amateur radio award progress. Key design principles:
|
||
|
||
1. **Externalized definitions**: Awards defined as JSON, not code
|
||
2. **Rule-based calculation**: Different rule types for different award patterns
|
||
3. **Filter-based variants**: Create awards variants without new rule types
|
||
4. **Standardized output**: Consistent progress format across all award types
|
||
5. **Confirmation system aware**: Supports LoTW and DCL separately
|
||
|
||
This specification provides all information needed to implement the system in any programming language.
|