xlsxrb 0.1.14 → 0.1.16

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +108 -0
  3. data/README.md +37 -17
  4. data/benchmark/micro_ips.rb +70 -0
  5. data/lib/xlsxrb/elements/styles.rb +427 -15
  6. data/lib/xlsxrb/elements/workbook.rb +67 -4
  7. data/lib/xlsxrb/elements/worksheet.rb +117 -7
  8. data/lib/xlsxrb/ooxml/cfb.rb +10 -0
  9. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +56 -38
  10. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +14 -2
  11. data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +12 -7
  12. data/lib/xlsxrb/ooxml/reader.rb +167 -139
  13. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +83 -28
  14. data/lib/xlsxrb/ooxml/styles_parser.rb +57 -14
  15. data/lib/xlsxrb/ooxml/utils.rb +59 -2
  16. data/lib/xlsxrb/ooxml/workbook_parser.rb +63 -4
  17. data/lib/xlsxrb/ooxml/workbook_writer.rb +232 -59
  18. data/lib/xlsxrb/ooxml/worksheet_parser.rb +192 -123
  19. data/lib/xlsxrb/ooxml/worksheet_writer.rb +307 -125
  20. data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +16 -12
  21. data/lib/xlsxrb/ooxml/writer/features_xml.rb +1 -1
  22. data/lib/xlsxrb/ooxml/writer/styles_xml.rb +64 -15
  23. data/lib/xlsxrb/ooxml/writer.rb +31 -28
  24. data/lib/xlsxrb/ooxml/xml_builder.rb +22 -2
  25. data/lib/xlsxrb/ooxml/xml_parser.rb +45 -0
  26. data/lib/xlsxrb/ooxml/zip_reader.rb +83 -32
  27. data/lib/xlsxrb/stream_row.rb +4 -3
  28. data/lib/xlsxrb/stream_sheet.rb +259 -7
  29. data/lib/xlsxrb/stream_writer.rb +197 -31
  30. data/lib/xlsxrb/utils.rb +68 -0
  31. data/lib/xlsxrb/version.rb +1 -1
  32. data/lib/xlsxrb/workbook_builder.rb +110 -19
  33. data/lib/xlsxrb/worksheet_builder.rb +526 -15
  34. data/lib/xlsxrb.rb +889 -90
  35. data/sig/generated/xlsxrb/elements/styles.rbs +271 -5
  36. data/sig/generated/xlsxrb/elements/workbook.rbs +54 -3
  37. data/sig/generated/xlsxrb/elements/worksheet.rbs +63 -2
  38. data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +2 -0
  39. data/sig/generated/xlsxrb/ooxml/reader.rbs +12 -0
  40. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +3 -1
  41. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +2 -0
  42. data/sig/generated/xlsxrb/ooxml/utils.rbs +22 -0
  43. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +7 -0
  44. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +13 -1
  45. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +18 -2
  46. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +13 -3
  47. data/sig/generated/xlsxrb/ooxml/writer/styles_xml.rbs +5 -2
  48. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +6 -1
  49. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +9 -6
  50. data/sig/generated/xlsxrb/stream_row.rbs +3 -2
  51. data/sig/generated/xlsxrb/stream_sheet.rbs +132 -6
  52. data/sig/generated/xlsxrb/stream_writer.rbs +118 -27
  53. data/sig/generated/xlsxrb/utils.rbs +45 -0
  54. data/sig/generated/xlsxrb/worksheet_builder.rbs +252 -9
  55. data/sig/generated/xlsxrb.rbs +84 -10
  56. metadata +2 -1
@@ -13,6 +13,7 @@ module Xlsxrb
13
13
  class WorksheetParser
14
14
  EMPTY_ARRAY = [].freeze
15
15
  EMPTY_HASH = {}.freeze
16
+ EMPTY_BINARY = "".b.freeze
16
17
  TAG_DELIM_BYTES = [32, 62, 9, 10, 13, 47].freeze
