helper

package
v0.17.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 1, 2026 License: MPL-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package-level note: excel.go implements the page-layout algorithm that writes pool match and elimination match data into an Excel workbook.

Layout model:

  • "Pool Matches" sheet: courts are placed side-by-side (8 columns each). Pools are rendered top-to-bottom within each court column. A soft page-break is inserted whenever the next pool block would overflow PoolMatchesRowsPerPage rows. Vertical page breaks separate courts so the sheet prints as distinct pages.

  • "Elimination Matches" section: elimination rounds are laid out top-to-bottom with all courts side-by-side. A new page break is inserted when the next match block would overflow EliminationRowsPerPage.

  • Tree sheets ("Tree 1", "Tree 2", …): one sheet per bracket segment. Leaf values reference pool-match winner cells via CONCATENATE formulas so the bracket updates automatically when scores are entered.

Row-count thresholds and layout constants are defined in constants.go.

CHK037, Kachinuki Excel rendering decision (T160 + T195–T203):

The main Pool Matches / Elimination Matches sheets continue to use the 8-column-per-court layout invariant (CourtsColumnsPerCourt = 8, see constants.go and CLAUDE.md). Variable-bout kachinuki grids would either overflow that budget or force a layout-mode switch the rest of the workbook can't accommodate, so the main sheets carry the team-match row only.

Bout-by-bout detail is rendered on a separate "Kachinuki Detail" sheet (helper.SheetKachinukiDetail). See internal/helper/excel_kachinuki.go, the sheet uses a flexible 8-column layout (NOT bound by CourtsColumnsPerCourt) and is opt-in: the engine export path (internal/engine/export.go → collectKachinukiMatches) emits it only when comp.TeamMatchType == kachinuki AND at least one match carries bouts. CLI export paths (cmd/create-pools.go, create-playoffs.go) are kachinuki-agnostic and produce zero changes to existing example files.

Index

Constants

View Source
const (
	// PoolMatchesRowsPerPage is the soft row budget before inserting a page
	// break on the Pool Matches sheet.
	PoolMatchesRowsPerPage = 45

	// PoolSpaceLines is the number of blank rows added after the pool header
	// before the first match block.
	PoolSpaceLines = 3

	// PoolDrawRowsPerPage is the soft row budget before inserting a page
	// break on the Pool Draw sheet.
	PoolDrawRowsPerPage = 42
)

Pool match sheet layout constants.

View Source
const (
	// EliminationRowsPerPage is the soft row budget before inserting a page
	// break in the elimination-match section.
	EliminationRowsPerPage = 44

	// EliminationSpaceLines is the number of blank rows printed between
	// elimination match rounds.
	EliminationSpaceLines = 5

	// EliminationMatchHeight is the row-height of a single individual
	// elimination match block.
	EliminationMatchHeight = 8

	// EliminationTeamMatchHeightBase is added to the team-match count to get
	// the row-height of a team elimination match block.
	EliminationTeamMatchHeightBase = 11
)

Elimination match layout constants.

View Source
const (
	DefaultPort     = 8080
	DefaultWinners  = 2
	DefaultPoolSize = 3
	DefaultCourts   = 2
)

Default flag values used by CLI commands and the web handler.

View Source
const (
	MinDateYear = 1900
	MaxDateYear = 2100
)

MinDateYear / MaxDateYear are the inclusive bounds on the year component of tournament + competition dates. The mobile-app HTTP handlers (validateDateDMY in handlers_tournament.go) enforce these on every write path so a value the API accepts is also one the admin UI can edit. Without matching bounds, a direct API/import write landing an out-of-range date would block every subsequent admin Settings save, the JS validator re-validates the stored date on every PUT and surfaces an inline error before reaching the wire.

Mirrored client-side as `MIN_YEAR` / `MAX_YEAR` in web-mobile/js/admin_helpers.jsx, keep all four in lockstep. Pin tests on both sides assert the literal values so cross-language drift fails CI rather than waiting for a date-related UX bug.

View Source
const (
	SheetData               = "data"
	SheetTimeEstimator      = "Time Estimator"
	SheetPoolDraw           = "Pool Draw"
	SheetPoolMatches        = "Pool Matches"
	SheetEliminationMatches = "Elimination Matches"
	SheetNamesToPrint       = "Names to Print"
	SheetTags               = "Tags"
	SheetTree               = "Tree"
	SheetKachinukiDetail    = "Kachinuki Detail"
)

Sheet names for every tab in the workbook. Use these constants wherever a sheet name is needed so that a rename only requires one edit here.

SheetKachinukiDetail is opt-in: only emitted by the engine export path when a competition has teamMatchType=kachinuki AND at least one kachinuki match has bout data to display. See excel_kachinuki.go (T199–T203).

