From 4c52212f3d34de442dd9471f0d4b0fcd6b311df1 Mon Sep 17 00:00:00 2001 From: Howler Monkey Date: Wed, 11 Dec 2024 15:51:19 -0700 Subject: [PATCH 01/17] feat(scripts): start single match upload script --- api/src/db/index.ts | 5 +++-- api/src/db/matches.ts | 16 ++++++++++++++++ scripts/upload/singleMatch.ts | 22 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 scripts/upload/singleMatch.ts diff --git a/api/src/db/index.ts b/api/src/db/index.ts index c70b4594..efe716ab 100644 --- a/api/src/db/index.ts +++ b/api/src/db/index.ts @@ -16,7 +16,8 @@ export const connect = async () => { } const url = !LOCAL_DEV ? MONGO_URL : MONGO_URL_LOCAL; - const publicLogsDBName = url!.split("@")[1]?.split(".")[0] || "local"; + const publicLogsDBHost = url!.split("@")[1]?.split(".")[0] || "local"; + const dbName = url!.split("?")[0]?.split("/").reverse()[0] || "root"; const _connect = () => { console.error("DB: connecting"); @@ -35,7 +36,7 @@ export const connect = async () => { await _connect(); }); mongoose.connection.on("connected", () => { - console.error(`DB: connected to ${publicLogsDBName}`); + console.error(`DB: connected to ${publicLogsDBHost} ${dbName}`); }); mongoose.connection.on("reconnected", () => { diff --git a/api/src/db/matches.ts b/api/src/db/matches.ts index f9226a1d..ed944d5c 100644 --- a/api/src/db/matches.ts +++ b/api/src/db/matches.ts @@ -96,6 +96,22 @@ const fetchMatchesRange = async ( })); }; +export const matchFromMatchDef = h => { + const updated = new Date(`${h.match_modifieddate}Z`); + return { + updated, + created: new Date(`${h.match_creationdate}Z`), + id: Number.parseInt(h.match_id.split("-").reverse()[0], 16), + name: h.match_name, + uuid: h.match_id, + date: h.match_date, + timestamp_utc_updated: updated.getTime(), + type: h.match_type, + subType: h.match_subtype, + templateName: h.templateName, + }; +}; + const fetchMatchesRangeByTimestamp = async ( latestTimestamp: number, template = "USPSA", diff --git a/scripts/upload/singleMatch.ts b/scripts/upload/singleMatch.ts new file mode 100644 index 00000000..763dbbb8 --- /dev/null +++ b/scripts/upload/singleMatch.ts @@ -0,0 +1,22 @@ +/* eslint-disable no-console */ +import { connect } from "../../api/src/db/index"; +import { matchFromMatchDef } from "../../api/src/db/matches"; +import { fetchPS } from "../../api/src/worker/uploads"; + +const go = async () => { + const matchUUID = process.argv[2]; + if (!matchUUID) { + console.error("must provide match name"); + process.exit(1); + } + + const { matchDef, scores, results } = await fetchPS(matchUUID); + const match = matchFromMatchDef(matchDef); + console.log(JSON.stringify(match, null, 2)); + + //await connect(); + + process.exit(0); +}; + +go(); From 3e20539852a68a52b8fdf5b5fd7b31165169de08 Mon Sep 17 00:00:00 2001 From: Howler Monkey Date: Wed, 11 Dec 2024 15:59:51 -0700 Subject: [PATCH 02/17] feat(singleMatch): save Match to db --- api/src/db/matches.ts | 16 +++++++++++++++- scripts/upload/singleMatch.ts | 21 ++++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/api/src/db/matches.ts b/api/src/db/matches.ts index ed944d5c..ce4f8267 100644 --- a/api/src/db/matches.ts +++ b/api/src/db/matches.ts @@ -22,6 +22,17 @@ interface AlgoliaMatchNumericFilters { timestamp_utc_updated: number; } +export interface MatchDef { + match_id: string; + match_name: string; + match_type: string; + match_subtype: string; + match_creationdate: string; + match_modifieddate: string; + match_date: string; // 2024-12-31 format + templateName: string; +} + const MatchesSchema = new mongoose.Schema( { updated: Date, @@ -96,7 +107,10 @@ const fetchMatchesRange = async ( })); }; -export const matchFromMatchDef = h => { +export const matchFromMatchDef = (h: MatchDef): Match & AlgoliaMatchNumericFilters => { + if (!h) { + return h; + } const updated = new Date(`${h.match_modifieddate}Z`); return { updated, diff --git a/scripts/upload/singleMatch.ts b/scripts/upload/singleMatch.ts index 763dbbb8..7f728cc0 100644 --- a/scripts/upload/singleMatch.ts +++ b/scripts/upload/singleMatch.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ import { connect } from "../../api/src/db/index"; -import { matchFromMatchDef } from "../../api/src/db/matches"; +import { Matches, matchFromMatchDef } from "../../api/src/db/matches"; import { fetchPS } from "../../api/src/worker/uploads"; const go = async () => { @@ -13,9 +13,24 @@ const go = async () => { const { matchDef, scores, results } = await fetchPS(matchUUID); const match = matchFromMatchDef(matchDef); console.log(JSON.stringify(match, null, 2)); + await connect(); + if (!match?.name) { + console.error("bad match"); + process.exit(1); + } + await Matches.bulkWrite([ + { + updateOne: { + filter: { + uuid: match.uuid, + }, + update: { $set: match }, + upsert: true, + }, + }, + ]); - //await connect(); - + console.log("done"); process.exit(0); }; From 5d4c611c7fd1942613fccee003cff9b09f504a88 Mon Sep 17 00:00:00 2001 From: Howler Monkey Date: Thu, 12 Dec 2024 14:58:25 -0700 Subject: [PATCH 03/17] feat(singleMatch): upload scores --- api/src/db/matches.ts | 7 ++- api/src/worker/uploads.ts | 84 +++++++++++++++++++++++++++++------ scripts/upload/singleMatch.ts | 25 ++++++++--- 3 files changed, 94 insertions(+), 22 deletions(-) diff --git a/api/src/db/matches.ts b/api/src/db/matches.ts index ce4f8267..a04233f5 100644 --- a/api/src/db/matches.ts +++ b/api/src/db/matches.ts @@ -107,7 +107,10 @@ const fetchMatchesRange = async ( })); }; -export const matchFromMatchDef = (h: MatchDef): Match & AlgoliaMatchNumericFilters => { +export const matchFromMatchDef = ( + h: MatchDef, + forcedTemplateName?: string, +): Match & AlgoliaMatchNumericFilters => { if (!h) { return h; } @@ -122,7 +125,7 @@ export const matchFromMatchDef = (h: MatchDef): Match & AlgoliaMatchNumericFilte timestamp_utc_updated: updated.getTime(), type: h.match_type, subType: h.match_subtype, - templateName: h.templateName, + templateName: forcedTemplateName || h.templateName, }; }; diff --git a/api/src/worker/uploads.ts b/api/src/worker/uploads.ts index c39981f0..959c2fa1 100644 --- a/api/src/worker/uploads.ts +++ b/api/src/worker/uploads.ts @@ -33,9 +33,9 @@ import { AfterUploadShooters, type AfterUploadShooter } from "../db/afterUploadS import { rehydrateClassifiers } from "../db/classifiers"; import { DQs } from "../db/dq"; import { connect } from "../db/index"; -import { Matches } from "../db/matches"; +import { Match, MatchDef, Matches } from "../db/matches"; import { hydrateRecHHFsForClassifiers } from "../db/recHHF"; -import { Scores } from "../db/scores"; +import { Score, Scores } from "../db/scores"; import { reclassifyShooters } from "../db/shooters"; import { hydrateStats } from "../db/stats"; @@ -211,7 +211,7 @@ const scsaMatchInfo = async matchInfo => { skipResults: true, }); if (!match || !scoresJson) { - return EmptyMatchResultsFactory(); + return EmptySingleMatchResultFactory(match); } /* match_penalties Structure: @@ -381,24 +381,53 @@ const scsaMatchInfo = async matchInfo => { return { scores, match, results: [] }; } catch (e) {} - return EmptyMatchResultsFactory(); + return EmptySingleMatchResultFactory(match); }; -export const uspsaOrHitFactorMatchInfo = async matchInfo => { - const { uuid } = matchInfo; - const { matchDef: match, results, scores: scoresJson } = await fetchPS(uuid); +const badCharsRegExp = /[:\s\t.,\-_+=!?]/gi; +const memberNumberFromMatchDefShooter = (s, mustHaveMemberNumbers) => { + if (mustHaveMemberNumbers) { + return s.sh_id; + } + + return s.sh_id || [s.sh_fn, s.sh_ln].join("").replace(badCharsRegExp, ""); +}; + +const classifierCodeFromMatchDefStage = (s, onlyClassifiers) => { + if (onlyClassifiers) { + return s.classifiercode; + } + + return `${s.stage_number || ""}.${s.stage_name.replace(badCharsRegExp, "").toUpperCase()}`; +}; + +export const hitFactorLikeMatchInfo = ( + matchInfo: Match, + s3MatchFiles, + onlyClassifiers: boolean = true, + mustHaveMemberNumbers: boolean = true, +): { match: MatchDef; results: unknown; scores: Score[] } => { + const { matchDef: match, results, scores: scoresJson } = s3MatchFiles; if (!match || !results || !scoresJson) { - return EmptyMatchResultsFactory(); + return EmptySingleMatchResultFactory(match); } const { match_shooters, match_stages } = match; - const shootersMap = Object.fromEntries(match_shooters.map(s => [s.sh_uuid, s.sh_id])); + const shootersMap = Object.fromEntries( + match_shooters.map(s => [ + s.sh_uuid, + memberNumberFromMatchDefShooter(s, mustHaveMemberNumbers), + ]), + ); match.memberNumberToNamesMap = Object.fromEntries( - match_shooters.map(s => [s.sh_id, [s.sh_fn, s.sh_ln].filter(Boolean).join(" ")]), + match_shooters.map(s => [ + memberNumberFromMatchDefShooter(s, mustHaveMemberNumbers), + [s.sh_fn, s.sh_ln].filter(Boolean).join(" "), + ]), ); const classifiersMap = Object.fromEntries( match_stages - .filter(s => !!s.stage_classifiercode) - .map(s => [s.stage_uuid, s.stage_classifiercode]), + .filter(s => !onlyClassifiers || !!s.stage_classifiercode) + .map(s => [s.stage_uuid, classifierCodeFromMatchDefStage(s, onlyClassifiers)]), ); const classifierUUIDs = Object.keys(classifiersMap); const classifierResults = results.filter(r => classifierUUIDs.includes(r.stageUUID)); @@ -466,7 +495,7 @@ export const uspsaOrHitFactorMatchInfo = async matchInfo => { memberNumber, classifier, division, - upload: uuid, + upload: match.match_id, clubid: match.match_clubcode, club_name: match.match_clubname || match.match_name, matchName: match.match_name, @@ -496,6 +525,26 @@ export const uspsaOrHitFactorMatchInfo = async matchInfo => { return { scores, match, results }; }; +export const uspsaOrHitFactorMatchInfo = async matchInfo => { + const { uuid } = matchInfo; + const s3MatchFiles = await fetchPS(uuid); + return hitFactorLikeMatchInfo(matchInfo, s3MatchFiles); +}; + +export const pcslNatsMatchInfo = async matchInfo => { + const { uuid } = matchInfo; + const s3MatchFiles = await fetchPS(uuid); + const result = hitFactorLikeMatchInfo(matchInfo, s3MatchFiles, false, false); + return { + ...result, + match: { + ...result.match, + templateName: matchInfo.templateName, + }, + }; +}; + +const EmptySingleMatchResultFactory = match => ({ scores: [], match, results: [] }); const EmptyMatchResultsFactory = () => ({ scores: [], matches: [], results: [] }); const uploadResultsForMatches = async matches => { @@ -505,6 +554,9 @@ const uploadResultsForMatches = async matches => { case "Steel Challenge": return scsaMatchInfo(match); + case "PCSLNats": + return pcslNatsMatchInfo(match); + case "USPSA": case "Hit Factor": default: { @@ -556,7 +608,10 @@ export const processUploadResults = async ({ uploadResults }) => { } acc.push({ - memberNumber: shooter.sh_id, + memberNumber: memberNumberFromMatchDefShooter( + shooter, + match.templateName !== "PCSLNats", + ), lastName: shooter.sh_ln, firstName: shooter.sh_fn, division: shooter.sh_dvp, @@ -570,6 +625,7 @@ export const processUploadResults = async ({ uploadResults }) => { }); return acc; }, []); + await DQs.bulkWrite( dqDocs.map(dq => ({ updateOne: { diff --git a/scripts/upload/singleMatch.ts b/scripts/upload/singleMatch.ts index 7f728cc0..3a411490 100644 --- a/scripts/upload/singleMatch.ts +++ b/scripts/upload/singleMatch.ts @@ -1,17 +1,22 @@ /* eslint-disable no-console */ import { connect } from "../../api/src/db/index"; import { Matches, matchFromMatchDef } from "../../api/src/db/matches"; -import { fetchPS } from "../../api/src/worker/uploads"; +import { + fetchPS, + // hitFactorLikeMatchInfo, + uploadMatches, +} from "../../api/src/worker/uploads"; const go = async () => { const matchUUID = process.argv[2]; - if (!matchUUID) { - console.error("must provide match name"); + const matchTemplateName = process.argv[2]; + if (!matchUUID || !matchTemplateName) { + console.error("must provide match name and templateName"); process.exit(1); } - const { matchDef, scores, results } = await fetchPS(matchUUID); - const match = matchFromMatchDef(matchDef); + const { matchDef } = await fetchPS(matchUUID); + const match = matchFromMatchDef(matchDef, process.argv[3]); console.log(JSON.stringify(match, null, 2)); await connect(); if (!match?.name) { @@ -30,7 +35,15 @@ const go = async () => { }, ]); - console.log("done"); + /* + const shit = hitFactorLikeMatchInfo(match, { matchDef, results, scores }, false, false); + console.log(JSON.stringify(shit.scores, null, 2)); + */ + + const upload = await uploadMatches({ matches: [match] }); + //console.log(JSON.stringify(upload, null, 2)); + + console.error("done"); process.exit(0); }; From 73a3fcd06bf10830c484bab9208723734c221c73 Mon Sep 17 00:00:00 2001 From: Howler Monkey Date: Sun, 15 Dec 2024 17:08:28 -0700 Subject: [PATCH 04/17] first classification results --- api/src/dataUtil/classifiersData.ts | 7 +- api/src/db/classifiers.ts | 53 ++++-- api/src/db/shooters.ts | 8 +- .../shooters/test/uspsaClassification.test.js | 152 ++++++++++++++++++ api/src/worker/uploads.ts | 20 ++- data/types/USPSA.ts | 1 + scripts/upload/singleMatch.ts | 18 ++- shared/constants/divisions.ts | 2 + shared/utils/classification.js | 9 +- 9 files changed, 240 insertions(+), 30 deletions(-) diff --git a/api/src/dataUtil/classifiersData.ts b/api/src/dataUtil/classifiersData.ts index 5b7f7239..1bba4008 100644 --- a/api/src/dataUtil/classifiersData.ts +++ b/api/src/dataUtil/classifiersData.ts @@ -3,7 +3,8 @@ import { loadJSON } from "../utils"; export type USPSAScoring = "Virginia" | "Comstock" | "Fixed Time"; export type SCSAScoring = "Time Plus"; -export type Scoring = USPSAScoring | SCSAScoring; +export type NAScoring = ""; +export type Scoring = USPSAScoring | SCSAScoring | NAScoring; export interface ClassifierJSON { id: string; @@ -35,6 +36,10 @@ export const basicInfoForClassifier = (c: ClassifierJSON): ClassifierBasicInfo = scoring: c?.scoring, }); +export const basicInfoForClassifierNumber = ( + classifierCode: string, +): ClassifierBasicInfo => basicInfoForClassifier(classifiersByNumber[classifierCode]); + export const basicInfoForClassifierCode = ( classifierCode: string, ): ClassifierBasicInfo | undefined => { diff --git a/api/src/db/classifiers.ts b/api/src/db/classifiers.ts index 2a01afe3..69c547c6 100644 --- a/api/src/db/classifiers.ts +++ b/api/src/db/classifiers.ts @@ -5,10 +5,11 @@ import mongoose, { Model } from "mongoose"; import { stringSort } from "../../../shared/utils/sort"; import { - basicInfoForClassifier, classifiers as _classifiers, - classifiersByNumber, ClassifierJSON, + basicInfoForClassifierNumber, + ClassifierBasicInfo, + Scoring, } from "../dataUtil/classifiersData"; import { divisionsForScoresAdapter, divShortNames } from "../dataUtil/divisions"; import { hhfsForDivision } from "../dataUtil/hhf"; @@ -86,16 +87,17 @@ const extendedInfoForClassifier = ( c: ClassifierJSON, division: string, hitFactorScores: Score[], + classifiersOnly?: boolean, ) => { if (!division || !c?.id) { return {}; } const divisionHHFs = hhfsForDivision(division); - if (!divisionHHFs) { + if (!divisionHHFs && classifiersOnly) { return {}; } - const curHHFInfo = divisionHHFs.find(dHHF => dHHF.classifier === c.id); - const hhf = Number(curHHFInfo.hhf); + const curHHFInfo = divisionHHFs?.find(dHHF => dHHF.classifier === c.id); + const hhf = Number(curHHFInfo?.hhf) || -1; const topXPercentileStats = x => ({ [`top${x}PercentilePercent`]: @@ -140,7 +142,7 @@ const extendedInfoForClassifier = ( .sort((a, b) => stringSort(a, b, "id", 1)); const result = { - updated: curHHFInfo.updated, //actualLastUpdate, // before was using curHHFInfo.updated, and it's bs + updated: curHHFInfo?.updated, //actualLastUpdate, // before was using curHHFInfo.updated, and it's bs hhf, prevHHF: hhfs.findLast(curHistorical => curHistorical.hhf !== hhf)?.hhf ?? hhf, hhfs, @@ -244,14 +246,33 @@ ClassifierSchema.index({ classifier: 1, division: 1 }, { unique: true }); ClassifierSchema.index({ division: 1 }); export const Classifiers = mongoose.model("Classifiers", ClassifierSchema); +export interface StageInfo { + code: string; // aka classifierCode/classifierNumber + number: number; // 1, 2, 3, etc + name: string; + scoring: Scoring; +} + +const basicInfoForStage = (stageClassifierCode: string): ClassifierBasicInfo => ({ + id: stageClassifierCode, + code: stageClassifierCode, + classifier: stageClassifierCode, + name: stageClassifierCode.split(".")[1], + scoring: "", +}); + export const singleClassifierExtendedMetaDoc = async ( division: string, classifier: string, recHHFReady?: RecHHF, + classifiersOnly?: boolean, ) => { - const c = classifiersByNumber[classifier]; - const basicInfo = basicInfoForClassifier(c); + const basicInfo = classifiersOnly + ? basicInfoForClassifierNumber(classifier) + : basicInfoForStage(classifier); + if (!basicInfo?.code) { + console.log(classifier); return null; } const [recHHFQuery, hitFactorScoresRaw] = await Promise.all([ @@ -282,7 +303,7 @@ export const singleClassifierExtendedMetaDoc = async ( return { division, ...basicInfo, - ...extendedInfoForClassifier(c, division, hitFactorScores), + ...extendedInfoForClassifier(basicInfo, division, hitFactorScores, classifiersOnly), recHHF, ...inverseRecPercentileStats(100), ...inverseRecPercentileStats(95), @@ -399,8 +420,14 @@ export const rehydrateSingleClassifier = async ( classifier: string, division: string, recHHF?: RecHHF, + onlyActualClassifiers: boolean = true, ) => { - const doc = await singleClassifierExtendedMetaDoc(division, classifier, recHHF); + const doc = await singleClassifierExtendedMetaDoc( + division, + classifier, + recHHF, + onlyActualClassifiers, + ); if (doc) { return Classifiers.updateOne( { division, classifier }, @@ -413,7 +440,10 @@ export const rehydrateSingleClassifier = async ( }; // linear rehydration to prevent OOMs on uploader and mongod -export const rehydrateClassifiers = async (classifiers: ClassifierDivision[]) => { +export const rehydrateClassifiers = async ( + classifiers: ClassifierDivision[], + onlyActualClassifiers: boolean = true, +) => { const recHHFs = await RecHHFs.find({ classifierDivision: { $in: classifiers.map(c => [c.classifier, c.division].join(":")), @@ -432,6 +462,7 @@ export const rehydrateClassifiers = async (classifiers: ClassifierDivision[]) => classifier, division, recHHFsByClassifierDivision[[classifier, division].join(":")], + onlyActualClassifiers, ); } }; diff --git a/api/src/db/shooters.ts b/api/src/db/shooters.ts index 62497240..b4dd67e2 100644 --- a/api/src/db/shooters.ts +++ b/api/src/db/shooters.ts @@ -12,7 +12,6 @@ import { hfuDivisionCompatabilityMap, hfuDivisionsShortNamesThatNeedMinorHF, mapDivisions, - uspsaDivShortNames, } from "../dataUtil/divisions"; import { psClassUpdatesByMemberNumber } from "../dataUtil/uspsa"; import { loadJSON, processImportAsyncSeq } from "../utils"; @@ -483,18 +482,13 @@ export const reclassifyShooters = async shooters => { ]); const updates = shooters - .filter( - ({ memberNumber, division }) => - // TODO: Implement Reclassify Shooters for SCSA - // https://github.com/CodeHowlerMonkey/hitfactor.info/issues/69 - memberNumber && uspsaDivShortNames.find(x => x === division), - ) .map(({ memberNumber, division, name }) => { if (!memberNumber) { return []; } const recMemberScores = recScoresByMemberNumber[memberNumber]; const curMemberScores = curScoresByMemberNumber[memberNumber]; + console.log(`${memberNumber}: ${recMemberScores.length} rec scores`); const recalcByCurPercent = calculateUSPSAClassification( curMemberScores, "curPercent", diff --git a/api/src/routes/api/shooters/test/uspsaClassification.test.js b/api/src/routes/api/shooters/test/uspsaClassification.test.js index 55d2897f..ca4cb77d 100644 --- a/api/src/routes/api/shooters/test/uspsaClassification.test.js +++ b/api/src/routes/api/shooters/test/uspsaClassification.test.js @@ -778,3 +778,155 @@ test("calculateUSPSAClassification A111317 should have co", () => { const result = calculateUSPSAClassification(noCurPercentButExpected, "curPercent"); assert.strictEqual(Number(result.co.percent.toFixed(2)), 72.6); }); + +test.only("pcslNats", () => { + const recScores = [ + { + hf: 5.7098, + sd: "2024-12-04T00:00:00.000Z", + memberNumber: "VARICKBEISE", + division: "competition", + classifier: "9.IT'S SO OVER", + classifierDivision: "9.IT'S SO OVER:competition", + percent: 0, + curPercent: -570.98, + recPercent: 111.9327, + source: "Stage Score", + }, + { + hf: 3.3858, + sd: "2024-12-04T00:00:00.000Z", + memberNumber: "VARICKBEISE", + division: "competition", + classifier: "1.GOLD COUNTRY", + classifierDivision: "1.GOLD COUNTRY:competition", + percent: 0, + curPercent: -338.58, + recPercent: 100.5076, + source: "Stage Score", + }, + { + hf: 7.762, + sd: "2024-12-04T00:00:00.000Z", + memberNumber: "VARICKBEISE", + division: "competition", + classifier: "8.WE ARE SO BACK", + classifierDivision: "8.WE ARE SO BACK:competition", + percent: 0, + curPercent: -776.2, + recPercent: 100.0026, + source: "Stage Score", + }, + { + hf: 5.1448, + sd: "2024-12-04T00:00:00.000Z", + memberNumber: "VARICKBEISE", + division: "competition", + classifier: "11.MOEZAMBIQUE", + classifierDivision: "11.MOEZAMBIQUE:competition", + percent: 0, + curPercent: -514.48, + recPercent: 97.1597, + source: "Stage Score", + }, + { + hf: 5.7792, + sd: "2024-12-04T00:00:00.000Z", + memberNumber: "VARICKBEISE", + division: "competition", + classifier: "10.ARE YA WINNIN SON", + classifierDivision: "10.ARE YA WINNIN SON:competition", + percent: 0, + curPercent: -577.92, + recPercent: 96.8982, + source: "Stage Score", + }, + { + hf: 5.29, + sd: "2024-12-04T00:00:00.000Z", + memberNumber: "VARICKBEISE", + division: "competition", + classifier: "7.BUY HIGH SELL LOW", + classifierDivision: "7.BUY HIGH SELL LOW:competition", + percent: 0, + curPercent: -529, + recPercent: 92.3937, + source: "Stage Score", + }, + { + hf: 3.0614, + sd: "2024-12-04T00:00:00.000Z", + memberNumber: "VARICKBEISE", + division: "competition", + classifier: "12.WOLVERINES", + classifierDivision: "12.WOLVERINES:competition", + percent: 0, + curPercent: -306.14, + recPercent: 90.0147, + source: "Stage Score", + }, + { + hf: 5.262, + sd: "2024-12-04T00:00:00.000Z", + memberNumber: "VARICKBEISE", + division: "competition", + classifier: "4.CROSSROADS", + classifierDivision: "4.CROSSROADS:competition", + percent: 0, + curPercent: -526.2, + recPercent: 86.3189, + source: "Stage Score", + }, + { + hf: 5.9628, + sd: "2024-12-04T00:00:00.000Z", + memberNumber: "VARICKBEISE", + division: "competition", + classifier: "5.TRAP HOUSE", + classifierDivision: "5.TRAP HOUSE:competition", + percent: 0, + curPercent: -596.28, + recPercent: 83.2003, + source: "Stage Score", + }, + { + hf: 4.5485, + sd: "2024-12-04T00:00:00.000Z", + memberNumber: "VARICKBEISE", + division: "competition", + classifier: "6.LEAD PEDAL", + classifierDivision: "6.LEAD PEDAL:competition", + percent: 0, + curPercent: -454.85, + recPercent: 73.2601, + source: "Stage Score", + }, + { + hf: 5.2349, + sd: "2024-12-04T00:00:00.000Z", + memberNumber: "VARICKBEISE", + division: "competition", + classifier: "2.BELT", + classifierDivision: "2.BELT:competition", + percent: 0, + curPercent: -523.49, + recPercent: 71.8724, + source: "Stage Score", + }, + { + hf: 5.7714, + sd: "2024-12-04T00:00:00.000Z", + memberNumber: "VARICKBEISE", + division: "competition", + classifier: "3.HARDBASS", + classifierDivision: "3.HARDBASS:competition", + percent: 0, + curPercent: -577.14, + recPercent: 66.7391, + source: "Stage Score", + }, + ]; + + const result = calculateUSPSAClassification(recScores, "recPercent"); + assert.strictEqual(Number(result.competition.percent.toFixed(2)), 100.0); +}); diff --git a/api/src/worker/uploads.ts b/api/src/worker/uploads.ts index 959c2fa1..dc7f4e9d 100644 --- a/api/src/worker/uploads.ts +++ b/api/src/worker/uploads.ts @@ -384,17 +384,17 @@ const scsaMatchInfo = async matchInfo => { return EmptySingleMatchResultFactory(match); }; -const badCharsRegExp = /[:\s\t.,\-_+=!?]/gi; +const badCharsRegExp = /[:\t.,\-_+=!?]/gi; const memberNumberFromMatchDefShooter = (s, mustHaveMemberNumbers) => { if (mustHaveMemberNumbers) { return s.sh_id; } - return s.sh_id || [s.sh_fn, s.sh_ln].join("").replace(badCharsRegExp, ""); + return s.sh_id || [s.sh_fn, s.sh_ln].join("").replace(badCharsRegExp, "").toUpperCase(); }; -const classifierCodeFromMatchDefStage = (s, onlyClassifiers) => { - if (onlyClassifiers) { +const classifierCodeFromMatchDefStage = (s, onlyActualClassifiers) => { + if (onlyActualClassifiers) { return s.classifiercode; } @@ -801,7 +801,7 @@ export const dqNames = async () => { console.log(JSON.stringify(dqs, null, 2)); }; -const metaClassifiersLoop = async (batchSize = 8) => { +const metaClassifiersLoop = async (batchSize = 8, onlyActualClassifiers = false) => { const totalCount = await AfterUploadClassifiers.countDocuments({}); console.log(`${totalCount} classifiers to update`); @@ -814,7 +814,7 @@ const metaClassifiersLoop = async (batchSize = 8) => { } await hydrateRecHHFsForClassifiers(classifiers); - await rehydrateClassifiers(classifiers); + await rehydrateClassifiers(classifiers, onlyActualClassifiers); await AfterUploadClassifiers.deleteMany({ _id: { $in: classifiers.map(c => c._id) }, }); @@ -851,7 +851,11 @@ const metaShootersLoop = async (batchSize = 8) => { } }; -export const metaLoop = async (curTry = 1, maxTries = 3) => { +export const metaLoop = async ( + onlyActualClassifiers = true, + curTry = 1, + maxTries = 3, +) => { try { await metaClassifiersLoop(); await metaShootersLoop(); @@ -859,7 +863,7 @@ export const metaLoop = async (curTry = 1, maxTries = 3) => { } catch (err) { console.error(err); if (curTry < maxTries) { - return metaLoop(curTry + 1, maxTries); + return metaLoop(onlyActualClassifiers, curTry + 1, maxTries); } } }; diff --git a/data/types/USPSA.ts b/data/types/USPSA.ts index b8eef28a..d5ea01a1 100644 --- a/data/types/USPSA.ts +++ b/data/types/USPSA.ts @@ -7,6 +7,7 @@ export interface HHFJSON { id: string; classifier: string; // number id of classifier in classifers.json hhf: string; // number in toFixed(4) + updated?: string; // date in "2022-03-01 21:39:39" format } /** diff --git a/scripts/upload/singleMatch.ts b/scripts/upload/singleMatch.ts index 3a411490..acac1bf8 100644 --- a/scripts/upload/singleMatch.ts +++ b/scripts/upload/singleMatch.ts @@ -3,6 +3,7 @@ import { connect } from "../../api/src/db/index"; import { Matches, matchFromMatchDef } from "../../api/src/db/matches"; import { fetchPS, + metaLoop, // hitFactorLikeMatchInfo, uploadMatches, } from "../../api/src/worker/uploads"; @@ -41,7 +42,22 @@ const go = async () => { */ const upload = await uploadMatches({ matches: [match] }); - //console.log(JSON.stringify(upload, null, 2)); + console.log(JSON.stringify(upload, null, 2)); + await Matches.bulkWrite( + [match].map(m => ({ + updateOne: { + filter: { uuid: m.uuid }, + update: { + $set: { + uploaded: new Date(), + hasScores: true, + }, + }, + }, + })), + ); + console.error("marked match as uploaded"); + await metaLoop(false); console.error("done"); process.exit(0); diff --git a/shared/constants/divisions.ts b/shared/constants/divisions.ts index 401c0cb5..19afb066 100644 --- a/shared/constants/divisions.ts +++ b/shared/constants/divisions.ts @@ -293,6 +293,8 @@ export const divisionsForRecHHFAdapter = division => { export const allDivShortNames = [ ...uspsaDivShortNames, ...hfuDivisionsShortNames, + "practical", + "competition", // TODO: pcsl // TODO: scsa ]; diff --git a/shared/utils/classification.js b/shared/utils/classification.js index ada7c323..7f249052 100644 --- a/shared/utils/classification.js +++ b/shared/utils/classification.js @@ -138,6 +138,8 @@ const windowSizeForScore = windowSize => { return 6; }; +const cappedScoreFunc = score => Math.min(100, score); +const uncappedScoreFunc = score => score; const ageForDate = (now, sd) => (now - new Date(sd)) / (28 * 24 * 60 * 60 * 1000); export const percentAndAgesForDivWindow = ( div, @@ -145,14 +147,17 @@ export const percentAndAgesForDivWindow = ( percentField = "percent", now = new Date(), ) => { + const isRecMode = percentField === "recPercent"; //de-dupe needs to be done in reverse, because percent are sorted asc let window = state[div].window; - if (percentField !== "recPercent") { + if (!isRecMode) { // don't use best dupe for recommended, only most recent one window = window.toSorted((a, b) => numSort(a, b, percentField, -1)); } const dFlagsApplied = uniqBy(window, c => c.classifier); + const scoreFunc = isRecMode ? uncappedScoreFunc : cappedScoreFunc; + // remove lowest 2 const newLength = windowSizeForScore(dFlagsApplied.length); const fFlagsApplied = dFlagsApplied @@ -160,7 +165,7 @@ export const percentAndAgesForDivWindow = ( .slice(0, newLength); const percent = fFlagsApplied.reduce( (acc, curValue, curIndex, allInWindow) => - acc + Math.min(100, curValue[percentField]) / allInWindow.length, + acc + scoreFunc(curValue[percentField]) / allInWindow.length, 0, ); From 19dfe45ce1fe8e8564ed69ee651396e26ba1cfc1 Mon Sep 17 00:00:00 2001 From: Howler Monkey Date: Wed, 18 Dec 2024 13:26:57 -0700 Subject: [PATCH 05/17] remove 2,3,8,9 exclusion --- api/src/worker/uploads.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/worker/uploads.ts b/api/src/worker/uploads.ts index dc7f4e9d..a7d539a9 100644 --- a/api/src/worker/uploads.ts +++ b/api/src/worker/uploads.ts @@ -428,6 +428,9 @@ export const hitFactorLikeMatchInfo = ( match_stages .filter(s => !onlyClassifiers || !!s.stage_classifiercode) .map(s => [s.stage_uuid, classifierCodeFromMatchDefStage(s, onlyClassifiers)]), + /*.filter( + ([uuid, stageCode]) => !["2", "3", "8", "9"].includes(stageCode.split(".")[0]), + ),*/ ); const classifierUUIDs = Object.keys(classifiersMap); const classifierResults = results.filter(r => classifierUUIDs.includes(r.stageUUID)); From 12e95d6cf0cebd9f1b247038d9f640057042dfea Mon Sep 17 00:00:00 2001 From: Howler Monkey Date: Wed, 18 Dec 2024 13:29:12 -0700 Subject: [PATCH 06/17] classifier page working, with r1/r2/r5 selector --- api/src/routes/api/classifiers/index.js | 27 +--- api/src/worker/uploads.ts | 2 +- shared/constants/divisions.ts | 12 ++ web/src/components/DivisionNavigation.jsx | 3 +- web/src/components/RunsTable.jsx | 54 -------- web/src/components/ShooterCell.jsx | 67 ++-------- web/src/components/chart/ScoresChart.jsx | 80 +++++------- .../components/ClassifierInfoTable.jsx | 121 +----------------- web/src/pages/ClassifiersPage/index.jsx | 10 +- 9 files changed, 66 insertions(+), 310 deletions(-) diff --git a/api/src/routes/api/classifiers/index.js b/api/src/routes/api/classifiers/index.js index 7251fdbf..324c2d4a 100644 --- a/api/src/routes/api/classifiers/index.js +++ b/api/src/routes/api/classifiers/index.js @@ -183,11 +183,6 @@ const classifiersRoutes = async fastify => { const { division, number } = req.params; const c = classifiers.find(cur => cur.classifier === number); - if (!c) { - res.statusCode = 404; - return { info: null }; - } - const basic = basicInfoForClassifier(c); const [extended, recHHFInfo] = await Promise.all([ Classifiers.findOne({ division, classifier: number }).lean(), @@ -253,8 +248,6 @@ const classifiersRoutes = async fastify => { fastify.get("/:division/:number/chart", async req => { const { division, number } = req.params; - const { full: fullString } = req.query; - const full = Number(fullString); const runs = await Scores.aggregate([ { @@ -298,6 +291,7 @@ const classifiersRoutes = async fastify => { { $addFields: { recHHF: _getRecHHFField("recHHF"), + name: _getShooterField("name"), }, }, { @@ -338,32 +332,17 @@ const classifiersRoutes = async fastify => { { $sort: { hf: -1 } }, ]); - const hhf = curHHFForDivisionClassifier({ number, division }); const allPoints = runs.map((run, index, allRuns) => ({ x: HF(run.hf), y: PositiveOrMinus1(Percent(index, allRuns.length)), - memberNumber: run.memberNumber, + memberNumber: run.name, curPercent: run.curPercent || 0, curHHFPercent: run.curHHFPercent || 0, recPercent: run.recPercent || 0, scoreRecPercent: run.scoreRecPercent || 0, })); - // for zoomed in mode return all points - if (full === 1) { - return allPoints; - } - - // always return top 100 points, and reduce by 0.5% grouping for other to make render easier - const first50 = allPoints.slice(0, 100); - const other = allPoints.slice(100, allPoints.length); - return [ - ...first50, - ...uniqBy( - other, - ({ x }) => Math.floor((200 * x) / hhf), // 0.5% grouping for graph points reduction - ), - ]; + return allPoints; }); }; diff --git a/api/src/worker/uploads.ts b/api/src/worker/uploads.ts index a7d539a9..edfceb47 100644 --- a/api/src/worker/uploads.ts +++ b/api/src/worker/uploads.ts @@ -384,7 +384,7 @@ const scsaMatchInfo = async matchInfo => { return EmptySingleMatchResultFactory(match); }; -const badCharsRegExp = /[:\t.,\-_+=!?]/gi; +const badCharsRegExp = /[\s:\t.,\-_+=!?]/gi; const memberNumberFromMatchDefShooter = (s, mustHaveMemberNumbers) => { if (mustHaveMemberNumbers) { return s.sh_id; diff --git a/shared/constants/divisions.ts b/shared/constants/divisions.ts index 19afb066..65342f8f 100644 --- a/shared/constants/divisions.ts +++ b/shared/constants/divisions.ts @@ -5,6 +5,18 @@ export const pairToDivision = pair => pair.split(":")[1]; // TODO: add allDivisions for multisport, use uspsaDivisions for USPSA only export const uspsaDivisions = divisionsFromJson.divisions; +export const pcsl2gunDivisions = [ + { + id: "0", + long_name: "Competition", + short_name: "competition", + }, + { + id: "00", + long_name: "Practical", + short_name: "practical", + }, +]; /** ["opn", "ltd", "l10", "prod", "rev", "ss", "co", "lo", "pcc"] */ export const divShortNames = uspsaDivisions.map(c => c.short_name.toLowerCase()); export const uspsaDivShortNames = divShortNames; diff --git a/web/src/components/DivisionNavigation.jsx b/web/src/components/DivisionNavigation.jsx index d71fc834..e6b6fca4 100644 --- a/web/src/components/DivisionNavigation.jsx +++ b/web/src/components/DivisionNavigation.jsx @@ -6,7 +6,7 @@ import { useParams } from "react-router-dom"; import { divisionChangeMap, hfuDivisions, - uspsaDivisions, + pcsl2gunDivisions as uspsaDivisions, sportName, scsaDivisions, } from "../../../shared/constants/divisions"; @@ -14,6 +14,7 @@ import features from "../../../shared/features"; import usePreviousEffect from "../utils/usePreviousEffect"; const SportSelector = ({ sportCode, setSportCode, uspsaOnly, disableSCSA, hideSCSA }) => { + return null; const menu = useRef(null); const items = [ { diff --git a/web/src/components/RunsTable.jsx b/web/src/components/RunsTable.jsx index b1a14a72..2f9889c9 100644 --- a/web/src/components/RunsTable.jsx +++ b/web/src/components/RunsTable.jsx @@ -141,7 +141,6 @@ const RunsTable = ({ classifier, division, clubs, onShooterSelection }) => { stripedRows lazy value={data ?? []} - tableStyle={{ minWidth: "50rem" }} {...sortProps} {...pageProps} paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink" @@ -205,59 +204,6 @@ const RunsTable = ({ classifier, division, clubs, onShooterSelection }) => { headerTooltip="What classifier percentage this score SHOULD earn if Recommended HHFs are used." headerTooltipOptions={headerTooltipOptions} /> -