python-calamine-reducto 0.7.18__tar.gz → 0.7.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/Cargo.lock +2 -2
  2. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/Cargo.toml +1 -1
  3. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/PKG-INFO +1 -1
  4. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/README.md +2 -1
  5. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/python/python_calamine/_python_calamine.pyi +40 -1
  6. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/src/types/grid.rs +99 -23
  7. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/src/types/workbook.rs +584 -79
  8. python_calamine_reducto-0.7.20/tests/create_hyperlink_fixture.py +30 -0
  9. python_calamine_reducto-0.7.20/tests/data/hyperlink_test.xlsx +0 -0
  10. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/test_band.py +18 -0
  11. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/test_bounded_reads.py +13 -0
  12. python_calamine_reducto-0.7.20/tests/test_gap_compression.py +258 -0
  13. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/test_images.py +83 -2
  14. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/.github/workflows/CI.yml +0 -0
  15. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/.github/workflows/release.yml +0 -0
  16. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/.gitignore +0 -0
  17. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/.pre-commit-config.yaml +0 -0
  18. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/.python-version +0 -0
  19. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/CLAUDE.md +0 -0
  20. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/LICENSE +0 -0
  21. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/build.rs +0 -0
  22. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/pyproject.toml +0 -0
  23. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/python/python_calamine/__init__.py +0 -0
  24. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/python/python_calamine/py.typed +0 -0
  25. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/renovate.json +0 -0
  26. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/src/lib.rs +0 -0
  27. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/src/types/cell.rs +0 -0
  28. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/src/types/errors.rs +0 -0
  29. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/src/types/mod.rs +0 -0
  30. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/src/types/sheet.rs +0 -0
  31. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/src/types/table.rs +0 -0
  32. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/__init__.py +0 -0
  33. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/conftest.py +0 -0
  34. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/create_empty_merge_fixture.py +0 -0
  35. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/create_hidden_col_range_fixture.py +0 -0
  36. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/create_richtext_fixture.py +0 -0
  37. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/create_test_fixture.py +0 -0
  38. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/any_sheets.ods +0 -0
  39. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/any_sheets.xls +0 -0
  40. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/any_sheets.xlsb +0 -0
  41. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/any_sheets.xlsx +0 -0
  42. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/base.ods +0 -0
  43. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/base.xls +0 -0
  44. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/base.xlsb +0 -0
  45. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/base.xlsx +0 -0
  46. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/dropdown_test.xlsx +0 -0
  47. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/empty_file.ods +0 -0
  48. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/empty_file.xlsb +0 -0
  49. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/empty_file.xlsx +0 -0
  50. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/empty_merge_test.xlsx +0 -0
  51. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/formula_test.xlsx +0 -0
  52. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/grid_test.xlsx +0 -0
  53. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/hidden_col_range_test.xlsx +0 -0
  54. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/image_test.xlsx +0 -0
  55. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/issue139.xlsx +0 -0
  56. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/multi_image_test.xlsx +0 -0
  57. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/password.ods +0 -0
  58. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/password.xls +0 -0
  59. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/password.xlsb +0 -0
  60. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/password.xlsx +0 -0
  61. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/richtext_test.xlsx +0 -0
  62. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/table-multiple.xlsb +0 -0
  63. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/table-multiple.xlsx +0 -0
  64. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/data/temperature-table.xlsb +0 -0
  65. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/test_base.py +0 -0
  66. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/test_grid.py +0 -0
  67. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/test_pandas.py +0 -0
  68. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/test_sheet_type.py +0 -0
  69. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/test_tables.py +0 -0
  70. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/test_trim_header_only.py +0 -0
  71. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/test_uncached_formulas.py +0 -0
  72. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/tests/xlsx_builder.py +0 -0
  73. {python_calamine_reducto-0.7.18 → python_calamine_reducto-0.7.20}/uv.lock +0 -0
@@ -80,7 +80,7 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
80
80
  [[package]]
81
81
  name = "calamine"
82
82
  version = "0.32.0"