View Source
const (
	// NearDupLevenshteinMax is the maximum edit distance that the secondary
	// (Levenshtein) typo gate will consider as a near-duplicate.
	NearDupLevenshteinMax = 2

	// NearDupRatioMin is the minimum similarity ratio (1 - lev/maxLen) required
	// alongside NearDupLevenshteinMax.  0.85 means "at most ~15 % different".
	NearDupRatioMin = 0.85
)

Fuzzy duplicate detection constants, document the threshold choices so reviewers can reason about them without digging into the algorithm.

View Source
const CourtsColumnsPerCourt = 8

CourtsColumnsPerCourt is the number of Excel columns allocated to each court (Shiaijo) on the Pool Matches and Elimination Matches sheets. Layout: Name | V | P | vs | P | V | Name | Spacer = 8 columns.

View Source
const MaxCourts = 26

MaxCourts is the upper bound for the number of Shiaijo (courts). It comes from the single-letter A–Z labelling used on Shiaijo headers throughout the workbook; values above this are rejected up front by ValidateCourts so we never silently truncate a user-requested layout.

Mirrored client-side as `MAX_COURTS` in web-mobile/js/admin_helpers.jsx, keep the two in lockstep. The JS side is anchored by a comment back here so changes are visible at both edit points.

View Source
const MaxPlayersPerTree = 16

MaxPlayersPerTree is the maximum leaf count on a single tree sheet. 16 players form a balanced bracket that fits on one A4 landscape page.

View Source
const MaxRankOverride = 1000

MaxRankOverride is the absolute upper bound for a manual rank override submitted via PUT /api/competitions/:id/pools/:poolId/override-rank. The override-rank handler ALSO validates against the actual pool size (the real semantic constraint, rank within a pool must be in [1..N] where N is the number of players in that pool). This cap is a defense-in-depth overflow guard for the rare case where pools have not been generated yet or LoadPools returns stale/unexpected data.

Mirrored client-side as `MAX_RANK` in web-mobile/js/admin_helpers.jsx, keep the two in lockstep. 1000 is arbitrary; no real pool has 1000+ participants.

View Source
const TreeTitleRows = 3

TreeTitleRows is the number of rows reserved at the top of every tree sheet for the user to add a title. Content starts below this offset.

Variables

View Source
var MobileWebFs embed.FS
View Source
var WebFs embed.FS

Functions

func AddPlayerDataToSheet added in v0.3.0

func AddPlayerDataToSheet(f *excelize.File, players []Player, sanitize bool, titlePrefix string) map[string]playerCellCoord

func AddPoolDataToSheet added in v0.3.0

func AddPoolDataToSheet(f *excelize.File, pools []Pool, sanitize bool, titlePrefix string) (map[string]cellCoord, map[string]playerCellCoord)

func AddPoolsToSheet

func AddPoolsToSheet(f *excelize.File, pools []Pool, poolCoords map[string]cellCoord, playerCoords map[string]playerCellCoord) error

func AddPoolsToTree

func AddPoolsToTree(f *excelize.File, sheetName string, pools []Pool, poolCoords map[string]cellCoord, pCoords map[string]playerCellCoord)

func ApplyPoolAdjustments added in v0.17.0

func ApplyPoolAdjustments(node *Node)

ApplyPoolAdjustments applies the same pre-order treeAdjustment traversal that PrintLeafNodes performs when pools=true. Use before TreeToLeafArray to reproduce the pool-finalist ordering the Excel bracket applies.

func ApplySeeds added in v0.11.0

func ApplySeeds(players []Player, assignments []domain.SeedAssignment) error

ApplySeeds assigns seeds to the helper players, handling swaps if needed Returns an error if an assigned name could not be matched

func AssignMatchNumbers added in v0.17.0

func AssignMatchNumbers(eliminationMatchRounds [][]*Node)

AssignMatchNumbers assigns sequential match numbers to all non-nil nodes in eliminationMatchRounds, in the same iteration order as the Excel FillInMatches output (earliest/widest round first, within-round left-to-right). Each non-nil node's matchNum field is set in-place.

This is the authoritative numbering for the printed Excel Tree sheet. The web API has a SEPARATE implementation, engine.assignBracketMatchNumbers, which operates on *state.Bracket (a different type) instead of []*Node, the two are NOT a literally-shared function. They are kept equal-by-contract: skip the same positions (a nil node here == a Hidden-or-both-sides-empty match there) and iterate the same round order, so the Nth real match gets the same number on both paths. That contract is verified by TestMatchNumberingParity_ExcelVsWeb in internal/engine, which builds both numberings from identical entrant sets (including bye-producing, non-power-of-two sizes) and asserts the sequences match position-for-position. The printed Excel sheet is authoritative; if they ever diverge, the web path must be corrected to match this one.

