File size: 4,044 Bytes
e0e1444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// Faithful port of scanutilities.py's out-of-class helpers (footfinder,
// longestMatch, dictinvert, uniquePermutations, AltLineLenCalc).

export interface FootFinderResult {
  feet: Array<{ foot: string; index: number }>;
  /** false the moment ANY chunk in [startpoint,endpoint) fails to match —
   *  mirrors every Python call site's `if footname: append() else: return
   *  ([],[])` bail-out.  An EMPTY range (startpoint===endpoint) is a valid,
   *  vacuous success (ok=true, feet=[]), never a failure. */
  ok: boolean;
}

/** Walk `scansion[startpoint:endpoint]` in `chunkSize`-wide steps, mapping
 *  each chunk through `fDict`. */
export function footFinder(
  fDict: Record<string, string>,
  scansion: string,
  chunkSize: number,
  startpoint: number,
  endpoint: number
): FootFinderResult {
  const feet: Array<{ foot: string; index: number }> = [];
  let pos = startpoint;
  while (pos < endpoint) {
    const possfoot = scansion.slice(pos, pos + chunkSize);
    const name = fDict[possfoot];
    if (name === undefined) return { feet, ok: false };
    pos += chunkSize;
    feet.push({ foot: name, index: pos });
  }
  return { feet, ok: true };
}

/** name -> its single pattern key.  Both footDict and AnapSubs happen to have
 *  every value distinct (verified against scanstrings.py), so a straight
 *  reverse map is the exact behavioural equivalent of Hartman's
 *  dictinvert()[name][0], without needing the list-of-keys wrapper. */
export function invertFootDict(d: Record<string, string>): Record<string, string> {
  const inv: Record<string, string> = {};
  for (const [k, v] of Object.entries(d)) inv[v] = k;
  return inv;
}

export interface LongestMatchResult {
  start: number;
  length: number;
}

/** Kent Johnson's "find the LAST-longest regex match" (scanutilities.py).
 *  Deliberately prefers the last of several equal-longest matches — Hartman's
 *  own comment: lines tend to be more regular at their ends than their
 *  beginnings.  `rx` must be a non-global, non-sticky pattern; a fresh `g`
 *  clone drives the repeated from-here search. */
export function longestMatch(rx: RegExp, s: string): LongestMatchResult | null {
  const search = new RegExp(rx.source, rx.flags.includes('g') ? rx.flags : rx.flags + 'g');
  let start = 0;
  let length = 0;
  let current = 0;
  for (;;) {
    search.lastIndex = current;
    const m = search.exec(s);
    if (!m) break;
    const mStart = m.index;
    const mEnd = mStart + m[0].length;
    current = mStart + 1;
    if (mEnd - mStart >= length) {
      start = mStart;
      length = mEnd - mStart;
    }
    if (m[0].length === 0) search.lastIndex = current; // guard against zero-width infinite loop
  }
  return length ? { start, length } : null;
}

/** All permutations of the characters of `s` (ActiveState Cookbook code,
 *  ported literally, including the >9-chars short-circuit that just returns
 *  `s` unchanged to avoid a combinatorial explosion). */
function getPermutations(a: string): string[] {
  if (a.length === 1 || a.length > 9) return [a];
  const out: string[] = [];
  for (let i = 0; i < a.length; i++) {
    const rest = a.slice(0, i) + a.slice(i + 1);
    for (const p of getPermutations(rest)) out.push(a[i] + p);
  }
  return out;
}

/** Deduped, lexicographically-sorted permutations of `lst`. */
export function uniquePermutations(lst: string): string[] {
  const all = getPermutations(lst);
  const u = new Set(all);
  return [...u].sort();
}

/** Rough minimum foot-count estimate: count stresses, but zero out the very
 *  first mark and any stress immediately following another stress — mutated
 *  IN PLACE, left-to-right, so a run of 3+ stresses only zeroes alternating
 *  members (matches the original's forward self-referential mutation). */
export function altLineLenCalc(lexmarks: string): number {
  const marklist = lexmarks.split('');
  for (let inx = 0; inx < marklist.length; inx++) {
    if (inx === 0 || marklist[inx - 1] === '/') marklist[inx] = 'x';
  }
  return marklist.filter(c => c === '/').length;
}