chat-bet-parse
Version:
TypeScript package for parsing sports betting contract text into structured data types compatible with SQL Server stored procedures
1,294 lines (1,289 loc) • 132 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
AmbiguousContractError: () => AmbiguousContractError,
ChatBetGradingClient: () => ChatBetGradingClient,
ChatBetParseError: () => ChatBetParseError,
ContractMappingError: () => ContractMappingError,
GradingConnectionError: () => GradingConnectionError,
GradingDataError: () => GradingDataError,
GradingError: () => GradingError,
GradingQueryError: () => GradingQueryError,
InvalidChatFormatError: () => InvalidChatFormatError,
InvalidContractTypeError: () => InvalidContractTypeError,
InvalidDateError: () => InvalidDateError,
InvalidGameNumberError: () => InvalidGameNumberError,
InvalidKeywordSyntaxError: () => InvalidKeywordSyntaxError,
InvalidKeywordValueError: () => InvalidKeywordValueError,
InvalidLineValueError: () => InvalidLineValueError,
InvalidParlayLegError: () => InvalidParlayLegError,
InvalidParlayStructureError: () => InvalidParlayStructureError,
InvalidParlayToWinError: () => InvalidParlayToWinError,
InvalidPeriodFormatError: () => InvalidPeriodFormatError,
InvalidPriceFormatError: () => InvalidPriceFormatError,
InvalidPropFormatError: () => InvalidPropFormatError,
InvalidRiskTypeError: () => InvalidRiskTypeError,
InvalidRotationNumberError: () => InvalidRotationNumberError,
InvalidRoundRobinLegError: () => InvalidRoundRobinLegError,
InvalidRoundRobinToWinError: () => InvalidRoundRobinToWinError,
InvalidSeriesLengthError: () => InvalidSeriesLengthError,
InvalidSizeFormatError: () => InvalidSizeFormatError,
InvalidTeamFormatError: () => InvalidTeamFormatError,
InvalidWriteinDateError: () => InvalidWriteinDateError,
InvalidWriteinDescriptionError: () => InvalidWriteinDescriptionError,
InvalidWriteinFormatError: () => InvalidWriteinFormatError,
LegCountMismatchError: () => LegCountMismatchError,
MissingNcrNotationError: () => MissingNcrNotationError,
MissingRiskTypeError: () => MissingRiskTypeError,
MissingSizeForFillError: () => MissingSizeForFillError,
UnknownKeywordError: () => UnknownKeywordError,
UnrecognizedChatPrefixError: () => UnrecognizedChatPrefixError,
americanToDecimalOdds: () => americanToDecimalOdds,
calculateCombination: () => calculateCombination,
calculateParlayFairToWin: () => calculateParlayFairToWin,
calculateRiskAndToWin: () => calculateRiskAndToWin,
calculateRiskFromToWin: () => calculateRiskFromToWin,
calculateRoundRobinFairToWin: () => calculateRoundRobinFairToWin,
calculateToWinFromRisk: () => calculateToWinFromRisk,
calculateTotalParlays: () => calculateTotalParlays,
createGradingClient: () => createGradingClient,
createGradingClientWithConfig: () => createGradingClientWithConfig,
createPositionError: () => createPositionError,
default: () => parseChat,
detectContestantType: () => detectContestantType,
detectPropType: () => detectPropType,
extractContestantAndProp: () => extractContestantAndProp,
inferSportAndLeague: () => inferSportAndLeague,
isFill: () => isFill,
isHandicapLine: () => isHandicapLine,
isHandicapML: () => isHandicapML,
isOrder: () => isOrder,
isParlay: () => isParlay,
isPropOU: () => isPropOU,
isPropYN: () => isPropYN,
isRoundRobin: () => isRoundRobin,
isSeries: () => isSeries,
isStraight: () => isStraight,
isTotalPoints: () => isTotalPoints,
isTotalPointsContestant: () => isTotalPointsContestant,
isWritein: () => isWritein,
knownLeagues: () => knownLeagues,
knownSports: () => knownSports,
leagueSportMap: () => leagueSportMap,
mapParseResultToContractLegSpec: () => mapParseResultToContractLegSpec,
mapParseResultToSqlParameters: () => mapParseResultToSqlParameters,
matchesIgnoreCase: () => matchesIgnoreCase,
parseChat: () => parseChat,
parseChatFill: () => parseChatFill,
parseChatOrder: () => parseChatOrder,
parseFillSize: () => parseFillSize,
parseGameNumber: () => parseGameNumber,
parseKeywords: () => parseKeywords,
parseLine: () => parseLine,
parseOrderSize: () => parseOrderSize,
parseOverUnder: () => parseOverUnder,
parseParlayKeywords: () => parseParlayKeywords,
parseParlaySize: () => parseParlaySize,
parsePeriod: () => parsePeriod,
parsePlayerWithTeam: () => parsePlayerWithTeam,
parsePrice: () => parsePrice,
parseRotationNumber: () => parseRotationNumber,
parseRoundRobinSize: () => parseRoundRobinSize,
parseStraightSize: () => parseStraightSize,
parseTeam: () => parseTeam,
parseTeams: () => parseTeams,
parseWriteinDate: () => parseWriteinDate,
validateContractLegSpec: () => validateContractLegSpec,
validateGradingParameters: () => validateGradingParameters,
validatePropFormat: () => validatePropFormat,
validateWriteinDescription: () => validateWriteinDescription
});
module.exports = __toCommonJS(index_exports);
// src/types/index.ts
var leagueSportMap = {
MLB: "Baseball",
WNBA: "Basketball",
CBK: "Basketball",
CBB: "Basketball",
NBA: "Basketball",
CFL: "Football",
CFB: "Football",
NFL: "Football",
UFL: "Football",
FCS: "Football",
LPGA: "Golf",
PGA: "Golf",
NHL: "Hockey",
UFC: "MMA",
WTA: "Tennis",
ATP: "Tennis"
};
var knownLeagues = new Set(Object.keys(leagueSportMap));
var knownSports = new Set(Object.values(leagueSportMap));
function isOrder(result) {
return result.chatType === "order";
}
function isFill(result) {
return result.chatType === "fill";
}
function isStraight(result) {
return result.betType === "straight";
}
function isParlay(result) {
return result.betType === "parlay";
}
function isRoundRobin(result) {
return result.betType === "roundRobin";
}
function isTotalPoints(contract) {
return "ContractSportCompetitionMatchType" in contract && contract.ContractSportCompetitionMatchType === "TotalPoints" && !contract.HasContestant;
}
function isTotalPointsContestant(contract) {
return "ContractSportCompetitionMatchType" in contract && contract.ContractSportCompetitionMatchType === "TotalPoints" && contract.HasContestant;
}
function isHandicapML(contract) {
return "ContractSportCompetitionMatchType" in contract && contract.ContractSportCompetitionMatchType === "Handicap" && contract.HasContestant && !contract.HasLine;
}
function isHandicapLine(contract) {
return "ContractSportCompetitionMatchType" in contract && contract.ContractSportCompetitionMatchType === "Handicap" && contract.HasContestant && contract.HasLine;
}
function isPropOU(contract) {
return "ContractSportCompetitionMatchType" in contract && contract.ContractSportCompetitionMatchType === "Prop" && contract.HasContestant && contract.HasLine;
}
function isPropYN(contract) {
return "ContractSportCompetitionMatchType" in contract && contract.ContractSportCompetitionMatchType === "Prop" && contract.HasContestant && !contract.HasLine;
}
function isSeries(contract) {
return "SeriesLength" in contract;
}
function isWritein(contract) {
return "EventDate" in contract && "Description" in contract;
}
// src/errors/index.ts
var ChatBetParseError = class extends Error {
constructor(message, rawInput, position) {
super(message.trimEnd());
this.name = "ChatBetParseError";
this.rawInput = rawInput;
this.position = position;
}
};
var InvalidChatFormatError = class extends ChatBetParseError {
constructor(rawInput, reason) {
super(`Invalid chat format: ${reason}. Input: "${rawInput}"`, rawInput);
this.name = "InvalidChatFormatError";
}
};
var InvalidContractTypeError = class extends ChatBetParseError {
constructor(rawInput, contractPortion) {
super(
`Unable to determine contract type from: "${contractPortion}". Input: "${rawInput}"`,
rawInput
);
this.name = "InvalidContractTypeError";
}
};
var InvalidPriceFormatError = class extends ChatBetParseError {
constructor(rawInput, priceStr) {
super(
`Invalid USA price format: "${priceStr}". Expected format: +150, -110, -115.5, ev, or even. Input: "${rawInput}"`,
rawInput
);
this.name = "InvalidPriceFormatError";
}
};
var InvalidSizeFormatError = class extends ChatBetParseError {
constructor(rawInput, sizeStr, expectedFormat) {
super(
`Invalid size format: "${sizeStr}". Expected: ${expectedFormat}. Input: "${rawInput}"`,
rawInput
);
this.name = "InvalidSizeFormatError";
}
};
var InvalidLineValueError = class extends ChatBetParseError {
constructor(rawInput, line) {
super(
`Invalid line value: ${line}. Line must be divisible by 0.5. Input: "${rawInput}"`,
rawInput
);
this.name = "InvalidLineValueError";
}
};
var InvalidTeamFormatError = class extends ChatBetParseError {
constructor(rawInput, teamStr, reason) {
super(`Invalid team format: "${teamStr}". ${reason}. Input: "${rawInput}"`, rawInput);
this.name = "InvalidTeamFormatError";
}
};
var InvalidPeriodFormatError = class extends ChatBetParseError {
constructor(rawInput, periodStr) {
super(
`Invalid period format: "${periodStr}". Expected formats: 1st inning, F5, 1H, Q1, etc. Input: "${rawInput}"`,
rawInput
);
this.name = "InvalidPeriodFormatError";
}
};
var InvalidGameNumberError = class extends ChatBetParseError {
constructor(rawInput, gameStr) {
super(
`Invalid game number format: "${gameStr}". Expected formats: G2, GM1, #2, etc. Input: "${rawInput}"`,
rawInput
);
this.name = "InvalidGameNumberError";
}
};
var InvalidRotationNumberError = class extends ChatBetParseError {
constructor(rawInput, rotationStr) {
super(
`Invalid rotation number: "${rotationStr}". Must be a positive integer. Input: "${rawInput}"`,
rawInput
);
this.name = "InvalidRotationNumberError";
}
};
var InvalidPropFormatError = class extends ChatBetParseError {
constructor(rawInput, propStr, availableProps) {
super(
`Invalid prop format: "${propStr}". Available props: ${availableProps.join(", ")}. Input: "${rawInput}"`,
rawInput
);
this.name = "InvalidPropFormatError";
}
};
var InvalidSeriesLengthError = class extends ChatBetParseError {
constructor(rawInput, lengthStr) {
super(
`Invalid series length: "${lengthStr}". Must be a positive integer. Input: "${rawInput}"`,
rawInput
);
this.name = "InvalidSeriesLengthError";
}
};
var MissingSizeForFillError = class extends ChatBetParseError {
constructor(rawInput) {
super(`Fill (YG/YGP/YGRR) messages require a size`, rawInput);
this.name = "MissingSizeForFillError";
}
};
var UnrecognizedChatPrefixError = class extends ChatBetParseError {
constructor(rawInput, prefix) {
super(
`Unrecognized chat prefix: "${prefix}". Chat must be either a chat order (start with "IW" for "i want") or a chat fill (start with "YG", for "you got"). Input: "${rawInput}"`,
rawInput
);
this.name = "UnrecognizedChatPrefixError";
}
};
var AmbiguousContractError = class extends ChatBetParseError {
constructor(rawInput, possibleTypes) {
super(
`Ambiguous contract type. Could be: ${possibleTypes.join(", ")}. Please be more specific. Input: "${rawInput}"`,
rawInput
);
this.name = "AmbiguousContractError";
}
};
var InvalidDateError = class extends ChatBetParseError {
constructor(rawInput, dateStr, reason) {
super(
`Invalid date: "${dateStr}". ${reason}. Expected formats: YYYY-MM-DD, MM/DD/YYYY, YYYY/MM/DD, MM-DD-YYYY, or equivalents without year. Input: "${rawInput}"`,
rawInput
);
this.name = "InvalidDateError";
}
};
var InvalidWriteinDateError = class extends ChatBetParseError {
constructor(rawInput, dateStr, reason) {
super(
`Invalid writein date: "${dateStr}". ${reason}. Expected formats: YYYY-MM-DD, MM/DD/YYYY, YYYY/MM/DD, MM-DD-YYYY, or equivalents without year. Input: "${rawInput}"`,
rawInput
);
this.name = "InvalidWriteinDateError";
}
};
var InvalidWriteinDescriptionError = class extends ChatBetParseError {
constructor(rawInput, description, reason) {
super(
`Invalid writein description: "${description}". ${reason}. Input: "${rawInput}"`,
rawInput
);
this.name = "InvalidWriteinDescriptionError";
}
};
var InvalidWriteinFormatError = class extends ChatBetParseError {
constructor(rawInput, reason) {
super(
`Invalid writein format: ${reason}. Expected format: "IW/YG writein DATE DESCRIPTION [@ price] [= size]". Input: "${rawInput}"`,
rawInput
);
this.name = "InvalidWriteinFormatError";
}
};
var InvalidKeywordSyntaxError = class extends ChatBetParseError {
constructor(rawInput, _keyword, message) {
super(message, rawInput);
this.name = "InvalidKeywordSyntaxError";
}
};
var InvalidKeywordValueError = class extends ChatBetParseError {
constructor(rawInput, _keyword, _value, message) {
super(message, rawInput);
this.name = "InvalidKeywordValueError";
}
};
var UnknownKeywordError = class extends ChatBetParseError {
constructor(rawInput, keyword) {
super(`Unknown keyword: ${keyword}`, rawInput);
this.name = "UnknownKeywordError";
}
};
var InvalidParlayStructureError = class extends ChatBetParseError {
constructor(rawInput, message) {
super(message, rawInput);
this.name = "InvalidParlayStructureError";
}
};
var InvalidParlayLegError = class extends ChatBetParseError {
constructor(rawInput, legNumber, message) {
super(`Leg ${legNumber}: ${message}`, rawInput);
this.name = "InvalidParlayLegError";
}
};
var InvalidParlayToWinError = class extends ChatBetParseError {
constructor(rawInput, message) {
super(message, rawInput);
this.name = "InvalidParlayToWinError";
}
};
var MissingNcrNotationError = class extends ChatBetParseError {
constructor(rawInput, message = "Round robin requires nCr notation") {
super(message, rawInput);
this.name = "MissingNcrNotationError";
}
};
var LegCountMismatchError = class extends ChatBetParseError {
constructor(rawInput, expected, actual) {
super(`Expected ${expected} legs from nCr notation, but found ${actual}`, rawInput);
this.name = "LegCountMismatchError";
}
};
var MissingRiskTypeError = class extends ChatBetParseError {
constructor(rawInput) {
super('Round robin requires risk type: "per" or "total"', rawInput);
this.name = "MissingRiskTypeError";
}
};
var InvalidRiskTypeError = class extends ChatBetParseError {
constructor(rawInput, value) {
super(`Invalid risk type: must be "per" or "total", got "${value}"`, rawInput);
this.name = "InvalidRiskTypeError";
}
};
var InvalidRoundRobinLegError = class extends ChatBetParseError {
constructor(rawInput, legNumber, message) {
super(`Leg ${legNumber}: ${message}`, rawInput);
this.name = "InvalidRoundRobinLegError";
}
};
var InvalidRoundRobinToWinError = class extends ChatBetParseError {
constructor(rawInput, message) {
super(message, rawInput);
this.name = "InvalidRoundRobinToWinError";
}
};
function createPositionError(ErrorClass, rawInput, position, ...args) {
const error = new ErrorClass(rawInput, ...args);
error.position = position;
return error;
}
// src/parsers/ncr.ts
var InvalidNcrNotationError = class extends ChatBetParseError {
constructor(rawInput, message) {
super(message, rawInput);
this.name = "InvalidNcrNotationError";
}
};
function parseNcrNotation(notation, rawInput) {
const trimmed = notation.trim();
if (/[cC].*,/.test(trimmed)) {
throw new InvalidNcrNotationError(rawInput, "Comma-separated parlay sizes not supported");
}
const match = trimmed.match(/^(-?[\d.]+|[A-Za-z]+)[cC](-?[\d.]+|[A-Za-z]+|[^-\s]+)(-+)?$/i);
if (!match) {
throw new InvalidNcrNotationError(rawInput, "Invalid nCr notation format");
}
const totalLegsStr = match[1];
const parlaySizeStr = match[2];
const minusModifier = match[3] || "";
if (minusModifier.length > 1) {
throw new InvalidNcrNotationError(rawInput, "Invalid at-most modifier");
}
const isAtMost = minusModifier === "-";
if (!/^-?\d+(?:\.\d+)?$/.test(totalLegsStr)) {
throw new InvalidNcrNotationError(rawInput, "Total legs must be a number");
}
if (!/^-?\d+(?:\.\d+)?$/.test(parlaySizeStr)) {
throw new InvalidNcrNotationError(rawInput, "Parlay size must be a number");
}
if (totalLegsStr.includes(".")) {
throw new InvalidNcrNotationError(rawInput, "Total legs must be an integer");
}
if (parlaySizeStr.includes(".")) {
throw new InvalidNcrNotationError(rawInput, "Parlay size must be an integer");
}
const totalLegs = parseInt(totalLegsStr, 10);
const parlaySize = parseInt(parlaySizeStr, 10);
if (totalLegs < 0) {
throw new InvalidNcrNotationError(rawInput, "Total legs must be positive");
}
if (parlaySize < 0) {
throw new InvalidNcrNotationError(rawInput, "Parlay size must be positive");
}
if (totalLegs < 3) {
throw new InvalidNcrNotationError(rawInput, "Total legs must be at least 3");
}
if (parlaySize < 2) {
throw new InvalidNcrNotationError(rawInput, "Parlay size must be at least 2");
}
if (parlaySize >= totalLegs) {
throw new InvalidNcrNotationError(rawInput, "Parlay size must be less than total legs");
}
return { totalLegs, parlaySize, isAtMost };
}
// src/parsers/utils.ts
function parsePrice(priceStr, rawInput) {
const cleaned = priceStr.trim();
if (cleaned.toLowerCase() === "ev" || cleaned.toLowerCase() === "even") {
return 100;
}
const match = cleaned.match(/^([+-])(\d+(?:\.\d+)?)$/);
if (!match) {
throw new InvalidPriceFormatError(rawInput, priceStr);
}
const sign = match[1];
const value = parseFloat(match[2]);
return sign === "+" ? value : -value;
}
function calculateRiskAndToWin(price, size) {
if (price >= 100) {
const risk = size;
const toWin = Math.round(size * price / 100 * 100) / 100;
return { risk, toWin };
} else if (price <= -100) {
const absOdds = Math.abs(price);
const risk = Math.round(size * absOdds / 100 * 100) / 100;
const toWin = size;
return { risk, toWin };
} else {
throw new Error(`Invalid price ${price}: must be >= 100 or <= -100`);
}
}
function calculateToWinFromRisk(price, risk) {
if (price >= 100) {
return Math.round(risk * price / 100 * 100) / 100;
} else if (price <= -100) {
const absOdds = Math.abs(price);
return Math.round(risk / (absOdds / 100) * 100) / 100;
} else {
throw new Error(`Invalid price ${price}: must be >= 100 or <= -100`);
}
}
function calculateRiskFromToWin(price, toWin) {
if (price >= 100) {
return Math.round(toWin / (price / 100) * 100) / 100;
} else if (price <= -100) {
const absOdds = Math.abs(price);
return Math.round(toWin * (absOdds / 100) * 100) / 100;
} else {
throw new Error(`Invalid price ${price}: must be >= 100 or <= -100`);
}
}
function validateCommaPlacement(str) {
if (!str.includes(",")) {
return true;
}
const numPart = str.startsWith("$") ? str.substring(1) : str;
const withoutK = numPart.toLowerCase().endsWith("k") ? numPart.slice(0, -1) : numPart;
const commaPattern = /^\d{1,3}(,\d{3})+$/;
return commaPattern.test(withoutK);
}
function parseSize(sizeStr, rawInput, config) {
const cleaned = sizeStr.trim();
if (!validateCommaPlacement(cleaned)) {
throw new InvalidSizeFormatError(
rawInput,
sizeStr,
`Invalid comma placement in number: "${sizeStr}". Use American thousands format (e.g., 1,234 or 12,345)`
);
}
const withoutCommas = cleaned.replace(/,/g, "");
if (withoutCommas.startsWith("$") && withoutCommas.toLowerCase().endsWith("k")) {
const numPart = withoutCommas.substring(1, withoutCommas.length - 1);
const value2 = parseFloat(numPart);
if (isNaN(value2) || value2 < 0) {
throw new InvalidSizeFormatError(
rawInput,
sizeStr,
"positive dollar amount with k like $11k or $2.5k"
);
}
return { value: value2 * 1e3, format: "k_notation" };
}
if (withoutCommas.startsWith("$")) {
const value2 = parseFloat(withoutCommas.substring(1));
if (isNaN(value2) || value2 < 0) {
throw new InvalidSizeFormatError(
rawInput,
sizeStr,
"positive dollar amount like $100 or $2.50"
);
}
return { value: value2, format: "dollar" };
}
if (withoutCommas.toLowerCase().endsWith("k")) {
const value2 = parseFloat(withoutCommas.slice(0, -1));
if (isNaN(value2) || value2 < 0) {
throw new InvalidSizeFormatError(rawInput, sizeStr, "positive number with k like 4k or 2.5k");
}
return { value: value2 * 1e3, format: "k_notation" };
}
const value = parseFloat(withoutCommas);
if (isNaN(value) || value < 0) {
const errorHint = config.interpretation === "decimal_thousands" ? "positive number like 100 (=$100) or 2.5 (=$2500)" : "positive decimal number like 2.0 or 0.50";
throw new InvalidSizeFormatError(rawInput, sizeStr, errorHint);
}
if (config.interpretation === "decimal_thousands" && withoutCommas.includes(".")) {
return { value: value * 1e3, format: "decimal_thousands" };
} else if (config.interpretation === "decimal_thousands") {
return { value, format: "plain_number" };
} else {
return { value, format: "unit" };
}
}
function parseOrderSize(sizeStr, rawInput) {
return parseSize(sizeStr, rawInput, { interpretation: "unit" });
}
function parseFillSize(sizeStr, rawInput) {
return parseSize(sizeStr, rawInput, { interpretation: "decimal_thousands" });
}
function parseStraightSize(sizeText, rawInput, interpretation) {
if (!sizeText.startsWith("=")) {
throw new InvalidSizeFormatError(rawInput, sizeText, "Size must start with =");
}
const text = sizeText.slice(1).trim();
const twMatch = text.match(/^([$\d.,]+k?)\s+(?:tw|to\s+win)\s+([$\d.,]+k?)$/i);
if (twMatch) {
const riskParsed = parseSize(twMatch[1], rawInput, { interpretation });
const toWinParsed = parseSize(twMatch[2], rawInput, { interpretation });
return { risk: riskParsed.value, toWin: toWinParsed.value };
}
const tpMatch = text.match(/^([$\d.,]+k?)\s+(?:tp|to\s+pay)\s+([$\d.,]+k?)$/i);
if (tpMatch) {
const riskParsed = parseSize(tpMatch[1], rawInput, { interpretation });
const toPayParsed = parseSize(tpMatch[2], rawInput, { interpretation });
const toWin = toPayParsed.value - riskParsed.value;
if (toWin < 0) {
throw new InvalidSizeFormatError(
rawInput,
sizeText,
"To-pay amount must be greater than risk amount"
);
}
return { risk: riskParsed.value, toWin: Math.round(toWin * 100) / 100 };
}
const riskMatch = text.match(/^risk\s+([$\d.,]+k?)$/i);
if (riskMatch) {
const riskParsed = parseSize(riskMatch[1], rawInput, { interpretation });
return { risk: riskParsed.value };
}
const toWinMatch = text.match(/^towin\s+([$\d.,]+k?)$/i);
if (toWinMatch) {
const toWinParsed = parseSize(toWinMatch[1], rawInput, { interpretation });
return { toWin: toWinParsed.value };
}
const sizeMatch = text.match(/^([$\d.,]+k?)$/);
if (sizeMatch) {
const sizeParsed = parseSize(sizeMatch[1], rawInput, { interpretation });
return { size: sizeParsed.value };
}
throw new InvalidSizeFormatError(
rawInput,
sizeText,
'Format: "= $100", "= $110 tw $100", "= $120 tp $220", "= risk $110", or "= towin $150"'
);
}
function parseLine(lineStr, rawInput) {
const value = parseFloat(lineStr);
if (isNaN(value)) {
throw new InvalidLineValueError(rawInput, value);
}
if (value * 2 % 1 !== 0) {
throw new InvalidLineValueError(rawInput, value);
}
return value;
}
function parsePeriod(periodStr, rawInput) {
const cleaned = periodStr.toLowerCase().trim();
if (!cleaned || cleaned === "fg" || cleaned === "full game") {
return { PeriodTypeCode: "M", PeriodNumber: 0 };
}
if (cleaned === "f5" || cleaned === "h1" || cleaned === "1h" || cleaned === "first half" || cleaned === "1st half" || cleaned === "first h" || cleaned === "1st h" || cleaned === "first five" || cleaned === "1st five" || cleaned === "first 5" || cleaned === "1st 5") {
return { PeriodTypeCode: "H", PeriodNumber: 1 };
}
if (cleaned === "f3") {
return { PeriodTypeCode: "H", PeriodNumber: 13 };
}
if (cleaned === "f7") {
return { PeriodTypeCode: "H", PeriodNumber: 17 };
}
if (cleaned === "h2" || cleaned === "2h" || cleaned === "second half" || cleaned === "2nd half" || cleaned === "second h" || cleaned === "2nd h") {
return { PeriodTypeCode: "H", PeriodNumber: 2 };
}
const quarterMatch = cleaned.match(/^(?:(\d+)(?:st|nd|rd|th)?\s*quarter?|q(\d+)|(\d+)q)$/);
if (quarterMatch) {
const qNum = parseInt(quarterMatch[1] || quarterMatch[2] || quarterMatch[3]);
if (qNum >= 1 && qNum <= 4) {
return { PeriodTypeCode: "Q", PeriodNumber: qNum };
}
}
const inningMatch = cleaned.match(/^(?:(\d+)(?:st|nd|rd|th)?\s*inning?|i(\d+)|(\d+)i)$/);
if (inningMatch) {
const iNum = parseInt(inningMatch[1] || inningMatch[2] || inningMatch[3]);
if (iNum >= 1 && iNum <= 15) {
return { PeriodTypeCode: "I", PeriodNumber: iNum };
}
}
const periodMatch = cleaned.match(/^(?:(\d+)(?:st|nd|rd|th)?\s*period?|p(\d+)|(\d+)p)$/);
if (periodMatch) {
const pNum = parseInt(periodMatch[1] || periodMatch[2] || periodMatch[3]);
if (pNum >= 1 && pNum <= 4) {
return { PeriodTypeCode: "P", PeriodNumber: pNum };
}
}
throw new InvalidPeriodFormatError(rawInput, periodStr);
}
function parseGameNumber(gameStr, rawInput) {
const cleaned = gameStr.toLowerCase().trim();
const match = cleaned.match(/^(?:(?:game|gm|g)\s*(\d+)|#\s*(\d+))$/);
if (!match) {
throw new InvalidGameNumberError(rawInput, gameStr);
}
const gameNum = parseInt(match[1] || match[2]);
if (gameNum < 1 || gameNum > 10) {
throw new InvalidGameNumberError(rawInput, gameStr);
}
return gameNum;
}
function parseRotationNumber(rotationStr, rawInput) {
const value = parseInt(rotationStr.trim());
if (isNaN(value) || value < 1 || value > 9999) {
throw new InvalidRotationNumberError(rawInput, rotationStr);
}
return value;
}
function parseTeam(teamStr, rawInput) {
const cleaned = teamStr.trim();
if (!cleaned) {
throw new InvalidTeamFormatError(rawInput, teamStr, "Team name cannot be empty");
}
if (!/^[a-zA-Z0-9\s&\-.']+$/.test(cleaned)) {
throw new InvalidTeamFormatError(rawInput, teamStr, "Team name contains invalid characters");
}
if (cleaned.length > 50) {
throw new InvalidTeamFormatError(rawInput, teamStr, "Team name too long (max 50 characters)");
}
return cleaned;
}
function detectContestantType(contestant) {
if (/^[A-Z]\.\s+[A-Za-z]+/.test(contestant)) {
return "Individual";
}
return void 0;
}
function parseTeams(teamsStr, rawInput) {
const parts = teamsStr.split("/");
if (parts.length === 1) {
return { team1: parseTeam(parts[0], rawInput) };
} else if (parts.length === 2) {
const team1 = parseTeam(parts[0], rawInput);
const team2 = parseTeam(parts[1], rawInput);
if (team1 === team2) {
throw new InvalidTeamFormatError(
rawInput,
teamsStr,
`Team1 and Team2 cannot be the same: "${team1}"`
);
}
return {
team1,
team2
};
} else {
throw new InvalidTeamFormatError(rawInput, teamsStr, 'Too many "/" separators');
}
}
function inferSportAndLeague(rotationNumber, explicitLeague, explicitSport) {
let sport = explicitSport;
let league = explicitLeague;
if (explicitLeague && explicitSport && leagueSportMap[explicitLeague] !== explicitSport) {
throw new Error("Conflicting explicit league and sport specifications");
}
if (explicitLeague && !sport) {
sport = leagueSportMap[explicitLeague];
}
if (league === "FCS") {
league = "CFB";
}
if (league === "CBB") {
league = "CBK";
}
if ((!sport || !league) && rotationNumber) {
if (rotationNumber >= 100 && rotationNumber < 499) {
const inferredSport = "Football";
if (!sport) sport = inferredSport;
return { sport: "Football" };
}
if (rotationNumber >= 500 && rotationNumber < 800) {
const inferredSport = "Basketball";
if (!sport) sport = inferredSport;
return { sport: "Basketball" };
}
if (rotationNumber >= 800 && rotationNumber < 900 || rotationNumber >= 9900 && rotationNumber < 1e4) {
const inferredSport = "Baseball";
if (!sport) sport = inferredSport;
return { sport: "Baseball" };
}
}
return { sport, league };
}
function matchesIgnoreCase(text, pattern) {
return text.toLowerCase().includes(pattern.toLowerCase());
}
function parsePlayerWithTeam(text) {
const trimmed = text.trim();
const match = trimmed.match(/^(.+?)\s*\(([A-Z]{2,4})\)$/);
if (match) {
return {
player: match[1].trim(),
team: match[2].trim()
};
}
return { player: trimmed };
}
function parseOverUnder(ouStr, rawInput) {
const match = ouStr.toLowerCase().match(/^([ou])(.+)$/);
if (!match) {
throw new InvalidLineValueError(rawInput, parseFloat(ouStr));
}
const isOver = match[1] === "o";
const lineAndPrice = match[2];
const attachedPriceMatch = lineAndPrice.match(/^(\d+(?:\.\d+)?)([+-]\d+(?:\.\d+)?)$/);
if (attachedPriceMatch) {
const line2 = parseLine(attachedPriceMatch[1], rawInput);
const priceStr = attachedPriceMatch[2];
const attachedPrice = parsePrice(priceStr, rawInput);
return { isOver, line: line2, attachedPrice };
}
const line = parseLine(lineAndPrice, rawInput);
return { isOver, line };
}
var PROP_TYPE_MAP = {
// ============================================================================
// BASEBALL PROPS
// ============================================================================
// Baseball - Hitting (Individual)
hits: { standardName: "Hits", category: "PropOU", contestantType: "Individual" },
"total bases": { standardName: "TotalBases", category: "PropOU", contestantType: "Individual" },
singles: { standardName: "Singles", category: "PropOU", contestantType: "Individual" },
doubles: { standardName: "Doubles", category: "PropOU", contestantType: "Individual" },
triples: { standardName: "Triples", category: "PropOU", contestantType: "Individual" },
"home runs": { standardName: "HomeRuns", category: "PropOU", contestantType: "Individual" },
hrs: { standardName: "HomeRuns", category: "PropOU", contestantType: "Individual" },
"runs scored": { standardName: "RunsScored", category: "PropOU", contestantType: "Individual" },
// Note: "runs" is ambiguous (appears in game totals like "o0.5 runs"), so it's excluded
rbi: { standardName: "RBI", category: "PropOU", contestantType: "Individual" },
rbis: { standardName: "RBI", category: "PropOU", contestantType: "Individual" },
walks: { standardName: "Walks", category: "PropOU", contestantType: "Individual" },
bb: { standardName: "Walks", category: "PropOU", contestantType: "Individual" },
"stolen bases": { standardName: "StolenBases", category: "PropOU", contestantType: "Individual" },
sb: { standardName: "StolenBases", category: "PropOU", contestantType: "Individual" },
// Baseball - Pitching (Individual)
ks: { standardName: "Ks", category: "PropOU", contestantType: "Individual" },
strikeouts: { standardName: "Ks", category: "PropOU", contestantType: "Individual" },
"earned runs": { standardName: "EarnedRuns", category: "PropOU", contestantType: "Individual" },
er: { standardName: "EarnedRuns", category: "PropOU", contestantType: "Individual" },
"hits allowed": { standardName: "HitsAllowed", category: "PropOU", contestantType: "Individual" },
"walks allowed": {
standardName: "WalksAllowed",
category: "PropOU",
contestantType: "Individual"
},
"innings pitched": {
standardName: "InningsPitched",
category: "PropOU",
contestantType: "Individual"
},
ip: { standardName: "InningsPitched", category: "PropOU", contestantType: "Individual" },
"pitches thrown": {
standardName: "PitchesThrown",
category: "PropOU",
contestantType: "Individual"
},
pitches: { standardName: "PitchesThrown", category: "PropOU", contestantType: "Individual" },
"outs recorded": {
standardName: "OutsRecorded",
category: "PropOU",
contestantType: "Individual"
},
// Baseball - Yes/No (Individual)
"to record a hit": {
standardName: "ToRecordAHit",
category: "PropYN",
contestantType: "Individual"
},
"to get a hit": {
standardName: "ToRecordAHit",
category: "PropYN",
contestantType: "Individual"
},
"to hit a home run": {
standardName: "ToHitHomeRun",
category: "PropYN",
contestantType: "Individual"
},
"to hit a hr": { standardName: "ToHitHomeRun", category: "PropYN", contestantType: "Individual" },
"to steal a base": {
standardName: "ToStealBase",
category: "PropYN",
contestantType: "Individual"
},
"to record a win": {
standardName: "ToRecordWin",
category: "PropYN",
contestantType: "Individual"
},
// ============================================================================
// FOOTBALL PROPS
// ============================================================================
// Football - Passing (Individual)
"passing yards": {
standardName: "PassingYards",
category: "PropOU",
contestantType: "Individual"
},
passingyards: { standardName: "PassingYards", category: "PropOU", contestantType: "Individual" },
"pass yards": { standardName: "PassingYards", category: "PropOU", contestantType: "Individual" },
"pass yds": { standardName: "PassingYards", category: "PropOU", contestantType: "Individual" },
"passing tds": { standardName: "PassingTDs", category: "PropOU", contestantType: "Individual" },
"pass tds": { standardName: "PassingTDs", category: "PropOU", contestantType: "Individual" },
completions: { standardName: "Completions", category: "PropOU", contestantType: "Individual" },
"pass completions": {
standardName: "Completions",
category: "PropOU",
contestantType: "Individual"
},
"pass attempts": {
standardName: "PassAttempts",
category: "PropOU",
contestantType: "Individual"
},
interceptions: {
standardName: "Interceptions",
category: "PropOU",
contestantType: "Individual"
},
ints: { standardName: "Interceptions", category: "PropOU", contestantType: "Individual" },
"longest completion": {
standardName: "LongestCompletion",
category: "PropOU",
contestantType: "Individual"
},
// Football - Rushing (Individual)
"rushing yards": {
standardName: "RushingYards",
category: "PropOU",
contestantType: "Individual"
},
"rush yards": { standardName: "RushingYards", category: "PropOU", contestantType: "Individual" },
"rush yds": { standardName: "RushingYards", category: "PropOU", contestantType: "Individual" },
"rushing tds": { standardName: "RushingTDs", category: "PropOU", contestantType: "Individual" },
"rush tds": { standardName: "RushingTDs", category: "PropOU", contestantType: "Individual" },
"rush attempts": {
standardName: "RushAttempts",
category: "PropOU",
contestantType: "Individual"
},
carries: { standardName: "RushAttempts", category: "PropOU", contestantType: "Individual" },
"longest rush": { standardName: "LongestRush", category: "PropOU", contestantType: "Individual" },
// Football - Receiving (Individual)
"receiving yards": {
standardName: "ReceivingYards",
category: "PropOU",
contestantType: "Individual"
},
receivingyards: {
standardName: "ReceivingYards",
category: "PropOU",
contestantType: "Individual"
},
"rec yards": { standardName: "ReceivingYards", category: "PropOU", contestantType: "Individual" },
"rec yds": { standardName: "ReceivingYards", category: "PropOU", contestantType: "Individual" },
"receiving tds": {
standardName: "ReceivingTDs",
category: "PropOU",
contestantType: "Individual"
},
"rec tds": { standardName: "ReceivingTDs", category: "PropOU", contestantType: "Individual" },
receptions: { standardName: "Receptions", category: "PropOU", contestantType: "Individual" },
catches: { standardName: "Receptions", category: "PropOU", contestantType: "Individual" },
"longest reception": {
standardName: "LongestReception",
category: "PropOU",
contestantType: "Individual"
},
// Football - Defense/Special Teams (Individual)
tackles: { standardName: "Tackles", category: "PropOU", contestantType: "Individual" },
sacks: { standardName: "Sacks", category: "PropOU", contestantType: "Individual" },
"tackles and assists": {
standardName: "TacklesAndAssists",
category: "PropOU",
contestantType: "Individual"
},
"field goals made": {
standardName: "FieldGoalsMade",
category: "PropOU",
contestantType: "Individual"
},
"fgs made": { standardName: "FieldGoalsMade", category: "PropOU", contestantType: "Individual" },
"extra points": { standardName: "ExtraPoints", category: "PropOU", contestantType: "Individual" },
xp: { standardName: "ExtraPoints", category: "PropOU", contestantType: "Individual" },
"kicking points": {
standardName: "KickingPoints",
category: "PropOU",
contestantType: "Individual"
},
// Football - Yes/No (Individual)
"anytime td": { standardName: "AnytimeTD", category: "PropYN", contestantType: "Individual" },
"anytime touchdown": {
standardName: "AnytimeTD",
category: "PropYN",
contestantType: "Individual"
},
"to score a touchdown": {
standardName: "AnytimeTD",
category: "PropYN",
contestantType: "Individual"
},
"first td": { standardName: "FirstTD", category: "PropYN", contestantType: "Individual" },
"first touchdown": { standardName: "FirstTD", category: "PropYN", contestantType: "Individual" },
"last td": { standardName: "LastTD", category: "PropYN", contestantType: "Individual" },
"last touchdown": { standardName: "LastTD", category: "PropYN", contestantType: "Individual" },
"2+ tds": { standardName: "TwoOrMoreTDs", category: "PropYN", contestantType: "Individual" },
"3+ tds": { standardName: "ThreeOrMoreTDs", category: "PropYN", contestantType: "Individual" },
// Football - Team Stats (TeamLeague)
"team passing yards": {
standardName: "PassingYards",
category: "PropOU",
contestantType: "TeamLeague"
},
"team rushing yards": {
standardName: "TeamRushingYards",
category: "PropOU",
contestantType: "TeamLeague"
},
"team sacks": { standardName: "TeamSacks", category: "PropOU", contestantType: "TeamLeague" },
// ============================================================================
// BASKETBALL PROPS
// ============================================================================
// Basketball - Scoring (Individual)
points: { standardName: "Points", category: "PropOU", contestantType: "Individual" },
pts: { standardName: "Points", category: "PropOU", contestantType: "Individual" },
// Basketball - Rebounding (Individual)
rebounds: { standardName: "Rebounds", category: "PropOU", contestantType: "Individual" },
rebs: { standardName: "Rebounds", category: "PropOU", contestantType: "Individual" },
"total rebounds": { standardName: "Rebounds", category: "PropOU", contestantType: "Individual" },
// Basketball - Assists (Individual)
assists: { standardName: "Assists", category: "PropOU", contestantType: "Individual" },
ast: { standardName: "Assists", category: "PropOU", contestantType: "Individual" },
asts: { standardName: "Assists", category: "PropOU", contestantType: "Individual" },
// Basketball - Defense (Individual)
steals: { standardName: "Steals", category: "PropOU", contestantType: "Individual" },
stls: { standardName: "Steals", category: "PropOU", contestantType: "Individual" },
blocks: { standardName: "Blocks", category: "PropOU", contestantType: "Individual" },
blks: { standardName: "Blocks", category: "PropOU", contestantType: "Individual" },
"steals and blocks": {
standardName: "StealsAndBlocks",
category: "PropOU",
contestantType: "Individual"
},
// Basketball - Other (Individual)
turnovers: { standardName: "Turnovers", category: "PropOU", contestantType: "Individual" },
tos: { standardName: "Turnovers", category: "PropOU", contestantType: "Individual" },
"three pointers made": {
standardName: "ThreePointersMade",
category: "PropOU",
contestantType: "Individual"
},
threes: { standardName: "ThreePointersMade", category: "PropOU", contestantType: "Individual" },
"3pm": { standardName: "ThreePointersMade", category: "PropOU", contestantType: "Individual" },
"3-pointers": {
standardName: "ThreePointersMade",
category: "PropOU",
contestantType: "Individual"
},
"3pt shots made": {
standardName: "ThreePointersMade",
category: "PropOU",
contestantType: "Individual"
},
"free throws made": {
standardName: "FreeThrowsMade",
category: "PropOU",
contestantType: "Individual"
},
ftm: { standardName: "FreeThrowsMade", category: "PropOU", contestantType: "Individual" },
// Basketball - Combo Stats (Individual)
"points rebounds assists": {
standardName: "PRA",
category: "PropOU",
contestantType: "Individual"
},
pra: { standardName: "PRA", category: "PropOU", contestantType: "Individual" },
"pts+reb+ast": { standardName: "PRA", category: "PropOU", contestantType: "Individual" },
"pts+rebs+asts": { standardName: "PRA", category: "PropOU", contestantType: "Individual" },
"points and rebounds": { standardName: "PR", category: "PropOU", contestantType: "Individual" },
"pts+rebs": { standardName: "PR", category: "PropOU", contestantType: "Individual" },
"points and assists": { standardName: "PA", category: "PropOU", contestantType: "Individual" },
"pts+ast": { standardName: "PA", category: "PropOU", contestantType: "Individual" },
"rebounds and assists": { standardName: "RA", category: "PropOU", contestantType: "Individual" },
"rebs+ast": { standardName: "RA", category: "PropOU", contestantType: "Individual" },
// Basketball - Yes/No (Individual)
"double double": {
standardName: "DoubleDouble",
category: "PropYN",
contestantType: "Individual"
},
"to record a double double": {
standardName: "DoubleDouble",
category: "PropYN",
contestantType: "Individual"
},
"triple double": {
standardName: "TripleDouble",
category: "PropYN",
contestantType: "Individual"
},
"to record a triple double": {
standardName: "TripleDouble",
category: "PropYN",
contestantType: "Individual"
},
// ============================================================================
// CROSS-SPORT TEAM PROPS (TeamLeague)
// ============================================================================
"first team to score": {
standardName: "FirstToScore",
category: "PropYN",
contestantType: "TeamLeague"
},
"1st team to score": {
standardName: "FirstToScore",
category: "PropYN",
contestantType: "TeamLeague"
},
"first to score": {
standardName: "FirstToScore",
category: "PropYN",
contestantType: "TeamLeague"
},
"to score first": {
standardName: "FirstToScore",
category: "PropYN",
contestantType: "TeamLeague"
},
"last team to score": {
standardName: "LastToScore",
category: "PropYN",
contestantType: "TeamLeague"
},
"last to score": {
standardName: "LastToScore",
category: "PropYN",
contestantType: "TeamLeague"
},
"to score last": {
standardName: "LastToScore",
category: "PropYN",
contestantType: "TeamLeague"
}
};
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function detectPropType(propText) {
const cleanText = propText.toLowerCase().trim();
const sortedKeywords = Object.entries(PROP_TYPE_MAP).sort(([a], [b]) => b.length - a.length);
for (const [keyword, info] of sortedKeywords) {
const escapedKeyword = escapeRegex(keyword).replace(/\s+/g, "\\s+");
const regex = new RegExp(`\\b${escapedKeyword}\\b`);
if (regex.test(cleanText)) {
return info;
}
}
return null;
}
function extractContestantAndProp(text) {
const cleanText = text.trim();
const sortedKeywords = Object.entries(PROP_TYPE_MAP).sort(([a], [b]) => b.length - a.length);
for (const [keyword] of sortedKeywords) {
const escapedKeyword = escapeRegex(keyword).replace(/\s+/g, "\\s+");
const regex = new RegExp(`\\b(${escapedKeyword})\\b`, "i");
const match = cleanText.match(regex);
if (match) {
const contestant = cleanText.substring(0, match.index).trim();
const propText = match[1].toLowerCase();
if (contestant) {
return { contestant, propText };
}
}
}
return null;
}
function validatePropFormat(propText, hasLine, rawInput) {
const propInfo = detectPropType(propText);
if (!propInfo) {
throw new InvalidContractTypeError(rawInput, `Unsupported prop type: ${propText}`);
}
if (propInfo.category === "PropOU" && !hasLine) {
throw new InvalidContractTypeError(
rawInput,
`${propInfo.standardName} props require an over/under line (e.g., "o12.5")`
);
}
if (propInfo.category === "PropYN" && hasLine) {
throw new InvalidContractTypeError(
rawInput,
`${propInfo.standardName} props cannot have a line - they are yes/no bets only`
);
}
}
function parseWriteinDate(dateString, rawInput, isWritein2 = true, referenceDate) {
const cleaned = dateString.trim();
const ErrorClass = isWritein2 ? InvalidWriteinDateError : InvalidDateError;
if (!cleaned) {
throw new ErrorClass(rawInput, dateString, "Date cannot be empty");
}
let parsedDate = null;
const refDate = referenceDate || /* @__PURE__ */ new Date();
const currentYear = refDate.getUTCFullYear();
const today = new Date(
Date.UTC(refDate.getUTCFullYear(), refDate.getUTCMonth(), refDate.getUTCDate())
);
const patterns = [
// Full date patterns with 4-digit year
/^(\d{4})[/-](\d{1,2})[/-](\d{1,2})$/,
// YYYY/MM/DD or YYYY-MM-DD
/^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$/,
// MM/DD/YYYY or MM-DD-YYYY
// Full date patterns with 2-digit year
/^(\d{1,2})[/-](\d{1,2})[/-](\d{2})$/,
// MM/DD/YY or MM-DD-YY
// Date patterns without year
/^(\d{1,2})[/-](\d{1,2})$/
// MM/DD or MM-DD
];
for (const pattern of patterns) {
const match = cleaned.match(pattern);
if (match) {
let year, month, day;
if (match.length === 4) {
if (pattern.source.startsWith("^(\\d{4})")) {
year = parseInt(match[1]);
month = parseInt(match[2]);
day = parseInt(match[3]);
} else if (pattern.source.includes("(\\d{2})$")) {
month = parseInt(match[1]);
day = parseInt(match[2]);
const twoDigitYear = parseInt(match[3]);
year = 2e3 + twoDigitYear;
} else {
month = parseInt(match[1]);
day = parseInt(match[2]);
year = parseInt(match[3]);
}
} else {
month = parseInt(match[1]);
day = parseInt(match[2]);
const dateThisYear = new Date(Date.UTC(currentYear, month - 1, day));
if (dateThisYear >= today) {
year = currentYear;
} else {
year = currentYear + 1;
}
}
if (month < 1 || month > 12 || day < 1 || day > 31) {
throw new ErrorClass(
rawInput,
dateString,
"Unable to parse date. Supported formats: YYYY-MM-DD, MM/DD/YYYY, YYYY/MM/DD, MM-DD-YYYY, MM/DD, MM-DD"
);
}
const testDate = new Date(Date.UTC(year, month - 1, day));
if (testDate.getUTCFullYear() === year && testDate.getUTCMonth() === month - 1 && testDate.getUTCDate() === day) {
parsedDate = testDate;
break;
} else {
throw new ErrorClass(
rawInput,
dateString,
`Invalid calendar date (e.g., February 30th doesn't exist)`
);
}
}
}
if (!parsedDate) {
throw new ErrorClass(
rawInput,
dateString,
"Unable to parse date. Supported formats: YYYY-MM-DD, MM/DD/YYYY, YYYY/MM/DD, MM-DD-YYYY, MM/DD, MM-DD"
);
}
return parsedDate;
}
function validateWriteinDescription(description, rawInput) {
const trimmed = description.trim();
if (!trimmed) {
throw new InvalidWriteinDescriptionError(rawInput, description, "Description cannot be empty");
}
if (trimmed.length < 10) {
throw new InvalidWrite