func AssignPlayerNumbers added in v0.16.0

func AssignPlayerNumbers(players []Player, prefix string, start int) int

AssignPlayerNumbers sets Number on each player to prefix+counter, where counter starts at start and increments by one. Returns the next counter value so callers can chain across multiple slices (e.g. pools).

func AssignPoolsToCourts added in v0.14.0

func AssignPoolsToCourts(numPools, numCourts int) ([]int, error)

AssignPoolsToCourts distributes numPools pools across numCourts courts using contiguous blocks that match the tree sheet grouping. The first court gets ceil(numPools/numCourts) pools, subsequent courts get the remainder. Returns an error when numCourts exceeds numPools.

func CalculateDepth

func CalculateDepth(node *Node) int

func CanonicalRegistrationSource added in v0.17.0

func CanonicalRegistrationSource(s string) string

CanonicalRegistrationSource returns the canonical stored form of a registration source: trimmed + lower-case. Keeps filter buckets from splitting on whitespace/casing ("Manual" vs "manual"). The legacy "reserved" token (an unused value in the old participant-tag enum) is aliased to "manual" so any hand-edited/older CSV row reloads as a recognised source instead of silently shifting into Metadata.

func CheckDuplicateEntries added in v0.14.0

func CheckDuplicateEntries(input []string) []string

CheckDuplicateEntries scans the raw entry list (one CSV row per element) for duplicates and returns a list of entries that appear more than once. Empty strings are ignored. The returned slice preserves first-seen order of the offending entries; an empty result means the list is unique.

func CheckDuplicateEntriesByNameDojo added in v0.17.0

func CheckDuplicateEntriesByNameDojo(entries [][2]string) []string

CheckDuplicateEntriesByNameDojo scans the raw entry list for perfect duplicate (name, dojo) pairs. Each entry is a [2]string of {name, dojo}. Returns a list of "name / dojo" strings that collide; empty means the list is unique.

This replaces the old whole-row CheckDuplicateEntries for contexts that know the name and dojo fields.

func CircleMethodRounds added in v0.17.0

func CircleMethodRounds(n int) [][]IntPair

CircleMethodRounds generates a full round-robin schedule for n players using the circle (polygon) method. Player 0 is fixed; players 1..N-1 rotate.

For even n: n-1 rounds, n/2 matches per round, n*(n-1)/2 total. For odd n: n rounds, (n-1)/2 matches per round (one bye per round),

n*(n-1)/2 total.

Returns nil for n < 2.

func ConvertPlayersToWinners added in v0.3.0

func ConvertPlayersToWinners(players []Player, sanitized bool, pCoords map[string]playerCellCoord) map[string]MatchWinner

func CourtLabel added in v0.16.0

func CourtLabel(i int) string

CourtLabel returns the letter label (A–Z) for a zero-based court index.

func CreateNamesToPrint

func CreateNamesToPrint(f *excelize.File, players []Player, sanitized bool, numCourts int, pCoords map[string]playerCellCoord)

func CreateNamesWithPoolToPrint added in v0.3.0

func CreateNamesWithPoolToPrint(f *excelize.File, pools []Pool, sanitized bool, numCourts int, pCoords map[string]playerCellCoord)

func CreatePartialPoolMatches added in v0.16.0

func CreatePartialPoolMatches(pools []Pool)

CreatePartialPoolMatches fills each pool's Matches slice with adjacent-neighbour pairings only, for N players it produces N-1 matches in the sequence (0,1), (1,2), ..., (N-2,N-1). This is the "partial round-robin" / league format selected via state.Competition.PoolFormat == "partial".

FR-052 / R7: full round-robin grows O(N²) so even modest pools become untenably long; partial pools give every player exactly two bouts (one with the neighbour above, one with the neighbour below) except the two endpoints who get one each.

Pre-condition: pools[i].Players must be sorted in the order the pairings should follow (typically seed/rank order, the caller in engine/pools.go applies PoolSeeding before passing them in).

func CreatePoolMatches

func CreatePoolMatches(pools []Pool)

func CreatePoolRoundRobinMatches

func CreatePoolRoundRobinMatches(pools []Pool)

func CreateTagsSheet added in v0.11.0

func CreateTagsSheet(f *excelize.File, pools []Pool, publicURL string) error

CreateTagsSheet adds a "Tags" sheet to f with two large competitor tags per A4 page (one tag per half-page, two copies per player). When publicURL is non-empty and a player has a Number, a QR code is embedded in the bottom-left corner of each tag linking to the public viewer pre-filtered to that competitor.

func CreateTreeBracket

func CreateTreeBracket(f *excelize.File, sheet string, col int, startRow int, size int) string

func EstimateMatchCounts added in v0.17.0

func EstimateMatchCounts(in EstimateMatchCountsInput) (poolMatchCount, playoffMatchCount int, err error)

