Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/schema/pay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,24 @@ const REVENUE = /\b(rev(?:enue)?(?:\s+share)?|profit\s+share|of\s+(?:the\s+)?rev

function parseRevenueShare(text: string): PayLine | string | null {
if (!REVENUE.test(text) || !/%|percent/i.test(text)) return null;
const numbers = [...text.matchAll(/(\d+(?:\.\d+)?)\s*(?:%|percent)?/g)].map((m) => Number(m[1]));
if (numbers.length === 0) return 'A revenue share needs a percentage: "10% revenue share".';
const matches = [...text.matchAll(/\d+(?:\.\d+)?/g)];
if (matches.length === 0) return 'A revenue share needs a percentage: "10% revenue share".';
const firstMatch = matches[0]!;
const secondMatch = matches[1];
// A number in surrounding prose is not a range endpoint, and a minus
// before the first number must not silently turn a negative share positive.
const prefix = text.slice(0, firstMatch.index).trim();
const separator = secondMatch === undefined
? null
: text.slice(firstMatch.index! + firstMatch[0].length, secondMatch.index);
if (
matches.length > 2 ||
/[-\u2212]$/.test(prefix) ||
(separator !== null && !/^\s*(?:%|percent)?\s*(?:-|\u2013|\u2014|to)\s*$/i.test(separator))
) {
return 'Write one non-negative revenue percentage or a range, such as "5-10% revenue share".';
}
const numbers = matches.map((match) => Number(match[0]));
const upTo = /^\s*(up\s*to|max(?:imum)?)\b/i.test(text);
const from = /^\s*(from|at\s+least|min(?:imum)?)\b/i.test(text) || /\+\s*%?/.test(text);
const [first, second] = numbers;
Expand Down
37 changes: 37 additions & 0 deletions test/pay-revenue-text.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { parsePayLine, formatPayLine, normalisePay } from '../src/schema/pay.ts';

for (const text of [
'-10% revenue share',
'from -10% revenue share',
'5% - -10% revenue share',
'10% revenue share after 12 months',
'10% revenue share after 6 months',
'5-10-15% revenue share',
]) {
test(`rejects ambiguous or negative revenue share: ${text}`, () => {
assert.equal(typeof parsePayLine(text), 'string');
assert.equal(typeof normalisePay({ pay: text }), 'string');
});
}

for (const [text, min, max] of [
['10% revenue share', 10, 10],
['5-10% of revenue', 5, 10],
['5% - 10% revenue share', 5, 10],
['5 to 10 percent profit share', 5, 10],
['Up to 10% revenue share', null, 10],
['From 10% revenue share', 10, null],
['10+% revenue share', 10, null],
['0.5-1.25% revenue share', 0.5, 1.25],
] as const) {
test(`preserves valid revenue share and round trip: ${text}`, () => {
const parsed = parsePayLine(text);
assert.notEqual(typeof parsed, 'string');
if (typeof parsed === 'string') throw new Error(parsed);
assert.equal(parsed.min, min);
assert.equal(parsed.max, max);
assert.deepEqual(parsePayLine(formatPayLine(parsed)), parsed);
});
}
Loading