83
- source = "git+https://github.com/reductoai/calamine.git?branch=main#297ee32c41c7449e22f9fb94e93c0291b0b4d15d"
83
+ source = "git+https://github.com/reductoai/calamine.git?branch=main#5a6418cb25f78e47a841f51c42c85363ee2cd06b"
84
84
  dependencies = [
85
85
  "atoi_simd",
86
86
  "byteorder",
@@ -612,7 +612,7 @@ dependencies = [
612
612
 
613
613
  [[package]]
614
614
  name = "python-calamine-reducto"
615
- version = "0.7.18"
615
+ version = "0.7.20"
616
616
  dependencies = [
617
617
  "calamine",
618
618
  "chrono",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "python-calamine-reducto"
3
- version = "0.7.18"
3
+ version = "0.7.20"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-calamine-reducto
3
- Version: 0.7.18
3
+ Version: 0.7.20
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: 3.11
@@ -25,7 +25,8 @@ Our additions live on `main`.
25
25
  - **`get_sheet_grid(name, rows, cols, include_styles=True)`** → `CellGrid` from the dense range.
26
26
  - **`get_sheet_layout(name)`** → dict with row heights, column widths, defaults
27
27
  - **`get_sheet_formulas`**, **`get_sheet_merged_ranges`**, **`get_sheet_images`**,
28
- **`get_sheet_data_validations`**, **`get_sheet_freeze_panes`**,
28
+ **`get_sheet_data_validations`**, **`get_sheet_hyperlinks`**, **`get_sheet_hyperlink_formulas`**,
29
+ **`get_sheet_freeze_panes`**,
29
30
  **`get_sheet_filled_count`**, **`get_sheet_uncached_formula_count`**,
30
31
  **`get_all_tables_metadata`**
31
32
 
@@ -204,6 +204,21 @@ class DataValidation(typing.TypedDict):
204
204
  formula2: str | None
205
205
  allow_blank: bool
206
206
 
207
+ @typing.type_check_only
208
+ class Hyperlink(typing.TypedDict):
209
+ ref: str
210
+ """Cell or range reference, e.g. `A1` or `A1:C3`."""
211
+ rel_id: str | None
212
+ """The `r:id` of the link, if any."""
213
+ target: str | None
214
+ """Relationship target, e.g. a URL. `None` for a link with only a `location`, and for an
215
+ `r:id` that has no hyperlink relationship."""
216
+ location: str | None
217
+ """Place inside the target or the workbook, e.g. `Sheet2!A1` or a URL fragment."""
218
+ display: str | None
219
+ external: bool
220
+ """`True` when the relationship has `TargetMode="External"`."""
221
+
207
222
  @typing.final
208
223
  class CellProxy:
209
224
  """One cell of a `CellGrid`. Read a field with `cell["value"]`."""
@@ -218,6 +233,14 @@ class CellGrid:
218
233
  def shape(self) -> tuple[int, int]: ...
219
234
  @property
220
235
  def size(self) -> int: ...
236
+ @property
237
+ def row_coords(self) -> npt.NDArray[np.int32]:
238
+ """Grid row -> 0-based sheet row: `arange` plus the band top, unless gap compression engaged."""
239
+
240
+ @property
241
+ def col_coords(self) -> npt.NDArray[np.int32]:
242
+ """Grid column -> 0-based sheet column."""
243
+
221
244
  @property
222
245
  def uncached_formula_count(self) -> int | None:
223
246
  """Formula cells with no cached value (streaming xlsx grids), else `None`."""
@@ -253,7 +276,9 @@ class CellGrid:
253
276
  def apply_formatting_rust(self) -> None:
254
277
  """Format each value with its number format (SSF, in Rust)."""
255
278
 
256
- def apply_dateutil_reformat(self, date_parse: Callable[[str], float | None]) -> None:
279
+ def apply_dateutil_reformat(
280
+ self, date_parse: Callable[[str], float | None]
281
+ ) -> None:
257
282
  """Reformat text cells that have a date format. `date_parse` gives a date serial."""
258
283
 
259
284
  def apply_raw_as_formatted(self) -> None: ...
@@ -385,10 +410,14 @@ class CalamineWorkbook:
385
410
  trigger_density: float = 0.001,
386
411
  coverage: float = 0.999,
387
412
  trim_header_only: bool = False,
413
+ max_empty_run: int | None = None,
388
414
  ) -> tuple[CellGrid, int, int, int]:
389
415
  """Stream a grid. Returns `(grid, declared_rows, declared_cols, dropped_cells)`.
390
416
 
391
417
  `trim_header_only` also drops the trailing columns whose only value is in the header row.
418
+ `max_empty_run` keeps at most that many rows (and columns) of each empty run inside the
419
+ trimmed bounds. It engages only above `trigger_cells`, and only when it halves the grid.
420
+ `coord` and `merge_parent` stay in sheet positions; `row_coords`/`col_coords` map them.
392
421
  """
393
422
 
394
423
  def get_sheet_filled_count(self, name: str) -> tuple[int, int, int]:
@@ -416,6 +445,16 @@ class CalamineWorkbook:
416
445
  """`(data, extension, anchor_row, anchor_col)` for each image."""
417
446
 
418
447
  def get_sheet_data_validations(self, name: str) -> list[DataValidation]: ...
448
+ def get_sheet_hyperlinks(
449
+ self, name: str, limit: int | None = None
450
+ ) -> list[Hyperlink]:
451
+ """The links of the sheet's `<hyperlinks>` element, at most `limit`. Empty for xlsb, xls, and ods."""
452
+
453
+ def get_sheet_hyperlink_formulas(
454
+ self, name: str, limit: int | None = None
455
+ ) -> list[tuple[int, int, str]]:
456
+ """`(row, col, formula)` for each formula that contains `HYPERLINK`, at most `limit`. Empty for xlsb, xls, and ods."""
457
+
419
458
  def get_sheet_freeze_panes(self, name: str) -> tuple[int, int]:
420
459
  """`(frozen_rows, frozen_cols)`."""
421
460
 
@@ -185,7 +185,8 @@ pub struct CellGrid {
185
185
  // Styles (None when styling skipped for large sheets)
186
186
  pub(crate) styles: Option<CellStyles>,
187
187
 
188
- // Merge map: (row, col) -> (parent_row, parent_col), absolute 0-based
188
+ // Merge map: (row, col) -> (parent_row, parent_col), both grid indexes. An anchor
189
+ // above the grid (a band) is negative. `anchor_position` maps it for `merge_parent`.
189
190
  pub(crate) merge_map: HashMap<(usize, usize), (i32, i32)>,
190
191
 
191
192
  // Mutable boolean fields
@@ -211,6 +212,14 @@ pub struct CellGrid {
211
212
  pub(crate) coord_row_offset: usize,
212
213
  pub(crate) coord_col_offset: usize,
213
214
 
215
+ // Added to a merge anchor for `merge_parent` (the band top for `propagate_merges`).
216
+ pub(crate) anchor_offset: (i32, i32),
217
+
218
+ // Gap compression: grid row/col -> 0-based sheet row/col, ascending. When set,
219
+ // `coord` and `merge_parent` use them and the offsets are 0.
220
+ pub(crate) row_coords: Option<Vec<i32>>,
221
+ pub(crate) col_coords: Option<Vec<i32>>,
222
+
214
223
  // Formula cells with no cached value in the source sheet. None when unknown
215
224
  // (non-xlsx, dense path, or a derived grid).
216
225
  pub(crate) uncached_formula_count: Option<u64>,
@@ -259,6 +268,9 @@ impl CellGrid {
259
268
  merge_parent_default,
260
269
  coord_row_offset: 0,
261
270
  coord_col_offset: 0,
271
+ anchor_offset: (0, 0),
272
+ row_coords: None,
273
+ col_coords: None,
262
274
  uncached_formula_count: None,
263
275
  }
264
276
  }
@@ -280,6 +292,38 @@ impl CellGrid {
280
292
  row * self.cols + col
281
293
  }
282
294
 
295
+ /// The 0-based sheet row of a grid row.
296
+ #[inline]
297
+ fn sheet_row(&self, row: usize) -> usize {
298
+ match &self.row_coords {
299
+ Some(coords) => coords[row] as usize,
300
+ None => row + self.coord_row_offset,
301
+ }
302
+ }
303
+
304
+ /// The 0-based sheet column of a grid column.
305
+ #[inline]
306
+ fn sheet_col(&self, col: usize) -> usize {
307
+ match &self.col_coords {
308
+ Some(coords) => coords[col] as usize,
309
+ None => col + self.coord_col_offset,
310
+ }
311
+ }
312
+
313
+ /// The flat index of a merge anchor, `None` when the anchor is outside the grid.
314
+ fn anchor_index(&self, pr: i32, pc: i32) -> Option<usize> {
315
+ let (row, col) = (usize::try_from(pr).ok()?, usize::try_from(pc).ok()?);
316
+ (row < self.rows && col < self.cols).then(|| self.idx(row, col))
317
+ }
318
+
319
+ /// The `merge_parent` of a merge anchor. A compressed grid keeps every anchor.
320
+ fn anchor_position(&self, pr: i32, pc: i32) -> (i32, i32) {
321
+ match (&self.row_coords, &self.col_coords) {
322
+ (Some(rows), Some(cols)) => (rows[pr as usize], cols[pc as usize]),
323
+ _ => (pr + self.anchor_offset.0, pc + self.anchor_offset.1),
324
+ }
325
+ }
326
+
283
327
  /// `fill_numpy_array` empties the per-cell storage (`header` first). A later
284
328
  /// cell access would index an empty `Vec`, so refuse it.
285
329
  fn ensure_not_drained(&self) -> PyResult<()> {
@@ -324,7 +368,7 @@ impl CellGrid {
324
368
  // Both parent and child cells — openpyxl uses value emptiness, not coordinates.
325
369
  match self.merge_map.get(&(row, col)) {
326
370
  Some(&(pr, pc)) if matches!(self.raw_values[self.idx(row, col)], RawValue::Empty) => {
327
- (pr, pc)
371
+ self.anchor_position(pr, pc)
328
372
  }
329
373
  _ => self.merge_parent_default,
330
374
  }
@@ -368,8 +412,8 @@ impl CellGrid {
368
412
  }
369
413
  "coord" => {
370
414
  let arr = [
371
- (row + self.coord_row_offset + 1) as i32,
372
- (col + self.coord_col_offset + 1) as i32,
415
+ (self.sheet_row(row) + 1) as i32,
416
+ (self.sheet_col(col) + 1) as i32,
373
417
  ];
374
418
  let numpy = py.import("numpy")?;
375
419
  let result = numpy.call_method1("array", (arr.to_vec(), "int32"))?;
@@ -508,15 +552,8 @@ impl CellGrid {
508
552
  // For merged cells, use parent's style
509
553
  let (row, col) = (idx / self.cols, idx % self.cols);
510
554
  let style_idx = match self.merge_map.get(&(row, col)) {
511
- Some(&(pr, pc)) if pr >= 0 && pc >= 0 => {
512
- let pidx = pr as usize * self.cols + pc as usize;
513
- if pidx < total {
514
- pidx
515
- } else {
516
- idx
517
- }
518
- }
519
- _ => idx,
555
+ Some(&(pr, pc)) => self.anchor_index(pr, pc).unwrap_or(idx),
556
+ None => idx,
520
557
  };
521
558
  let id = styles.ids[style_idx];
522
559
  let (font, fill) = match names.entry(id) {
@@ -562,26 +599,43 @@ impl CellGrid {
562
599
  if r < self.rows && c < self.cols {
563
600
  let idx = r * self.cols + c;
564
601
  if self.raw_values[idx].is_empty() {
565
- data[idx * 2] = pr;
566
- data[idx * 2 + 1] = pc;
602
+ (data[idx * 2], data[idx * 2 + 1]) = self.anchor_position(pr, pc);
567
603
  }
568
604
  }
569
605
  }
570
606
  data
571
607
  }
572
608
 
573
- /// `coord` as flat 1-based `(row, col)` pairs, with the coord offsets.
609
+ /// `coord` as flat 1-based `(row, col)` sheet positions.
574
610
  fn coord_pairs(&self) -> Vec<i32> {
611
+ let col_coords: Vec<i32> = (0..self.cols)
612
+ .map(|c| (self.sheet_col(c) + 1) as i32)
613
+ .collect();
575
614
  let mut data = Vec::with_capacity(self.rows * self.cols * 2);
576
615
  for r in 0..self.rows {
577
- for c in 0..self.cols {
578
- data.push((r + self.coord_row_offset + 1) as i32);
579
- data.push((c + self.coord_col_offset + 1) as i32);
616
+ let row = (self.sheet_row(r) + 1) as i32;
617
+ for &col in &col_coords {
618
+ data.push(row);
619
+ data.push(col);
580
620
  }
581
621
  }
582
622
  data
583
623
  }
584
624
 
625
+ /// 0-based sheet indexes of the grid rows or columns, as a numpy `int32` array:
626
+ /// a copy of the compressed `coords`, else `offset..offset + n`.
627
+ fn sheet_coords<'py>(
628
+ py: Python<'py>,
629
+ coords: Option<&[i32]>,
630
+ n: usize,
631
+ offset: usize,
632
+ ) -> Bound<'py, PyArray1<i32>> {
633
+ match coords {
634
+ Some(coords) => PyArray1::from_slice(py, coords),
635
+ None => PyArray1::from_iter(py, (offset..offset + n).map(|i| i as i32)),
636
+ }
637
+ }
638
+
585
639
  fn font_bolds(&self) -> Vec<bool> {
586
640
  match &self.styles {
587
641
  Some(styles) => styles
@@ -752,6 +806,29 @@ impl CellGrid {
752
806
  self.rows * self.cols
753
807
  }
754
808
 
809
+ /// Grid row -> 0-based sheet row: `arange` plus the coord offset (the band top),
810
+ /// unless gap compression engaged.
811
+ #[getter]
812
+ fn row_coords<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<i32>> {
813
+ Self::sheet_coords(
814
+ py,
815
+ self.row_coords.as_deref(),
816
+ self.rows,
817
+ self.coord_row_offset,
818
+ )
819
+ }
820
+
821
+ /// Grid column -> 0-based sheet column, like `row_coords`.
822
+ #[getter]
823
+ fn col_coords<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<i32>> {
824
+ Self::sheet_coords(
825
+ py,
826
+ self.col_coords.as_deref(),
827
+ self.cols,
828
+ self.coord_col_offset,
829
+ )
830
+ }
831
+
755
832
  /// Formula cells with no cached value, counted by `get_sheet_grid_streaming`.
756
833
  /// `None` when unknown (non-xlsx workbook or a grid from another builder).
757
834
  #[getter]
@@ -1110,7 +1187,7 @@ impl CellGrid {
1110
1187
  self.merge_map.iter().map(|(&k, &v)| (k, v)).collect();
1111
1188
  for ((row, col), (pr, pc)) in entries {
1112
1189
  // Skip if this IS the parent
1113
- if pr == row as i32 && pc == col as i32 {
1190
+ if (pr, pc) == (row as i32, col as i32) {
1114
1191
  continue;
1115
1192
  }
1116
1193
  let idx = row * self.cols + col;
@@ -1119,9 +1196,8 @@ impl CellGrid {
1119
1196
  .as_ref()
1120
1197
  .map(|s| s.is_empty())
1121
1198
  .unwrap_or(true);
1122
- if child_empty && pr >= 0 && pc >= 0 {
1123
- let parent_idx = pr as usize * self.cols + pc as usize;
1124
- if parent_idx < self.formatted_values.len() {
1199
+ if child_empty {
1200
+ if let Some(parent_idx) = self.anchor_index(pr, pc) {
1125
1201
  self.formatted_values[idx] = self.formatted_values[parent_idx].clone();
1126
1202
  }
1127
1203
  }