EstimateMatchCounts returns the expected number of pool matches and playoff bracket matches for a competition given its configuration and participant count. The estimates are purely derived from the same formulas used by the real draw pipeline:

  • Pool count: helper.CreatePools (ceiling vs floor division by PoolSize, driven by PoolSizeMode == "max").
  • Pool matches: poolMatchesPerPool for each individual pool size.
  • Bracket matches: bracketMatchCount(numFinalists), which returns the court-time-consuming match count (excludes auto-resolved byes), equal to numFinalists-1 now that byes are distributed to the top seeds (mp-sess).

Pool count and per-pool sizes are derived by calling the real CreatePools (no duplication). Per-pool match counting (poolMatchesPerPool) and bracket match counting (bracketMatchCount) mirror the draw's match-generation logic; these are lightweight formulas cross-checked by integration tests against the real generators. See mp-zoh plan's "Central design risk".

Returned counts reflect court-time-consuming matches only. Auto-resolved bracket byes (player-vs-bye leaf matches marked Completed at generation time) are excluded because assignBracketMatchSlots does not advance the court cursor for them (scheduler_slots.go: 286-291). Pool-side byes are not applicable (pools have no bye mechanism). Negative PlayerCount or zero-round Swiss are clamped to zero matches rather than erroring, because the estimator may be called speculatively before validation is complete.

Returns an error for:

  • Unknown Format strings (only when PlayerCount > 0; the zero-player early return takes precedence and returns nil, callers should not rely on a format error for zero-player inputs).
  • PoolSize == 0 for the mixed format (would divide by zero).

func FillEstimations added in v0.6.0

func FillEstimations(f *excelize.File, numPools int64, totalPoolMatches int64, teamSize int64, numEliminationMatches int64, numCourts int)

func FillInMatches

func FillInMatches(f *excelize.File, eliminationMatchRounds [][]*Node)

func GenerateFinals

func GenerateFinals(pools []Pool, poolWinners int) []string

GenerateFinals interleaves pool finalists so that when CreateBalancedTree distributes them into bracket slots, the first-place finisher of one pool is paired against the second-place finisher of another pool.

The algorithm emits one full pass over the pools per "round" r (r = 0..poolWinners-1). Within a round, pool p contributes the finisher of rank (p + r) % poolWinners. For any fixed pool p, the ranks chosen across the rounds form a cyclic shift of {0..poolWinners-1}, a permutation, so every "<pool>-<ordinal>" placeholder appears EXACTLY once: no duplicates, none missing, for ALL pool counts and poolWinners values. Adjacent slots hold different pools whose ranks differ by 1 (mod poolWinners), preserving the cross-pool seeding intent so 1st-place finishers are paired against lower finishers of other pools.

(The previous formulation gated its round counter on `len(pools)%poolWinners == 0`, which aliased the rank rotation for non-coprime combinations, e.g. poolWinners>=4 with 2/6/10 pools, silently duplicating some placeholders and dropping others. Since mp-turx makes these placeholders the leaves of the LIVE in-place knockout, that corrupted real results; this formulation is duplicate-free by construction.)

Example with 4 pools and 2 winners per pool:

result = [Pool_A-1st, Pool_B-2nd, Pool_C-1st, Pool_D-2nd,
          Pool_A-2nd, Pool_B-1st, Pool_C-2nd, Pool_D-1st]

func GetBorderStyleBottomLeft

func GetBorderStyleBottomLeft(f *excelize.File) int

func GetBorderStyleLeft

func GetBorderStyleLeft(f *excelize.File) int

func GetOrdinal added in v0.17.0

func GetOrdinal(n int) string

func IsPoolFinalistPlaceholder added in v0.17.0

func IsPoolFinalistPlaceholder(s string) bool

IsPoolFinalistPlaceholder reports whether s is a pool-origin finalist placeholder ("Pool A-1st", "Pool B-2nd", etc.) as emitted by helper.GenerateFinals. Unlike IsReservedParticipantName, this does NOT match next-round feeder labels ("Winner of r1-m3"), callers that need to distinguish the two patterns (e.g. bracketHasPoolPlaceholders) should use this instead.

func IsRegistrationSource added in v0.17.0

func IsRegistrationSource(s string) bool

IsRegistrationSource reports whether s is a recognised participant registration source (case-insensitive): manual / registered / transfer. Exported so the API boundary validator can reject unknown values before they are persisted, the CSV loader only recognises these tokens, so an unexpected value would otherwise shift into Metadata on reload.

func IsReservedParticipantName added in v0.17.0

func IsReservedParticipantName(name string) bool

IsReservedParticipantName reports whether name collides with a bracket placeholder pattern used by the engine. Such names would be misclassified as unresolved bracket slots and make knockout matches permanently unscoreable.

