Skip to content

Commit 4ff3171

Browse files
CarsonFAndre Turner
andcommitted
Add problems to extraction results
Co-authored-by: Andre Turner <[email protected]>
1 parent 7420217 commit 4ff3171

11 files changed

+368
-79
lines changed

src/components/pnp/extract-scripture.ts

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,88 @@
11
import { parseScripture, tryParseScripture } from '@seedcompany/scripture';
22
import { Row } from '~/common/xlsx.util';
33
import { ScriptureRange } from '../scripture/dto';
4+
import { PnpExtractionResult } from './extraction-result';
5+
import { addProblemMismatchScriptureAndVerseCount } from './isGoalRow';
46
import { WrittenScripturePlanningSheet } from './planning-sheet';
57

6-
export const extractScripture = (row: Row<WrittenScripturePlanningSheet>) => {
8+
export const extractScripture = (
9+
row: Row<WrittenScripturePlanningSheet>,
10+
result: PnpExtractionResult,
11+
) => {
712
const sheet = row.sheet;
8-
const totalVerses = sheet.totalVerses(row)!;
9-
const scriptureFromBookCol = parseScripture(sheet.bookName(row));
13+
14+
const totalVersesCell = sheet.totalVerses(row)!;
15+
const totalVerses = totalVersesCell.asNumber!;
16+
17+
const bookCell = sheet.bookName(row);
18+
const scriptureFromBookCol = parseScripture(bookCell.asString);
19+
const book = scriptureFromBookCol[0].start.book;
1020

1121
const common = {
12-
bookName: scriptureFromBookCol[0].start.book.name,
22+
bookName: book.name,
1323
totalVerses,
1424
};
25+
let mismatchError = false;
1526

1627
// If scripture from book column matches total count, use it.
17-
if (ScriptureRange.totalVerses(...scriptureFromBookCol) === totalVerses) {
28+
const totalVersesInBookCol = ScriptureRange.totalVerses(
29+
...scriptureFromBookCol,
30+
);
31+
if (totalVersesInBookCol === totalVerses) {
1832
return {
1933
...common,
2034
scripture: scriptureFromBookCol.map(ScriptureRange.fromVerses),
2135
};
36+
// If it is more than just the book name (aka not just the book name) then
37+
// the verse count will be less and if it doesn't match the total, there is a problem
38+
} else if (totalVersesInBookCol < book.totalVerses) {
39+
mismatchError = true;
40+
// TODO I think this is a redundant check.
41+
// I don't think we will ever get here because the row is filtered out with
42+
// the isGoalRow function.
43+
addProblemMismatchScriptureAndVerseCount(
44+
result,
45+
totalVersesInBookCol,
46+
bookCell,
47+
totalVersesCell,
48+
);
2249
}
2350

2451
// Otherwise, if note column has scripture that matches the total count use it.
25-
const scriptureFromNoteCol = tryParseScripture(sheet.myNote(row));
26-
if (
27-
scriptureFromNoteCol &&
28-
ScriptureRange.totalVerses(...scriptureFromNoteCol) === totalVerses
29-
) {
30-
return {
31-
...common,
32-
scripture: scriptureFromNoteCol.map(ScriptureRange.fromVerses),
33-
};
52+
const noteCell = sheet.myNote(row);
53+
const scriptureFromNoteCol = tryParseScripture(noteCell.asString);
54+
if (scriptureFromNoteCol) {
55+
const totalVersesFromNoteCol = ScriptureRange.totalVerses(
56+
...scriptureFromNoteCol,
57+
);
58+
if (totalVersesFromNoteCol === totalVerses) {
59+
return {
60+
...common,
61+
scripture: scriptureFromNoteCol.map(ScriptureRange.fromVerses),
62+
};
63+
}
64+
mismatchError = true;
65+
result.addProblem({
66+
severity: 'Error',
67+
groups:
68+
'Mismatch between the planned scripture in _My Notes_ column and the number of verses to translate',
69+
message: `"${noteCell.asString!}" \`${
70+
noteCell.ref
71+
}\` is **${totalVersesFromNoteCol}** verses, but the goal declares **${totalVerses}** verses to translate \`${
72+
totalVersesCell.ref
73+
}\``,
74+
source: noteCell,
75+
});
3476
}
3577

3678
// Otherwise, fallback to unspecified scripture.
79+
!mismatchError &&
80+
result.addProblem({
81+
severity: 'Warning',
82+
groups: 'Unspecified scripture reference',
83+
message: `"${book.name}" \`${bookCell.ref}\` does not a have specified scripture reference (either in the _Books_ or _My Notes_ column)`,
84+
source: totalVersesCell,
85+
});
3786
return {
3887
...common,
3988
scripture: [],

src/components/pnp/findStepColumns.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import levenshtein from 'fastest-levenshtein';
33
import { startCase, without } from 'lodash';
44
import { Column } from '~/common/xlsx.util';
55
import { ProductStep as Step } from '../product/dto';
6+
import { PnpExtractionResult } from './extraction-result';
67
import { PlanningSheet } from './planning-sheet';
78
import { ProgressSheet } from './progress-sheet';
89
import 'ix/add/iterable-operators/filter.js';
@@ -14,16 +15,17 @@ import 'ix/add/iterable-operators/toarray.js';
1415
*/
1516
export function findStepColumns(
1617
sheet: PlanningSheet | ProgressSheet,
18+
result?: PnpExtractionResult,
1719
availableSteps: readonly Step[] = [...Step],
1820
) {
1921
const matchedColumns: Partial<Record<Step, Column>> = {};
2022
let remainingSteps = availableSteps;
2123
const possibleSteps = sheet.stepLabels
2224
.walkRight()
2325
.filter((cell) => !!cell.asString)
24-
.map((cell) => ({ label: cell.asString!, column: cell.column }))
26+
.map((cell) => ({ label: cell.asString!, column: cell.column, cell }))
2527
.toArray();
26-
possibleSteps.forEach(({ label, column }, index) => {
28+
possibleSteps.forEach(({ label, column, cell }, index) => {
2729
if (index === possibleSteps.length - 1) {
2830
// The last step should always be called Completed in CORD per Seth.
2931
// Written PnP already match, but OBS calls it Record. This is mislabeled
@@ -43,6 +45,12 @@ export function findStepColumns(
4345
([_, distance]) => distance,
4446
)[0]?.[0];
4547
if (!chosen) {
48+
result?.addProblem({
49+
severity: 'Error',
50+
groups: 'The step header label is non standard',
51+
message: `"${label}" \`${cell.ref}\` is not a standard step label`,
52+
source: cell,
53+
});
4654
return;
4755
}
4856
matchedColumns[chosen] = column;

src/components/pnp/isGoalRow.ts

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,102 @@
1-
import { Book, parseScripture } from '@seedcompany/scripture';
1+
import { Book, parseScripture, Range, Verse } from '@seedcompany/scripture';
22
import { Cell } from '~/common/xlsx.util';
33
import { ScriptureRange } from '../scripture/dto';
4+
import { PnpExtractionResult } from './extraction-result';
45
import { PlanningSheet } from './planning-sheet';
56
import { ProgressSheet } from './progress-sheet';
67

7-
export const isGoalRow = (cell: Cell<PlanningSheet | ProgressSheet>) => {
8+
export const isGoalRow = (
9+
cell: Cell<PlanningSheet | ProgressSheet>,
10+
result?: PnpExtractionResult,
11+
) => {
812
if (cell.sheet.isOBS()) {
913
return !!cell.sheet.storyName(cell.row);
1014
}
1115
if (!cell.sheet.isWritten()) {
1216
return false;
1317
}
1418

15-
const rawBook = cell.sheet.bookName(cell.row);
16-
const versesToTranslate = cell.sheet.totalVerses(cell.row) ?? 0;
19+
const bookCell = cell.sheet.bookName(cell.row);
20+
const versesCell = cell.sheet.totalVerses(cell.row);
21+
const rawBook = bookCell.asString;
22+
const versesToTranslate = versesCell.asNumber ?? 0;
1723

1824
if (versesToTranslate <= 0) {
1925
return false;
2026
}
2127

22-
// Try as book name first since it's faster than parsing scripture string
23-
const maybeBook = rawBook ? Book.namedMaybe(rawBook) : undefined;
28+
if (!rawBook) {
29+
result?.addProblem({
30+
severity: 'Warning',
31+
groups: 'No book name given',
32+
message: `Ignoring row with no book name \`${bookCell.ref}\` even though there are **${versesToTranslate}** verses to translate \`${versesCell.ref}\``,
33+
source: cell,
34+
});
35+
return false;
36+
}
37+
38+
// Try as book name first since it is faster than parsing scripture string
39+
// And ensure total verses given is plausible
40+
const maybeBook = Book.namedMaybe(rawBook);
2441
if (maybeBook) {
25-
// Sanity check to ensure total verses given is plausible
26-
return versesToTranslate <= maybeBook.totalVerses;
42+
const validVerseCount = versesToTranslate <= maybeBook.totalVerses;
43+
!validVerseCount &&
44+
result?.addProblem({
45+
severity: 'Error',
46+
groups: 'The verses to translate exceeds total verses in book',
47+
message: `Ignoring _${maybeBook.name}_ because **${versesToTranslate}** \`${versesCell.ref}\` verses to translate exceeds the total number of verses in the book`,
48+
source: cell,
49+
});
50+
return validVerseCount;
2751
}
2852

29-
let scriptureRanges;
53+
let parsedRange = true;
54+
let scriptureRanges: ReadonlyArray<Range<Verse>> = [];
3055
try {
3156
scriptureRanges = parseScripture(rawBook);
3257
} catch {
58+
parsedRange = false;
59+
}
60+
if (!parsedRange || scriptureRanges.length === 0) {
61+
result?.addProblem({
62+
severity: 'Error',
63+
groups: 'Could not determine book reference',
64+
message: `"${rawBook}" \`${bookCell.ref}\` could not be identified as a book name or scripture reference`,
65+
source: cell,
66+
});
3367
return false;
3468
}
3569

36-
const totalVersesInRange = ScriptureRange.totalVerses(...scriptureRanges);
37-
3870
// Treat range(s) as valid if the total verses represented
39-
// equals what's been given in the other column.
40-
return versesToTranslate === totalVersesInRange;
71+
// equals what has been given in the other column.
72+
const totalVersesInRange = ScriptureRange.totalVerses(...scriptureRanges);
73+
const validRange = totalVersesInRange === versesToTranslate;
74+
if (!validRange) {
75+
addProblemMismatchScriptureAndVerseCount(
76+
result,
77+
totalVersesInRange,
78+
bookCell,
79+
versesCell,
80+
);
81+
}
82+
return validRange;
4183
};
84+
85+
export function addProblemMismatchScriptureAndVerseCount(
86+
result: PnpExtractionResult | undefined,
87+
parsedVerseCount: number,
88+
book: Cell,
89+
totalVerses: Cell,
90+
) {
91+
result?.addProblem({
92+
severity: 'Error',
93+
groups:
94+
'Mismatch between the planned scripture in _Books_ column and the number of verses to translate',
95+
message: `"${book.asString!}" \`${
96+
book.ref
97+
}\` is **${parsedVerseCount}** verses, but the goal declares **${totalVerses.asNumber!}** verses to translate \`${
98+
totalVerses.ref
99+
}\``,
100+
source: book,
101+
});
102+
}

src/components/pnp/isGoalStepPlannedInsideProject.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { trimStart } from 'lodash';
2-
import { CalendarDate, fullFiscalYear } from '~/common';
2+
import { fullFiscalYear } from '~/common';
33
import { Cell } from '~/common/xlsx.util';
4+
import { ProductStep } from '../product/dto';
5+
import { PnpExtractionResult } from './extraction-result';
46
import { PlanningSheet } from './planning-sheet';
57
import { Pnp } from './pnp';
68

@@ -9,10 +11,46 @@ import { Pnp } from './pnp';
911
*/
1012
export const isGoalStepPlannedInsideProject = (
1113
pnp: Pnp,
12-
cell: Cell<PlanningSheet> | CalendarDate | undefined,
13-
): cell is CalendarDate => {
14-
const fullFY = cell instanceof Cell ? stepPlanCompleteDate(cell) : cell;
15-
return !!fullFY && pnp.planning.projectFiscalYears.contains(fullFY);
14+
cell: Cell<PlanningSheet>,
15+
step: ProductStep,
16+
result: PnpExtractionResult,
17+
) => {
18+
const fullFY = stepPlanCompleteDate(cell);
19+
const fiscalYear = new Date(String(fullFY)).getFullYear();
20+
const isPlanned =
21+
!!fullFY && pnp.planning.projectFiscalYears.contains(fullFY);
22+
if (isPlanned) {
23+
return true;
24+
}
25+
if (!fullFY) {
26+
// Empty cell generates no problem
27+
return false;
28+
}
29+
const goalLabel = pnp.planning.goalName(cell.row).asString ?? '';
30+
const stepLabel = ProductStep.entry(step).label;
31+
32+
if (pnp.planning.projectFiscalYears.isAfter(fullFY)) {
33+
result.addProblem({
34+
severity: 'Notice',
35+
groups: [
36+
`Step(s) of goal(s) were finished **before** this project's fiscal years`,
37+
`Step(s) of _${goalLabel}_ were finished **before** this project's fiscal years`,
38+
],
39+
message: `Ignoring _${stepLabel}_ for _${goalLabel}_ \`${cell.ref}\` which was finished _FY${fiscalYear}_ before this project's fiscal years`,
40+
source: cell,
41+
});
42+
} else {
43+
result.addProblem({
44+
severity: 'Error',
45+
groups: [
46+
`Step(s) of goal(s) are planned to be complete **after** this project's fiscal years`,
47+
`Step(s) of _${goalLabel}_ are planned to be complete **after** this project's fiscal years`,
48+
],
49+
message: `_${stepLabel}_ for _${goalLabel}_ \`${cell.ref}\` is planned to be completed _FY${fiscalYear}_ which is **after** this project's fiscal years`,
50+
source: cell,
51+
});
52+
}
53+
return false;
1654
};
1755

1856
export const stepPlanCompleteDate = (cell: Cell<PlanningSheet>) => {

src/components/pnp/isProgressCompletedOutsideProject.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,52 @@
1-
import { fullFiscalQuarter } from '~/common';
1+
import { fiscalQuarter, fiscalYear, fullFiscalQuarter } from '~/common';
22
import { Cell } from '~/common/xlsx.util';
3+
import { ProductStep } from '../product/dto';
4+
import { PnpExtractionResult } from './extraction-result';
35
import { Pnp } from './pnp';
46
import { ProgressSheet } from './progress-sheet';
57

68
export const isProgressCompletedOutsideProject = (
79
pnp: Pnp,
810
cell: Cell<ProgressSheet>,
11+
step: ProductStep,
12+
result: PnpExtractionResult,
913
) => {
1014
const completeDate = stepCompleteDate(cell);
11-
return completeDate && !pnp.planning.projectDateRange.contains(completeDate);
15+
if (!completeDate) {
16+
return false;
17+
}
18+
if (pnp.planning.projectDateRange.contains(completeDate)) {
19+
return false;
20+
}
21+
22+
const stepLabel = ProductStep.entry(step).label;
23+
const goalLabel = pnp.planning.goalName(cell.row).asString ?? '';
24+
const dateLabel = `Q${fiscalQuarter(completeDate)} FY${fiscalYear(
25+
completeDate,
26+
)}`;
27+
28+
if (pnp.planning.projectDateRange.isAfter(completeDate)) {
29+
result.addProblem({
30+
severity: 'Notice',
31+
groups: [
32+
`Step(s) of goal(s) were finished **before** this project`,
33+
`Step(s) of _${goalLabel}_ were finished **before** this project`,
34+
],
35+
message: `Ignoring _${stepLabel}_ for _${goalLabel}_ \`${cell.ref}\` which was finished _${dateLabel}_ before this project started`,
36+
source: cell,
37+
});
38+
} else {
39+
result.addProblem({
40+
severity: 'Error',
41+
groups: [
42+
`Step(s) of goal(s) are marked complete **after** this project's date range`,
43+
`Step(s) of _${goalLabel}_ are marked complete **after** this project's date range`,
44+
],
45+
message: `_${stepLabel}_ for _${goalLabel}_ \`${cell.ref}\` is marked completed on _${dateLabel}_ which is **after** this project ends`,
46+
source: cell,
47+
});
48+
}
49+
return true;
1250
};
1351

1452
/**

0 commit comments

Comments
 (0)