17
18
  ROW_ATTR_START_BYTES = [104, 99, 111, 115].freeze
18
19
 
@@ -214,10 +215,7 @@ module Xlsxrb
214
215
  tag_end = xml.index(">", row_start + row_start_len)
215
216
  break unless tag_end
216
217
 
217
- if xml.getbyte(tag_end - 1) == 47
218
- pos = tag_end + 1
219
- next
220
- end
218
+ is_self_closing = xml.getbyte(tag_end - 1) == 47
221
219
 
222
220
  # Row index and attrs from tag substring (bounded search)
223
221
  row_tag = xml.byteslice(row_start, tag_end - row_start)
@@ -227,9 +225,6 @@ module Xlsxrb
227
225
 
228
226
  attrs = extract_row_attrs(row_tag)
229
227
 
230
- row_end = xml.index(row_end_tag, tag_end + 1)
231
- break unless row_end
232
-
233
228
  row_source = { part: part_name, row: row_index }
234
229
  block.call(Event.new(
235
230
  type: :row_start,
@@ -237,6 +232,19 @@ module Xlsxrb
237
232
  source: row_source
238
233
  ))
239
234
 
235
+ if is_self_closing
236
+ block.call(Event.new(
237
+ type: :row_end,
238
+ args: [],
239
+ source: row_source
240
+ ))
241
+ pos = tag_end + 1
242
+ next
243
+ end
244
+
245
+ row_end = xml.index(row_end_tag, tag_end + 1)
246
+ break unless row_end
247
+
240
248
  fast_scan_cells_events(xml, tag_end + 1, row_end, shared_strings, row_source, prefix, &block)
241
249
 