func IsUUIDv4 added in v0.16.0

func IsUUIDv4(s string) bool

IsUUIDv4 reports whether s has the lowercase canonical-UUID shape (8-4-4-4-12 lowercase hex). Despite the historical name, this is a SHAPE check, it does NOT enforce the v4 version nibble or the 8/9/a/b variant nibble. Many in-repo test fixtures use non-v4 or invalid-variant UUIDs (e.g. `…-7c3a-11e7-…` v1 timestamps, `…-4ddd-dddd-dddd-…` synthetic ids) and pass this check. Callers that need true v4 validation should parse with `uuid.Parse(s)` and inspect `Version()`/`Variant()` instead.

func MatchHeader

func MatchHeader(f *excelize.File, sheetName string, startColName string, poolRow int, middleColName string, endColName string, mirror bool)

func NewUUID4 added in v0.16.0

func NewUUID4() string

NewUUID4 generates a random UUID v4 string.

func NextPow2 added in v0.14.0

func NextPow2(n int) int

NextPow2 returns the smallest power of 2 that is >= n. Returns 1 for n <= 1.

func NormalizeParticipantName added in v0.17.0

func NormalizeParticipantName(s string) string

NormalizeParticipantName applies the shared normalization used by both the perfect-match dedup key and the near-duplicate signals. The JS mirror in web-mobile/js/data.jsx must stay byte-for-byte equivalent (parity fixture):

  1. NFD decompose (canonical decomposition), then strip ONLY combining marks in U+0300–U+036F (Latin combining diacriticals). This folds Müller→muller and Ï→i, but MUST leave Japanese dakuten (が) and all CJK/kana intact because dakuten's combining mark U+3099 is outside the stripped range.
  2. Re-NFC so the result is in canonical composed form.
  3. Lowercase, trim, and collapse internal whitespace.

func ParseSeedsFile added in v0.11.0

func ParseSeedsFile(filePath string) ([]domain.SeedAssignment, error)

ParseSeedsFile reads a CSV file mapping names to seed positions

func PathGraphRounds added in v0.17.0

func PathGraphRounds(n int) [][]IntPair

PathGraphRounds generates a two-round schedule for the n-1 adjacent matches of a path graph: (0,1), (1,2), …, (n-2, n-1).

Round 0 contains even-indexed edges: (0,1),(2,3),… Round 1 contains odd-indexed edges: (1,2),(3,4),… (omitted if empty).

Returns nil for n < 2.

func PrintLeafNodes

func PrintLeafNodes(node *Node, f *excelize.File, sheetName string, startCol int, startRow int, depth int, pools bool, matchWinners map[string]MatchWinner)

func PrintPoolMatches

func PrintPoolMatches(f *excelize.File, pools []Pool, teamMatches int, numWinners int, numCourts int, mirror bool, poolCoords map[string]cellCoord, pCoords map[string]playerCellCoord) map[string]MatchWinner

func PrintTeamEliminationMatches added in v0.2.0

func PrintTeamEliminationMatches(f *excelize.File, poolMatchWinners map[string]MatchWinner, eliminationMatchRounds [][]*Node, numTeamMatches int, numCourts int, mirror bool)

func ProtectAllSheets added in v0.16.0

func ProtectAllSheets(f *excelize.File)

ProtectAllSheets applies protection to the Tree sheets, Names to Print, Tags, Pool Matches, and Elimination Matches. The score-entry sheets have explicitly unlocked cells for data entry.

func ProtectSheets added in v0.16.0

func ProtectSheets(f *excelize.File, sheetNames []string)

ProtectSheets applies sheet-level protection.

func ReadCSVFile added in v0.17.0

func ReadCSVFile(filePath string) ([][]string, error)

ReadCSVFile reads a CSV file using encoding/csv, properly handling RFC 4180 quoting (fields with commas, double-quotes, or newlines).

func ReadEntriesFromFile added in v0.9.0

func ReadEntriesFromFile(filePath string) ([]string, error)

func RemoveDuplicates

func RemoveDuplicates(input []string) []string

RemoveDuplicates removes duplicate strings from the input slice and returns a new slice without duplicates.

The function takes a parameter named input, which is a slice of strings. It represents the input slice from which duplicates and empty strings will be removed.

func RoundToPowerOf2 added in v0.4.0

func RoundToPowerOf2(x, y float64) (int, error)

func SanitizeName added in v0.16.0

func SanitizeName(name string) string

SanitizeName returns the canonical display form derived from a participant name: a single token uppercased ("KAZUKI") or "F. LAST" for multi-token names. Exported so state.SaveParticipants can detect display names that match the auto-derived form and avoid round-trip data corruption (a 3-column row whose DisplayName equals SanitizeName(Name) carries no extra information and must not be written for non-zekken competitions, see internal/state/participants.go).

