Repository navigation
Conversation
A template row that mixes {data.xxx} placeholders with a formula cell
lost the formula in every row created by a list fill: template analysis
only inspects string cells, and the fill only writes the placeholder
columns into the created rows.
The fill executor now records the formula cells of the collection
template rows and recreates them in each created row, translating the
formula with POI's FormulaShifter (row/column copy) so relative
references follow the new position and absolute ones stay anchored.
Styles follow the existing autoStyle behavior and unparsable formulas
are skipped instead of failing the fill.
Fixes apache#199
Generated-by: ZCode
Signed-off-by: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com>
The CI check job failed spotless:check on the test file and the fill executor. Re-ran spotless:apply; no logic changes. Generated-by: ZCode
skytin1004
left a comment
There was a problem hiding this comment.
@BigDataDZ Thanks for working on this. While testing it locally, I found that formulas can still be missed when there are multiple collection areas on the same sheet. I left an inline comment with the details. Could you take a look?
| } | ||
| } | ||
| } | ||
| collectFormulaTemplateCells(sheet); |
There was a problem hiding this comment.
The initial template scan populates the analysis cache for all collection keys, but this only collects formula cells for the current collection. A later collection can therefore hit the early return above before this method runs for that collection.
I reproduced this locally with two collection areas on the same sheet: formulas were copied for the first collection, but the formula next to the second collection was missing.
Could we make sure formula cells are collected for each collection even when its analysis data is already cached, and add a regression test covering multiple collection areas?
The full template scan fills the analysis cache for all collection keys at once, so every later collection hits the early return in readTemplateData and skipped formula collection: only the first filled collection kept its formulas. Collect the formula cells on the cached path too, guarded so the same collection never collects twice. Adds a regression test with two collection areas on one sheet: the collection filled second (its analysis data already cached) now carries its formulas into created rows as well. The test uses the writer-level fill API because ExcelWriterSheetBuilder.doFill finishes the writer on every call. Review: skytin1004 on apache#1215. Generated-by: ZCode
|
@skytin1004 Thanks for catching this — you were right on both points. Fixed in 906bc04:
Full fesod-sheet suite passes (1047 tests, 0 failures); spotless clean. |
There was a problem hiding this comment.
Thanks for the quick follow-up @BigDataDZ. I verified that 906bc04 fixes the cached-analysis case I originally reported, and the new multi-collection regression test correctly covers that failure.
While re-reviewing the full formula-copy path, I found two separate correctness issues that I missed in my first pass. Sorry for the extra round, but both can result in incorrect formulas or lost data, so I'm requesting changes once more. Please take a look at the inline comments.
| } | ||
| } | ||
| } | ||
| collectFormulaTemplateCells(sheet); |
There was a problem hiding this comment.
I reproduced a forceNewRow case where a formula references a row below the collection area. Formula cells are cached here before shiftRows() runs, so TemplateFormulaCell retains the pre-shift formula text even though POI updates the formula in the workbook.
For example, with B2 = A4*2, filling two items with forceNewRow(true) correctly updates the original B2 to A5*2, but the copied B3 is also A5*2 instead of the expected A6*2.
Could we invalidate or refresh the formula cache after structural row shifts and add a regression test for this case?
| vertical ? templateFormulaCell.getRowIndex() + relativeRowIndex : templateFormulaCell.getRowIndex(); | ||
| int targetColumnIndex = vertical | ||
| ? templateFormulaCell.getColumnIndex() | ||
| : templateFormulaCell.getColumnIndex() + relativeRowIndex; |
There was a problem hiding this comment.
I also reproduced a collision in horizontal fill. With {data.name} in A2, =A2*2 in B2, and three horizontally filled items, B2 ends up as a FORMULA cell rather than the second item's STRING cell. The value only survives as the formula's cached result and is lost when the workbook recalculates.
Formula discovery is currently row-based while horizontal collections expand by column. Could we make discovery and placement direction-aware and add a horizontal regression test that verifies both cell types/values and formulas after reopening the workbook?
Two correctness issues found in re-review of the formula-copy path: 1. Horizontal fill collision: collectFormulaTemplateCells scanned the full collection row, so formulas in data columns were collected and later overwrote filled data. Columns holding collection placeholders are now excluded from formula discovery. 2. forceNewRow formula staleness: shiftRows may update formula references in the live sheet (e.g. =A4*2 becomes =A5*2 when the referenced row shifts), but TemplateFormulaCell cached the pre-shift text before shiftRows ran. The cache is now refreshed after structural row shifts by adjusting row references in the cached formula text. Adds regression tests for both: horizontal fill preserves data cells; forceNewRow translates the post-shift formula into created rows. Review: skytin1004 on apache#1215 (second round). Generated-by: ZCode
|
@skytin1004 Both issues confirmed and fixed in 4902242:
Full fesod-sheet suite: 1049 tests, 0 failures. Spotless clean. Thanks for the thorough second pass — both were real bugs worth catching. |
Matcher.appendReplacement accepts only StringBuffer on JDK 8; the StringBuilder variant is JDK 9+. The project compiles on JDK 8 in CI. Generated-by: ZCode
There was a problem hiding this comment.
Thanks for the updates @BigDataDZ. I pulled these changes into my fork and tested the formula-copy paths locally, including saving and reopening the generated workbooks with Apache POI.
I found a few cases that still produce incorrect formulas or overwrite existing data. I left inline comments with the reproductions and the behavior I observed. Could you take another look and add regression coverage for these cases?
| // shifted range. Shift the cached formula text manually so copyFormulaCells | ||
| // translates the post-shift formula. | ||
| if (shiftNumber > 0) { | ||
| refreshFormulaCacheAfterShift(shiftMaxRow + 1, shiftNumber); |
There was a problem hiding this comment.
While checking the forceNewRow path, I noticed that the formula cache is refreshed even when shiftRows() doesn't actually move any rows.
In forceNewRowUpdatesFormulaCacheAfterShift(), the template contains only rows 0 and 1, so shiftRows() returns early. However, this call still shifts the cached reference from A4 to A5, and the copied row ends up with A6.
Since no structural shift occurred, copying the original formula down one row should produce A5, not A6.
Could we update the cache only when rows are actually shifted? I'd also suggest adding a test with a populated row below the collection to exercise the real Sheet.shiftRows() path.
| } | ||
|
|
||
| private static final java.util.regex.Pattern ROW_REF_PATTERN = | ||
| java.util.regex.Pattern.compile("(\\$?[A-Z]+\\$?)(\\d+)"); |
There was a problem hiding this comment.
I checked the row-reference replacement logic with a few different formula cases and found that it also modifies text that isn't an actual cell reference.
For example, with startRow=3 and amount=1:
INDIRECT("A4")becomesINDIRECT("A5").'SHEET4'!A4becomes'SHEET5'!A5.SUM(Table1[COL4])becomesSUM(Table1[COL5]).
I also noticed that a long identifier such as "ORDER12345678901234567890" can trigger a NumberFormatException when the matched digits are parsed as an integer.
It looks like the regex doesn't distinguish actual cell references from string literals, sheet names, or structured references.
Could we use POI's formula parsing and shifting utilities instead of modifying the raw formula string? I think that would be safer, especially for more complex formulas.
| } | ||
| } | ||
| return FormulaRenderer.toFormulaString((FormulaRenderingWorkbook) parsingWorkbook, ptgs); | ||
| } catch (Exception e) { |
There was a problem hiding this comment.
I noticed that if formula parsing or shifting fails, we silently return the original formula.
Since we're copying the formula to a different row or column, the original relative references might no longer point to the intended cells.
The PR description mentions that formulas which cannot be translated are skipped, but this path still attempts to write the untranslated formula.
Could we make this failure behavior more explicit? I think it would be safer to avoid silently writing a potentially incorrect formula, and a regression test for an unsupported formula would help clarify the expected behavior.
| String translatedFormula = | ||
| translateFormula(cachedSheet, templateFormulaCell, targetRowIndex, targetColumnIndex, vertical); | ||
| try { | ||
| targetCell.setCellFormula(translatedFormula); |
There was a problem hiding this comment.
I tested this locally with a full XLSX round trip and found that this can still overwrite an unrelated populated cell during horizontal fill.
For example, I used a template with {data.name} in A1, =A1*2 in C1, and Keep in D1, then filled two items horizontally. After saving the workbook and reopening it with Apache POI, D1 was a formula cell containing B1*2 instead of the original string Keep.
The existing horizontal test covers a formula placed in a placeholder destination column, but it does not cover a translated formula landing on a different populated cell. Could we preserve an existing non-empty destination cell, or define the collision behavior explicitly, and add this case as a round-trip regression test?
…failures
Third review round from skytin1004, three issues:
1. Formula cache was refreshed even when shiftRows() returned early
(no structural move). shiftRows now returns boolean and the cache
is refreshed only when rows actually shifted. The forceNewRow test
without rows below now expects the untranslated copy (A5*2), and a
new test with a populated row below exercises the real shift path.
2. The regex row-reference shifter modified string literals
(INDIRECT("A4")), sheet names ('SHEET4'!A4) and structured refs
(Table1[COL4]) and could overflow on long identifiers. Replaced
with POI FormulaShifter.createForRowShift which operates on parsed
Ptg tokens and only touches real cell references.
3. translateFormula silently returned the original formula when
parsing/shifting failed, producing wrong references in copied rows.
It now returns null and copyFormulaCells skips the formula instead
of writing an incorrect one.
Full fesod-sheet suite: 1050 tests, 0 failures.
Review: skytin1004 on apache#1215 (third round).
Generated-by: ZCode
|
@skytin1004 All three issues fixed in 702178f:
Full fesod-sheet suite: 1050 tests, 0 failures. Spotless clean. Thanks for the detailed reproductions — the |
Fixes #199
What changes were proposed in this pull request?
When a template row holds a formula cell next to
{data.xxx}placeholders, filling a list created one row per item but wrote only the placeholder columns - the formula silently disappeared from every created row, so formula columns next to the data went empty.The fill executor now records the formula cells that sit in the collection template rows and recreates each of them in every created row. References are translated with POI's
FormulaShifter(row copy for vertical fills, column copy for horizontal fills) the way a spreadsheet copies a row down: relative references follow the new position, absolute ones stay anchored. Cell style is carried over under the existingautoStylebehavior, and a formula that cannot be parsed or translated is skipped instead of failing the fill.How was this patch tested?
FillWithFormulaInCreatedRowsTest: a programmatic template mixing a relative formula (A2*2) and an absolute one ($A$2*10) with{data.name}; filling 3 items with and withoutforceNewRownow asserts the created rows carryA3*2/A4*2while$A$2*10stays anchored.mvn install -Dmaven.test.skip=false, JDK 25).Generated-by: ZCode
Signed-off-by: BigDataDZ 76271875+BigDataDZ@users.noreply.github.com