import { parseAllMw } from "@dci/core"; /** * MW figures that describe the announced SITE, as opposed to a company's portfolio, a market statistic or a regional * total. Shared by the page classifier (`classifyPage().mw`) and the news announcement extractor, so a press release * saying "brings the company's footprint to 1.4 GW" never puts 1 400 MW on a project or a facility. */ /** Portfolio / company-wide context BEFORE a MW figure — not the size of the announced site. */ export const PORTFOLIO_BEFORE_RE = /\b(total(?:l?ing)?|across|portfolio|combined|footprint|pipeline|capacity to(?: over| more than)?|to over|to more than|brings?(?: its| the| total)?[^.]{0,60}(?:to|capacity)|bringing[^.]{0,60}to|taking[^.]{0,60}to|now (?:has|operates|owns|manages)|operates?(?: over| more than)?|owns?(?: over| more than)?|manages?(?: over| more than)?|(?:more than|over) \d+ (?:data cent(?:er|re)s?|facilities|sites|campuses)[^.]{0,20}|globally|worldwide|company-?wide|nationwide|in (?:asia|apj|japan|europe|emea|the region|australia|the us|the u\.s\.|the country|india|latin america|the americas|the uk|the middle east)|of (?:total )?capacity (?:in|across))\s*(?:of\s*)?$/i; /** Portfolio / company-wide context AFTER a MW figure. */ export const PORTFOLIO_AFTER_RE = /^\s*(?:MW|GW|megawatts?|gigawatts?)?\s*(?:across|portfolio|globally|worldwide|company-?wide|nationwide|in (?:asia|apj|japan|europe|emea|the region|australia|the us|the country|india|latin america|the americas|the uk|the middle east)|of (?:total )?(?:capacity |power )?(?:across|in operation|under (?:management|development)|planned)|of (?:contracted|committed|operational|total) (?:capacity|power))\b/i; const MW_TOKEN_RE = /(\d{1,3}(?:[.,]\d{3})+|\d+(?:[.,]\d+)?)\+?\s?(gigawatts?|gw|megawatts?|mw)\b/gi; /** Is the MW token at `start`…`end` of `scope` a portfolio / company-wide figure rather than the site's? */ export function isPortfolioFigure(scope: string, start: number, end: number): boolean { const before = scope.slice(Math.max(0, start - 100), start); const after = scope.slice(end, end + 40); return PORTFOLIO_BEFORE_RE.test(before) || PORTFOLIO_AFTER_RE.test(after); } /** * MW figures in a scope that describe the announced site (largest first, deduplicated, 0 < MW ≤ 20 000), skipping * company-wide totals and market statistics. When every figure in the scope is a portfolio total, none is returned — * the caller moves to the next scope. */ export function projectMwFigures(scope: string): number[] { const site: number[] = []; MW_TOKEN_RE.lastIndex = 0; for (const m of scope.matchAll(MW_TOKEN_RE)) { const start = m.index ?? 0; const value = parseAllMw(m[0])[0]; if (value == null || value <= 0 || value > 20_000) continue; if (isPortfolioFigure(scope, start, start + m[0].length)) continue; site.push(value); } return [...new Set(site)].sort((a, b) => b - a); }