242
250
  block.call(Event.new(
@@ -431,60 +439,128 @@ module Xlsxrb
431
439
  private_class_method :fast_scan_cells_events
432
440
 
433
441
  def self.extract_inline_text(xml, from, to)
442
+ xml = xml.b
434
443
  result = +""
435
444
  pos = from
436
445
  while pos < to
437
- t_start = xml.index("<", pos)
438
- break unless t_start && t_start < to
446
+ tag_start = xml.index("<", pos)
447
+ break unless tag_start && tag_start < to
448
+
449
+ # Check self-closing tag or closing tag
450
+ next_byte = xml.getbyte(tag_start + 1)
451
+ if next_byte == 47 # </...
452
+ tag_end = xml.index(">", tag_start + 2)
453
+ break unless tag_end
439
454
 
440
- t_term = xml.index("t", t_start)
441
- break unless t_term && t_term < to
455
+ pos = tag_end + 1
456
+ next
457
+ end
442
458
 
443
- t_tag_end = xml.index(">", t_term)
444
- break unless t_tag_end && t_tag_end < to
459
+ tag_end = xml.index(">", tag_start + 1)
460
+ break unless tag_end && tag_end <= to
445
461
 
446
- if xml.getbyte(t_tag_end - 1) == 47 # <t ... />
447
- pos = t_tag_end + 1
462
+ # Skip self-closing tags (<.../>)
463
+ if xml.getbyte(tag_end - 1) == 47
464
+ pos = tag_end + 1
448
465
  next
449
466
  end
450
467
 
451
- t_close = xml.index("</", t_tag_end + 1)
452
- break unless t_close && t_close <= to
468
+ tag_header = xml.byteslice(tag_start + 1, tag_end - tag_start - 1)
469
+ tag_name = tag_header.split(/[ \t\r\n]/, 2).first || ""
470
+ tag_localname = tag_name.include?(":") ? tag_name.split(":", 2).last : tag_name
471
+
472
+ if tag_localname == "rPh"
473
+ # Skip phonetic readings
474
+ close_rph = xml.index("</#{tag_name}>", tag_end + 1) || xml.index("</rPh>", tag_end + 1)
475
+ break unless close_rph && close_rph < to
453
476
 
454
- t_end = xml.index(">", t_close + 2)
455
- break unless t_end && t_end <= to
477
+ close_end = xml.index(">", close_rph)
478
+ break unless close_end
456
479
 
457
- text_segment = xml.byteslice(t_tag_end + 1, t_close - t_tag_end - 1).force_encoding("UTF-8")
458
- result << normalize_text(text_segment)
480
+ pos = close_end + 1
481
+ next
482
+ elsif tag_localname == "t"
483
+ close_t = xml.index("</#{tag_name}>", tag_end + 1) || xml.index("</t>", tag_end + 1)
484
+ break unless close_t && close_t <= to
459
485
 
460
- pos = t_end + 1
486
+ close_end = xml.index(">", close_t)
487
+ break unless close_end
488
+
489
+ text_segment = xml.byteslice(tag_end + 1, close_t - tag_end - 1).force_encoding("UTF-8")
490
+ result << normalize_text(text_segment)
491
+ pos = close_end + 1
492
+ next
493
+ else
494
+ pos = tag_end + 1
495
+ end
461
496
  end
462
497
  result
463
498
  end
464
499
 
465
500
  private_class_method :extract_inline_text
466
501
 
467
- def self.resolve_fast_value(raw, type, shared_strings)
502
+ STRICT_NUMERIC_RE = /\A[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\z/
503
+ private_constant :STRICT_NUMERIC_RE
504
+
505
+ # Resolves a cell value from its type attribute and parsed XML content (<v> or <is>).
506
+ # Consolidates parsing across fast-scan direct, row values, events, and listener paths.
507
+ #: (String? type, String? raw_value, String? is_xml, Array[String | Elements::RichText | untyped] shared_strings) -> untyped
508
+ def self.resolve_cell_value(type, raw_value, is_xml, shared_strings)
468
509
  case type
469
510
  when "s"
470
- shared_strings[raw.to_i] || ""
511
+ raw_value ? (shared_strings[raw_value.to_i] || "") : nil
471
512
  when "b"
472
- raw == "1"
473
- when "e", "str", "inlineStr"
474
- normalize_text(raw.force_encoding("UTF-8"))
513
+ if raw_value
514
+ v = raw_value.strip
515
+ v == "1" || v.casecmp?("true")
516
+ end
517
+ when "inlineStr", "str", "e"
518
+ if is_xml
519
+ extract_inline_text(is_xml, 0, is_xml.bytesize)
520
+ elsif raw_value
521
+ normalize_text(raw_value)
522
+ end
523
+ when "d"
524
+ raw_value && normalize_text(raw_value)
525
+ when "n"
526
+ raw_value ? numeric_cell_value(raw_value) : nil
475
527
  else
476
- return nil if raw.empty?
477
-
478
- if raw.include?(".")
479
- raw.to_f
480
- else
481
- raw.to_i
528
+ if raw_value
529
+ text = raw_value.strip
530
+ return nil if text.empty?
531
+
532
+ if text.match?(STRICT_NUMERIC_RE)
533
+ Integer(text, 10, exception: false) ||
534
+ Float(text, exception: false) ||
535
+ normalize_text(raw_value)
536
+ else
537
+ normalize_text(raw_value)
538
+ end
482
539
  end
483
540
  end
484
541
  end
485
542
 
543
+ def self.resolve_fast_value(raw, type, shared_strings)
544
+ resolve_cell_value(type, raw, nil, shared_strings)
545
+ end
546
+
486
547
  private_class_method :resolve_fast_value
487
548
 
549
+ # Converts the text of a numeric cell (<c t="n">) into an Integer or Float.
550
+ # An empty <v></v> carries no value and yields nil. Text that is not an xsd:double
551
+ # (e.g. "abc") cannot come from a conforming producer and raises ParseError
552
+ # instead of silently becoming 0.
553
+ #: (String raw) -> (Integer | Float)?
554
+ def self.numeric_cell_value(raw)
555
+ text = raw.strip
556
+ return nil if text.empty?
557
+
558
+ raise(ParseError, "invalid numeric cell value: #{text.inspect}") unless text.match?(STRICT_NUMERIC_RE)
559
+
560
+ Integer(text, 10, exception: false) || Float(text, exception: false) ||
561
+ raise(ParseError, "invalid numeric cell value: #{text.inspect}")
562
+ end
563
+
488
564
  # SECURITY NOTE: Decodes only standard predefined XML entities in a single pass.
489
565
  # This completely prevents "Billion Laughs" attacks (exponential entity expansion)
490
566
  # because it avoids recursive expansion and ignores custom entities entirely.
@@ -496,7 +572,8 @@ module Xlsxrb
496
572
  #: (String str) -> String
497
573
  def self.normalize_text(str)
498
574
  s = decode_xml_entities(str)
499
- s.include?("\r") ? s.gsub("\r\n", "\n").tr("\r", "\n") : s
575
+ s = s.gsub("\r\n", "\n").tr("\r", "\n") if s.include?("\r")
576
+ s.encoding == Encoding::BINARY ? s.dup.force_encoding(Encoding::UTF_8) : s
500
577
  end
501
578
 
502
579
  # Parses <cols> section for column definitions.
@@ -579,10 +656,7 @@ module Xlsxrb
579
656
  tag_end = xml.index(">", row_start + row_start_len)
580
657
  break unless tag_end
581
658
 
582
- if xml.getbyte(tag_end - 1) == 47
583
- pos = tag_end + 1
584
- next
585
- end
659
+ is_self_closing = xml.getbyte(tag_end - 1) == 47
586
660
 
587
661
  row_index = nil
588
662
  has_custom_attrs = false
@@ -618,6 +692,29 @@ module Xlsxrb
618
692
  EMPTY_HASH
619
693
  end
620
694
 
695
+ if is_self_closing
696
+ row_obj = StreamRow.fast_create(
697
+ row_index,
698
+ EMPTY_BINARY,
699
+ 0,
700
+ 0,
701
+ shared_strings,
702
+ prefix,
703
+ attrs[:height],
704
+ attrs[:hidden] || false,
705
+ attrs[:custom_height] || false,
706
+ attrs[:outline_level],
707
+ attrs[:style_index],
708
+ styles,
709
+ date1904,
710
+ attrs[:collapsed] || false,
711
+ pad_empty_cells
712
+ )
713
+ block.call(row_obj)
714
+ pos = tag_end + 1
715
+ next
716
+ end
717
+
621
718
  row_end = xml.index(row_end_tag, tag_end + 1)
622
719
  break unless row_end
623
720
 
@@ -726,17 +823,15 @@ module Xlsxrb
726
823
  tag_end = buffer.index(">", next_pos)
727
824
  break if tag_end.nil?
728
825
 
729
- if buffer.getbyte(tag_end - 1) == 47
730
- pos = tag_end + 1
731
- last_consumed_pos = pos
732
- next
733
- end
826
+ is_self_closing = buffer.getbyte(tag_end - 1) == 47
734
827
 
735
- row_end = buffer.index(row_end_tag, tag_end + 1)
736
- if row_end.nil?
737
- raise ArgumentError, "Malformed XML: row size exceeds maximum buffer limit" if buffer.bytesize > 50 * 1024 * 1024
828
+ unless is_self_closing
829
+ row_end = buffer.index(row_end_tag, tag_end + 1)
830
+ if row_end.nil?
831
+ raise ArgumentError, "Malformed XML: row size exceeds maximum buffer limit" if buffer.bytesize > 50 * 1024 * 1024
738
832
 
739
- break
833
+ break
834
+ end
740
835
  end
741
836
 
742
837
  row_index = nil
@@ -773,6 +868,30 @@ module Xlsxrb
773
868
  EMPTY_HASH
774
869
  end
775
870
 
871
+ if is_self_closing
872
+ row_obj = StreamRow.fast_create(
873
+ row_index,
874
+ EMPTY_BINARY,
875
+ 0,
876
+ 0,
877
+ shared_strings,
878
+ prefix,
879
+ attrs[:height],
880
+ attrs[:hidden] || false,
881
+ attrs[:custom_height] || false,
882
+ attrs[:outline_level],
883
+ attrs[:style_index],
884
+ styles,
885
+ date1904,
886
+ attrs[:collapsed] || false,
887
+ pad_empty_cells
888
+ )
889
+ block.call(row_obj)
890
+ pos = tag_end + 1
891
+ last_consumed_pos = pos
892
+ next
893
+ end
894
+
776
895
  cells_xml = buffer.byteslice(tag_end + 1, row_end - (tag_end + 1))
777
896
  row_obj = StreamRow.fast_create(
778
897
  row_index,
@@ -846,11 +965,11 @@ module Xlsxrb
846
965
  [COL_LETTERS[idx], idx]
847
966
  end.freeze
848
967
 
849
- CELL_FAST_RE = %r{<(?:[a-zA-Z0-9_]+:)?c\s+r="([A-Za-z0-9]+)"(?:[^>]*?s="(\d+)")?(?:[^>]*?t="([a-zA-Z]+)")?[^>]*>\s*(?:<(?:[a-zA-Z0-9_]+:)?f\b[^>]*?(?:/>|>([^<]*)</(?:[a-zA-Z0-9_]+:)?f>)\s*)?(?:<(?:[a-zA-Z0-9_]+:)?v>([^<]*)</(?:[a-zA-Z0-9_]+:)?v>\s*)?(?:<(?:[a-zA-Z0-9_]+:)?is>(.*?)</(?:[a-zA-Z0-9_]+:)?is>\s*)?.*?</(?:[a-zA-Z0-9_]+:)?c>|<(?:[a-zA-Z0-9_]+:)?c\s+r="([A-Za-z0-9]+)"(?:[^>]*?s="(\d+)")?[^>]*/>}m
968
+ CELL_FAST_RE = %r{<(?:[a-zA-Z0-9_]+:)?c\b([^>]*(?<!/))>\s*(?:<(?:[a-zA-Z0-9_]+:)?f\b[^>]*?(?:/>|>([^<]*)</(?:[a-zA-Z0-9_]+:)?f>)\s*)?(?:<(?:[a-zA-Z0-9_]+:)?v>([^<]*)</(?:[a-zA-Z0-9_]+:)?v>\s*)?(?:<(?:[a-zA-Z0-9_]+:)?is>(.*?)</(?:[a-zA-Z0-9_]+:)?is>\s*)?.*?</(?:[a-zA-Z0-9_]+:)?c>|<(?:[a-zA-Z0-9_]+:)?c\b([^>]*)/?>}m
850
969
  CELL_GENERIC_RE = %r{<(?:[a-zA-Z0-9_]+:)?c\b([^>]*?)(?:>\s*(?:<(?:[a-zA-Z0-9_]+:)?f\b[^>]*?(?:/>|>([^<]*)</(?:[a-zA-Z0-9_]+:)?f>)\s*)?(?:<(?:[a-zA-Z0-9_]+:)?v>([^<]*)</(?:[a-zA-Z0-9_]+:)?v>\s*)?(?:<(?:[a-zA-Z0-9_]+:)?is>(.*?)</(?:[a-zA-Z0-9_]+:)?is>\s*)?.*?</(?:[a-zA-Z0-9_]+:)?c>|/>)}m
851
- ATTR_R = /r="([A-Za-z0-9]+)"/
852
- ATTR_T = /t="([a-zA-Z]+)"/
853
- ATTR_S = /s="(\d+)"/
970
+ ATTR_R = /\br="([A-Za-z0-9]+)"/
971
+ ATTR_T = /\bt="([a-zA-Z]+)"/
972
+ ATTR_S = /\bs="(\d+)"/
854
973
 
855
974
  def self.fast_parse_cells_direct(xml, from, to, shared_strings, row_source, prefix = "")
856
975
  cells = []
@@ -866,14 +985,17 @@ module Xlsxrb
866
985
  col_idx = 0
867
986
  matched_any = false
868
987
 
869
- chunk.scan(CELL_FAST_RE) do |r, s, t, f, v, is, self_r, self_s|
988
+ chunk.scan(CELL_FAST_RE) do |attrs_str, f, v, is, self_attrs|
870
989
  matched_any = true
871
- ref = r || self_r
872
- s_val = s || self_s
873
- style_idx = s_val&.to_i
990
+ raw_attrs = attrs_str || self_attrs || ""
991
+ r = raw_attrs[ATTR_R, 1]
992
+ s = raw_attrs.include?('s="') ? raw_attrs[ATTR_S, 1] : nil
993
+ t = raw_attrs.include?('t="') ? raw_attrs[ATTR_T, 1] : nil
994
+ style_idx = s&.to_i
995
+ ref = r
874
996
  c_idx = if ref
875
997
  expected_letter = COL_LETTERS[col_idx]
876
- if expected_letter && ref.start_with?(expected_letter)
998
+ if expected_letter && ref.start_with?(expected_letter) && ref.getbyte(expected_letter.bytesize)&.between?(48, 57)
877
999
  col_idx
878
1000
  else
879
1001
  c_len = 0
@@ -888,21 +1010,7 @@ module Xlsxrb
888
1010
  v = v.strip if v
889
1011
  f = f.strip if f
890
1012
 
891
- val = if t == "s"
892
- shared_strings[v.to_i] || ""
893
- elsif t == "b"
894
- v == "1"
895
- elsif %w[inlineStr str e].include?(t)
896
- if is
897
- extract_inline_text(is, 0, is.bytesize)
898
- elsif v
899
- normalize_text(v)
900
- else
901
- ""
902
- end
903
- elsif v
904
- v.include?(".") ? v.to_f : v.to_i
905
- end
1013
+ val = resolve_cell_value(t, v, is, shared_strings)
906
1014
 
907
1015
  raw_val = if is
908
1016
  extract_inline_text(is, 0, is.bytesize)
@@ -927,7 +1035,7 @@ module Xlsxrb
927
1035
 
928
1036
  c_idx = if r
929
1037
  expected_letter = COL_LETTERS[col_idx]
930
- if expected_letter && r.start_with?(expected_letter)
1038
+ if expected_letter && r.start_with?(expected_letter) && r.getbyte(expected_letter.bytesize)&.between?(48, 57)
931
1039
  col_idx
932
1040
  else
933
1041
  c_len = 0
@@ -942,21 +1050,7 @@ module Xlsxrb
942
1050
  v = v.strip if v
943
1051
  f = f.strip if f
944
1052
 
945
- val = if t == "s"
946
- shared_strings[v.to_i] || ""
947
- elsif t == "b"
948
- v == "1"
949
- elsif %w[inlineStr str e].include?(t)
950
- if is
951
- extract_inline_text(is, 0, is.bytesize)
952
- elsif v
953
- normalize_text(v)
954
- else
955
- ""
956
- end
957
- elsif v
958
- v.include?(".") ? v.to_f : v.to_i
959
- end
1053
+ val = resolve_cell_value(t, v, is, shared_strings)
960
1054
 
961
1055
  raw_val = if is
962
1056
  extract_inline_text(is, 0, is.bytesize)
@@ -974,7 +1068,7 @@ module Xlsxrb
974
1068
  end
975
1069
 
976
1070
  # rubocop:disable Style/OptionalBooleanParameter
977
- #: (String xml, Integer from, Integer to, Array[String] shared_strings, ?untyped styles, ?bool date1904, ?type_cast: bool) -> Array[untyped]
1071
+ #: (String xml, Integer from, Integer to, Array[String | Elements::RichText | untyped] shared_strings, ?untyped styles, ?bool date1904, ?type_cast: bool) -> Array[untyped]
978
1072
  def self.fast_scan_row_values(xml, from, to, shared_strings, styles = nil, date1904 = false, type_cast: false)
979
1073
  # rubocop:enable Style/OptionalBooleanParameter
980
1074
  chunk = xml.byteslice(from, to - from)
@@ -984,13 +1078,16 @@ module Xlsxrb
984
1078
  matched_any = false
985
1079
  row_values = []
986
1080
 
987
- chunk.scan(CELL_FAST_RE) do |r, s, t, _f, v, is, self_r, self_s|
1081
+ chunk.scan(CELL_FAST_RE) do |attrs_str, _f, v, is, self_attrs|
988
1082
  matched_any = true
989
- ref = r || self_r
990
- s_val = s || self_s
1083
+ raw_attrs = attrs_str || self_attrs || ""
1084
+ r = raw_attrs[ATTR_R, 1]
1085
+ s_val = raw_attrs.include?('s="') ? raw_attrs[ATTR_S, 1] : nil
1086
+ t = raw_attrs.include?('t="') ? raw_attrs[ATTR_T, 1] : nil
1087
+ ref = r
991
1088
  c_idx = if ref
992
1089
  expected_letter = COL_LETTERS[col_idx]
993
- if expected_letter && ref.start_with?(expected_letter)
1090
+ if expected_letter && ref.start_with?(expected_letter) && ref.getbyte(expected_letter.bytesize)&.between?(48, 57)
994
1091
  col_idx
995
1092
  else
996
1093
  c_len = 0
@@ -1004,21 +1101,7 @@ module Xlsxrb
1004
1101
  col_idx = c_idx + 1
1005
1102
  v = v.strip if v
1006
1103
 
1007
- val = if t == "s"
1008
- shared_strings[v.to_i] || ""
1009
- elsif t == "b"
1010
- v == "1"
1011
- elsif %w[inlineStr str e].include?(t)
1012
- if is
1013
- extract_inline_text(is, 0, is.bytesize)
1014
- elsif v
1015
- normalize_text(v)
1016
- else
1017
- ""
1018
- end
1019
- elsif v
1020
- v.include?(".") ? v.to_f : v.to_i
1021
- end
1104
+ val = resolve_cell_value(t, v, is, shared_strings)
1022
1105
 
1023
1106
  if type_cast && val.is_a?(Numeric) && s_val && styles
1024
1107
  fmt_type = NumberFormatter.format_type_for(s_val.to_i, styles)
@@ -1047,7 +1130,7 @@ module Xlsxrb
1047
1130
 
1048
1131
  c_idx = if r
1049
1132
  expected_letter = COL_LETTERS[col_idx]
1050
- if expected_letter && r.start_with?(expected_letter)
1133
+ if expected_letter && r.start_with?(expected_letter) && r.getbyte(expected_letter.bytesize)&.between?(48, 57)
1051
1134
  col_idx
1052
1135
  else
1053
1136
  c_len = 0
@@ -1061,21 +1144,7 @@ module Xlsxrb
1061
1144
  col_idx = c_idx + 1
1062
1145
  v = v.strip if v
1063
1146
 
1064
- val = if t == "s"
1065
- shared_strings[v.to_i] || ""
1066
- elsif t == "b"
1067
- v == "1"
1068
- elsif %w[inlineStr str e].include?(t)
1069
- if is
1070
- extract_inline_text(is, 0, is.bytesize)
1071
- elsif v
1072
- normalize_text(v)
1073
- else
1074
- ""
1075
- end
1076
- elsif v
1077
- v.include?(".") ? v.to_f : v.to_i
1078
- end
1147
+ val = resolve_cell_value(t, v, is, shared_strings)
1079
1148
 
1080
1149
  if type_cast && val.is_a?(Numeric) && s && styles
1081
1150
  fmt_type = NumberFormatter.format_type_for(s.to_i, styles)