func SetSheetLayoutLandscapeA3 added in v0.11.0

func SetSheetLayoutLandscapeA3(f *excelize.File, sheetName string)

func SetSheetLayoutPortraitA4 added in v0.11.0

func SetSheetLayoutPortraitA4(f *excelize.File, sheetName string)

func SetSheetLayoutPortraitA4Centered added in v0.14.0

func SetSheetLayoutPortraitA4Centered(f *excelize.File, sheetName string)

SetSheetLayoutPortraitA4Centered configures portrait A4 with print-centering. Unlike SetSheetLayoutPortraitA4 it does not enable FitToPage, so smaller content keeps its natural size and the horizontal/vertical centering print option actually has room to take effect.

func SetSheetLayoutPortraitA4DownThenOver added in v0.14.0

func SetSheetLayoutPortraitA4DownThenOver(f *excelize.File, sheetName string, numCourts int)

func SetTreeSheetTitle added in v0.14.0

func SetTreeSheetTitle(f *excelize.File, sheetName string, title string)

SetTreeSheetTitle writes a title formula into the first row of a tree sheet, spanning a wide range of columns to cover the bracket layout. The formula prepends the value of data!$B$1 (the user-supplied title prefix) to the given title string, so editing that single cell updates all tree sheets.

func SubtreeCourtIndex added in v0.16.0

func SubtreeCourtIndex(numSubtrees, numCourts, idx int) int

SubtreeCourtIndex returns the zero-based court index for tree subtree idx when numSubtrees are spread across numCourts. Mirrors the grouping used by poolBoundsForSubtree so that court labels are always consistent.

func TitleCaseName added in v0.17.0

func TitleCaseName(name string) string

TitleCaseName applies the same Unicode Title-casing that CreatePlayers uses so names stored to participants.csv (and seeds.csv) match what is read back on the next load, avoiding seed-merge mismatches. TrimSpace is applied first to match CreatePlayers' per-column trim before title-casing.

func TreePageLayout added in v0.16.0

func TreePageLayout(numPlayers, numCourts int, singleTree bool) (int, error)

TreePageLayout computes the number of tree sheet pages needed for numPlayers competitors assigned to numCourts Shiaijo. When singleTree is true the result is always 1 (unless court expansion requires more, which singleTree suppresses). numCourts must be clamped by the caller before calling if caller-specific rules apply (e.g. capping at numPools).

func TreeToLeafArray added in v0.17.0

func TreeToLeafArray(node *Node) []string

TreeToLeafArray converts a tree built by CreateBalancedTree into a power-of-two leaf array suitable for buildBracketFromLeaves. Internal nodes recurse into left and right subtrees, padding each side to NextPow2(max(len(left), len(right))) with "" (bye slots) before concatenating. The result length is always NextPow2(N) where N is the number of real leaves, and bye positions mirror the tree's structural asymmetry so the same matchups produced by the Excel bracket are reproduced.

func ValidateCourts added in v0.14.0

func ValidateCourts(n int) error

ValidateCourts returns an error when n is outside the supported court range. Courts are labelled A–Z, so MaxCourts (26) is the hard upper bound. n < 1 is also rejected so the caller does not have to guess what "0 courts" should mean.

func WriteKachinukiDetailSheet added in v0.16.0

func WriteKachinukiDetailSheet(f *excelize.File, matches []KachinukiMatchDetail) error

WriteKachinukiDetailSheet creates the SheetKachinukiDetail sheet and writes one section per match. When matches is empty the sheet is NOT created, the caller is responsible for checking the input length AND the renderer guards against accidental emission of an empty sheet.

Types

type EstimateMatchCountsInput added in v0.17.0

type EstimateMatchCountsInput struct {
	// Format is state.Competition.Format: "playoffs", "mixed", "league",
	// "swiss". Empty string is treated as "playoffs" for backward
	// compatibility with legacy configs. Any other value returns an error.
	Format string

	// PlayerCount is the number of participants that will take part in
	// this competition.
	PlayerCount int

	// Pool-phase fields, relevant for "mixed" and "league" formats.
	PoolSize     int    // state.Competition.PoolSize
	PoolSizeMode string // state.Competition.PoolSizeMode: "max" or "min" (any other value including "" treated as "min")
	PoolWinners  int    // state.Competition.PoolWinners (default 2 when 0)
	RoundRobin   bool   // state.Competition.RoundRobin
	PoolFormat   string // state.Competition.PoolFormat: "" | "full" | "partial"

	// SwissRounds is the number of rounds for a swiss-format competition.
	SwissRounds int
}

EstimateMatchCountsInput carries the scalar fields from a competition configuration that are needed to derive pre-draw pool and playoff match counts. Only scalar types are used (no *state.Competition) because the state package imports helper, adding the reverse import would create a cycle. Callers in the engine layer should populate this struct from the relevant Competition fields.

Required field matrix by Format:

"playoffs", PlayerCount only (no pool fields needed).
"mixed", PlayerCount, PoolSize (>0), PoolSizeMode, PoolWinners,
               RoundRobin, PoolFormat.
"league", PlayerCount only (single pool, always full round-robin).
"swiss", PlayerCount, SwissRounds.

type IntPair added in v0.17.0

type IntPair struct {
	A, B int
}

IntPair represents a match pairing by player index (A < B always).

type KachinukiBout added in v0.16.0

type KachinukiBout struct {
	Position  int    // 1-based bout index within the team match
	SideAName string // player name for Side A
	SideAPos  string // lineup position (Senpo, Jiho, Chuken, Fukusho, Taisho), may be empty
	ScoreA    string // accumulated ippon string (e.g. "MK", "MMK") or empty
	SideBName string
	SideBPos  string
	ScoreB    string
	Winner    string // player name of the winner; empty for hikiwake
	Decision  string // canonical decision wire value (fought / hikiwake / kiken / fusenpai / fusensho / daihyosen / kachinuki-exhaustion)
}

KachinukiBout is one bout in a kachinuki team match.

type KachinukiMatchDetail added in v0.16.0

type KachinukiMatchDetail struct {
	Label        string // human-readable match identifier (e.g. "Pool A - Match 1")
	SideATeam    string // team name on Side A
	SideBTeam    string // team name on Side B
	Bouts        []KachinukiBout
	Winner       string // winning team name; empty when the match did not end with a winner
	Decision     string // canonical wire decision for the parent match (typically "kachinuki-exhaustion")
	EliminationA int    // count of Side A players retired by the end of the match
	EliminationB int    // count of Side B players retired by the end of the match
}

KachinukiMatchDetail is a single team match's bout log with team-level summary metadata. One section is rendered per entry in WriteKachinukiDetailSheet.

type Match

type Match struct {
	SideA *Player `json:"sideA"`
	SideB *Player `json:"sideB"`
	Round int     `json:"round"`
}

type MatchWinner

type MatchWinner struct {
	// contains filtered or unexported fields
}

MatchWinner records the Excel cell that contains a pool or elimination match winner's name; used to build cross-sheet formula references in bracket trees.

type NearDupWarning added in v0.17.0

type NearDupWarning struct {
	// Kind is always "near-duplicate".
	Kind string `json:"kind"`
	// A and B are the ORIGINAL (un-normalized) input names of the pair, so
	// the operator sees exactly what they typed/imported.
	A string `json:"a"`
	// B is the second member of the pair (also original, un-normalized).
	B string `json:"b"`
	// Score is a human-readable description of why the pair fired (e.g.
	// "token-subset" or "levenshtein:2/ratio:0.90").
	Score string `json:"score"`
}

NearDupWarning describes a single near-duplicate pair.

func FindNearDupWarnings added in v0.17.0

func FindNearDupWarnings(entries [][2]string) []NearDupWarning

FindNearDupWarnings scans normalised names for near-duplicate pairs. Each entry is {name, dojo} already as raw (non-normalized) strings; the function normalises internally. Returns non-blocking warnings.

Signal 1: token-subset, tokens of A ⊆ tokens of B (or vice-versa). Identical normalized strings are skipped first (na == nb → Tier-1), so the surviving cases are a genuine proper subset ("Ana Maria Rossi" / "Ana Rossi", a middle-name-drop) OR equal token sets reached by reordering ("Rossi Ana" / "Ana Rossi"), both are intentionally flagged as likely the same person.

Signal 2: Levenshtein typo gate, lev ≤ NearDupLevenshteinMax AND ratio ≥ NearDupRatioMin, suppressed when the two strings differ only in a single trailing single-character token (squad suffix).

type Node

type Node struct {
	LeafNode bool

	// sheet for Cell Values
	SheetName string

	// Pool Number or Cell value
	LeafVal string
	Val     int64
	Left    *Node
	Right   *Node
	// contains filtered or unexported fields
}

func CreateBalancedTree

func CreateBalancedTree(leafValues []string) *Node

func OrderStringsAlphabetically

func OrderStringsAlphabetically(strings []*Node) []*Node

func SubdivideTree added in v0.4.0

func SubdivideTree(node *Node, numSubtrees int) []*Node

function that subdivides a tree into a specified number of subtrees

func TraverseRounds

func TraverseRounds(node *Node, depth int, maxDepth int) []*Node

func (*Node) MatchNum added in v0.17.0

func (n *Node) MatchNum() int64

MatchNum returns the sequential match number assigned to this node by AssignMatchNumbers (0 if not yet assigned). Exported so cross-package callers, notably the engine-vs-Excel numbering-parity test, can read the authoritative printed-sheet number without reaching into the unexported field.

type Player

type Player = domain.Player

Player is a type alias for domain.Player. The helper package used to own a parallel struct during the NFR-007 migration; it was collapsed to an alias once the two were proven field-identical (the converters were copying fields 1:1 with no translation). The helper name is kept for rendering-side ergonomics inside this package.

func CreatePlayers

func CreatePlayers(entries []string, withZekkenName bool) ([]Player, error)

func CreatePlayersFromRecords added in v0.17.0

func CreatePlayersFromRecords(records [][]string, withZekkenName bool) ([]Player, error)

CreatePlayersFromRecords builds players from pre-parsed CSV records (each record is a slice of fields). Use this when the CSV has already been parsed by encoding/csv so that quoted commas are handled correctly.

func PoolSeeding added in v0.14.0

func PoolSeeding(players []Player, numPools int, numCourts int) []Player

PoolSeeding reorders players for pool distribution so that top seeds land in pools that are appropriately spread across the given number of courts.

It assigns seeds to courts in a round-robin fashion and uses a per-court priority to ensure correct bracket placement (e.g., top and bottom of the court's bracket) after the pools are deinterleaved by ReorderPoolsForCourts.

func StandardSeeding added in v0.11.0

func StandardSeeding(players []Player) []Player

StandardSeeding reorders players into bracket positions such that seeded participants (Seed > 0) are spaced according to tournament standards (e.g., #1 and #2 on opposite halves). Unseeded players fill the remaining slots.

func StandardSeedingFull added in v0.17.0

func StandardSeedingFull(players []Player) []Player

StandardSeedingFull places players into a FULL power-of-two bracket and returns a slice of length NextPow2(len(players)), or nil for an empty input. Each player is positioned by its seeding RANK via generateBracketOrder, and the surplus high-rank slots are left as zero-value Players (empty Name), i.e. byes.

Rank assignment matches StandardSeeding (and the Excel draw): a seeded player (Seed > 0) claims its Seed NUMBER as its rank, so an operator who assigns non-contiguous seeds (e.g. {1, 2, 5}) gets the #5 seed at the rank-5 bracket position, not the third-from-top. Genuine unseeded players fill the remaining ranks 1..N in input order; any seed whose number is out of range or collides is then treated as unseeded too (appended after the genuine unseeded players, so a displaced seed's exact slot is unspecified; these are degenerate inputs).

Unlike StandardSeeding (which returns a dense len(players) slice and leaves the caller to pad byes at the end of the leaf array), the byes here are interleaved at their standard positions: a bracket of N players in a 2^k draw gives the top 2^k, N seeds a first-round bye, and because every bye rank pairs with a distinct low (top-seed) rank in round 1, the draw never contains an empty-vs-empty match. Used by the live-playoffs leaf builder so the knockout tree matches conventional seeding instead of clustering all byes at the bottom.

type Pool

type Pool struct {
	PoolName string   `json:"poolName"`
	Players  []Player `json:"players"`
	Matches  []Match  `json:"matches,omitempty"`
}

func CreatePools

func CreatePools(players []Player, poolSize int, isMax bool) ([]Pool, error)

func ReorderPoolsForCourts added in v0.14.0

func ReorderPoolsForCourts(pools []Pool, numCourts int) []Pool

ReorderPoolsForCourts deinterleaves pools so that when divided into contiguous court blocks, each block has balanced pool sizes and seeds are spread across courts. Original pool i goes to court block (i % numCourts). Pool names are re-assigned alphabetically after reordering.

type RowStack added in v0.2.0

type RowStack struct {
	// contains filtered or unexported fields
}

func (*RowStack) Peek added in v0.2.1

func (s *RowStack) Peek() (int, error)

func (*RowStack) Pop added in v0.2.0

func (s *RowStack) Pop() (int, error)

func (*RowStack) Push added in v0.2.0

func (s *RowStack) Push(value int)

func (*RowStack) PushHighest added in v0.2.1

func (s *RowStack) PushHighest(first int, second int)

type Stack

type Stack []*Node

func (*Stack) IsEmpty

func (s *Stack) IsEmpty() bool

func (*Stack) Pop

func (s *Stack) Pop() *Node

func (*Stack) Push

func (s *Stack) Push(node *Node)

Directories

Path Synopsis
Package bracket will hold bracket/tree algorithms extracted from internal/helper.
Package bracket will hold bracket/tree algorithms extracted from internal/helper.
Package csv will hold CSV parsing extracted from internal/helper.
Package csv will hold CSV parsing extracted from internal/helper.
Package seeding will hold seeding algorithms extracted from internal/helper.
Package seeding will hold seeding algorithms extracted from internal/helper.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL