xlsxrb 0.1.12 → 0.1.14

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 (198) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +116 -42
  3. data/README.md +28 -22
  4. data/lib/xlsxrb/colors.rb +118 -0
  5. data/lib/xlsxrb/dsl_helpers.rb +59 -0
  6. data/lib/xlsxrb/elements/cell.rb +205 -16
  7. data/lib/xlsxrb/elements/column.rb +6 -4
  8. data/lib/xlsxrb/elements/coordinate_access.rb +32 -0
  9. data/lib/xlsxrb/elements/image.rb +128 -0
  10. data/lib/xlsxrb/elements/row.rb +206 -9
  11. data/lib/xlsxrb/elements/styles.rb +157 -0
  12. data/lib/xlsxrb/elements/types.rb +95 -1
  13. data/lib/xlsxrb/elements/workbook.rb +96 -4
  14. data/lib/xlsxrb/elements/worksheet.rb +424 -29
  15. data/lib/xlsxrb/elements.rb +2 -0
  16. data/lib/xlsxrb/number_formatter.rb +429 -0
  17. data/lib/xlsxrb/ooxml/cfb.rb +1 -1
  18. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +7 -5
  19. data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +11 -4
  20. data/lib/xlsxrb/ooxml/reader.rb +12 -14
  21. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +10 -6
  22. data/lib/xlsxrb/ooxml/styles_parser.rb +39 -9
  23. data/lib/xlsxrb/ooxml/utils.rb +34 -18
  24. data/lib/xlsxrb/ooxml/workbook_parser.rb +53 -15
  25. data/lib/xlsxrb/ooxml/workbook_writer.rb +70 -21
  26. data/lib/xlsxrb/ooxml/worksheet_parser.rb +466 -63
  27. data/lib/xlsxrb/ooxml/worksheet_writer.rb +206 -61
  28. data/lib/xlsxrb/ooxml/writer/features_xml.rb +4 -2
  29. data/lib/xlsxrb/ooxml/writer.rb +16 -1
  30. data/lib/xlsxrb/ooxml/xml_builder.rb +20 -0
  31. data/lib/xlsxrb/ooxml/zip_reader.rb +452 -82
  32. data/lib/xlsxrb/ooxml/zip_writer.rb +32 -5
  33. data/lib/xlsxrb/stream_row.rb +243 -16
  34. data/lib/xlsxrb/stream_sheet.rb +504 -17
  35. data/lib/xlsxrb/stream_writer.rb +246 -83
  36. data/lib/xlsxrb/style_builder.rb +308 -22
  37. data/lib/xlsxrb/utils.rb +158 -0
  38. data/lib/xlsxrb/version.rb +1 -1
  39. data/lib/xlsxrb/workbook_builder.rb +74 -4
  40. data/lib/xlsxrb/worksheet_builder.rb +230 -47
  41. data/lib/xlsxrb.rb +1005 -229
  42. data/sig/generated/xlsxrb/colors.rbs +40 -0
  43. data/sig/generated/xlsxrb/dsl_helpers.rbs +32 -0
  44. data/sig/generated/xlsxrb/elements/cell.rbs +113 -8
  45. data/sig/generated/xlsxrb/elements/column.rbs +3 -2
  46. data/sig/generated/xlsxrb/elements/coordinate_access.rbs +18 -0
  47. data/sig/generated/xlsxrb/elements/image.rbs +82 -0
  48. data/sig/generated/xlsxrb/elements/row.rbs +83 -4
  49. data/sig/generated/xlsxrb/elements/styles.rbs +79 -0
  50. data/sig/generated/xlsxrb/elements/types.rbs +43 -0
  51. data/sig/generated/xlsxrb/elements/workbook.rbs +58 -2
  52. data/sig/generated/xlsxrb/elements/worksheet.rbs +231 -18
  53. data/sig/generated/xlsxrb/number_formatter.rbs +132 -0
  54. data/sig/generated/xlsxrb/ooxml/reader.rbs +6 -0
  55. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +2 -2
  56. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +5 -2
  57. data/sig/generated/xlsxrb/ooxml/utils.rbs +17 -8
  58. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +9 -3
  59. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +31 -8
  60. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +3 -1
  61. data/sig/generated/xlsxrb/ooxml/writer.rbs +2 -0
  62. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +11 -0
  63. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +106 -8
  64. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +5 -1
  65. data/sig/generated/xlsxrb/stream_row.rbs +100 -6
  66. data/sig/generated/xlsxrb/stream_sheet.rbs +288 -14
  67. data/sig/generated/xlsxrb/stream_writer.rbs +120 -14
  68. data/sig/generated/xlsxrb/style_builder.rbs +55 -18
  69. data/sig/generated/xlsxrb/utils.rbs +119 -0
  70. data/sig/generated/xlsxrb/workbook_builder.rbs +26 -0
  71. data/sig/generated/xlsxrb/worksheet_builder.rbs +107 -11
  72. data/sig/generated/xlsxrb.rbs +202 -36
  73. metadata +16 -146
  74. data/.devcontainer/Dockerfile +0 -65
  75. data/.devcontainer/devcontainer.json +0 -17
  76. data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +0 -9
  77. data/.gem_rbs_collection/ast/2.4/ast.rbs +0 -73
  78. data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +0 -9
  79. data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +0 -4
  80. data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +0 -16
  81. data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +0 -96
  82. data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +0 -4
  83. data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +0 -68
  84. data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +0 -249
  85. data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +0 -4
  86. data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +0 -47
  87. data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +0 -5
  88. data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +0 -9
  89. data/.gem_rbs_collection/csv/3.3/csv.rbs +0 -3871
  90. data/.gem_rbs_collection/csv/3.3/manifest.yaml +0 -3
  91. data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +0 -9
  92. data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +0 -48
  93. data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +0 -9
  94. data/.gem_rbs_collection/listen/3.9/listen.rbs +0 -25
  95. data/.gem_rbs_collection/listen/3.9/listener.rbs +0 -24
  96. data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +0 -9
  97. data/.gem_rbs_collection/logger/1.7/formatter.rbs +0 -45
  98. data/.gem_rbs_collection/logger/1.7/log_device.rbs +0 -100
  99. data/.gem_rbs_collection/logger/1.7/logger.rbs +0 -796
  100. data/.gem_rbs_collection/logger/1.7/manifest.yaml +0 -2
  101. data/.gem_rbs_collection/logger/1.7/period.rbs +0 -17
  102. data/.gem_rbs_collection/logger/1.7/severity.rbs +0 -34
  103. data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +0 -9
  104. data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +0 -2332
  105. data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +0 -4
  106. data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +0 -9
  107. data/.gem_rbs_collection/parallel/1.20/parallel.rbs +0 -86
  108. data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +0 -9
  109. data/.gem_rbs_collection/parser/3.2/manifest.yaml +0 -7
  110. data/.gem_rbs_collection/parser/3.2/parser.rbs +0 -194
  111. data/.gem_rbs_collection/parser/3.2/polyfill.rbs +0 -4
  112. data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +0 -9
  113. data/.gem_rbs_collection/rainbow/3.0/global.rbs +0 -7
  114. data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +0 -209
  115. data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +0 -5
  116. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +0 -9
  117. data/.gem_rbs_collection/rake/13.0/manifest.yaml +0 -2
  118. data/.gem_rbs_collection/rake/13.0/rake.rbs +0 -39
  119. data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +0 -9
  120. data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +0 -17
  121. data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +0 -9
  122. data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +0 -208
  123. data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +0 -9
  124. data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +0 -903
  125. data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +0 -9
  126. data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +0 -8
  127. data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +0 -42
  128. data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +0 -5
  129. data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +0 -47
  130. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +0 -30
  131. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +0 -9
  132. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +0 -7
  133. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +0 -19
  134. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +0 -31
  135. data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +0 -18
  136. data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +0 -12
  137. data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +0 -11
  138. data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +0 -13
  139. data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +0 -95
  140. data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +0 -31
  141. data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +0 -58
  142. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +0 -24
  143. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +0 -17
  144. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +0 -23
  145. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +0 -22
  146. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +0 -30
  147. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +0 -20
  148. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +0 -15
  149. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +0 -26
  150. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +0 -21
  151. data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +0 -131
  152. data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +0 -14
  153. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +0 -33
  154. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +0 -21
  155. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +0 -63
  156. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +0 -55
  157. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +0 -35
  158. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +0 -7
  159. data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +0 -10
  160. data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +0 -22
  161. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +0 -29
  162. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +0 -17
  163. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +0 -13
  164. data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +0 -10
  165. data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +0 -8
  166. data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +0 -6
  167. data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +0 -30
  168. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +0 -10
  169. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +0 -10
  170. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +0 -5
  171. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +0 -15
  172. data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +0 -3
  173. data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +0 -40
  174. data/.mutant.yml +0 -8
  175. data/Rakefile +0 -633
  176. data/Steepfile +0 -18
  177. data/benchmark.rb +0 -406
  178. data/docs/ARCHITECTURE.md +0 -514
  179. data/docs/DEVELOPMENT.md +0 -137
  180. data/docs/MUTATION_TESTING.md +0 -52
  181. data/docs/PEER_LIBRARIES.md +0 -121
  182. data/docs/QUALITY_ASSURANCE.md +0 -26
  183. data/docs/SPEC_SOURCES.md +0 -48
  184. data/docs/assets/benchmark_results.svg +0 -144
  185. data/docs/assets/lsp_autocompletion.png +0 -0
  186. data/docs/assets/playground_preview.png +0 -0
  187. data/docs/coi-serviceworker.js +0 -82
  188. data/docs/office_thread.js +0 -77
  189. data/docs/preview.html +0 -719
  190. data/docs/visual/VisualGallery.md +0 -4683
  191. data/docs/wasm/ruby.wasm +0 -0
  192. data/docs/wasm/wasm_doc_helper.css +0 -351
  193. data/docs/wasm/wasm_doc_helper.js +0 -439
  194. data/docs/zeta.js +0 -1107
  195. data/rbs_collection.lock.yaml +0 -252
  196. data/rbs_collection.yaml +0 -19
  197. data/vendor/sdk_runner/Program.cs +0 -93
  198. data/vendor/sdk_runner/sdk_runner.csproj +0 -13
@@ -16,7 +16,7 @@ module Xlsxrb
16
16
  #
17
17
  # @api public
18
18
  class Cell
19
- attr_reader :row_index, :column_index, :value, :formula, :style_index, :unmapped_data, :errors
19
+ attr_reader :row_index, :column_index, :value, :formula, :style_index, :unmapped_data, :errors, :hyperlink, :comment, :raw_value, :format_code, :date1904
20
20
 
21
21
  # @param row_index [Integer] 0-based row index.
22
22
  # @param column_index [Integer] 0-based column index.
@@ -25,8 +25,13 @@ module Xlsxrb
25
25
  # @param style_index [Integer, String, nil] Style identifier.
26
26
  # @param unmapped_data [Hash] Additional metadata.
27
27
  # @param errors [Array<String>, nil] Validation errors.
28
- #: (row_index: untyped, column_index: untyped, ?value: untyped, ?formula: (Elements::Formula | String)?, ?style_index: (Integer | String)?, ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
29
- def initialize(row_index:, column_index:, value: nil, formula: nil, style_index: nil, unmapped_data: EMPTY_HASH, errors: nil)
28
+ # @param hyperlink [Hash, String, Elements::Hyperlink, nil] Optional hyperlink metadata or URL.
29
+ # @param comment [Hash, String, nil] Optional cell comment or note.
30
+ # @param raw_value [String, nil] Optional unparsed raw string value.
31
+ # @param format_code [String, nil] Optional OpenXML number/date format code.
32
+ # @param date1904 [Boolean] Whether the cell belongs to a 1904 date system workbook.
33
+ #: (row_index: untyped, column_index: untyped, ?value: untyped, ?formula: (Elements::Formula | String)?, ?style_index: (Integer | String)?, ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?, ?hyperlink: (Hash[Symbol, untyped] | String | Elements::Hyperlink)?, ?comment: (Hash[Symbol, untyped] | String)?, ?raw_value: String?, ?format_code: String?, ?date1904: bool) -> void
34
+ def initialize(row_index:, column_index:, value: nil, formula: nil, style_index: nil, unmapped_data: EMPTY_HASH, errors: nil, hyperlink: nil, comment: nil, raw_value: nil, format_code: nil, date1904: false)
30
35
  @row_index = row_index
31
36
  @column_index = column_index
32
37
  @value = value
@@ -35,9 +40,15 @@ module Xlsxrb
35
40
  @unmapped_data = unmapped_data || EMPTY_HASH
36
41
  computed_errors = errors || self.class.validate(row_index, column_index, value)
37
42
  @errors = computed_errors.frozen? ? computed_errors : computed_errors.freeze
43
+ @hyperlink = hyperlink
44
+ @comment = comment
45
+ @raw_value = raw_value
46
+ @format_code = format_code
47
+ @date1904 = date1904 ? true : false
38
48
  end
39
49
 
40
- def self.fast_create(row_index, column_index, value, style_index = nil, formula = nil)
50
+ # rubocop:disable Style/OptionalBooleanParameter
51
+ def self.fast_create(row_index, column_index, value, style_index = nil, formula = nil, raw_value = nil, format_code = nil, date1904 = false)
41
52
  inst = allocate
42
53
  inst.instance_variable_set(:@row_index, row_index)
43
54
  inst.instance_variable_set(:@column_index, column_index)
@@ -46,8 +57,14 @@ module Xlsxrb
46
57
  inst.instance_variable_set(:@style_index, style_index)
47
58
  inst.instance_variable_set(:@unmapped_data, EMPTY_HASH)
48
59
  inst.instance_variable_set(:@errors, EMPTY_ERRORS)
60
+ inst.instance_variable_set(:@hyperlink, nil)
61
+ inst.instance_variable_set(:@comment, nil)
62
+ inst.instance_variable_set(:@raw_value, raw_value)
63
+ inst.instance_variable_set(:@format_code, format_code)
64
+ inst.instance_variable_set(:@date1904, date1904 ? true : false)
49
65
  inst
50
66
  end
67
+ # rubocop:enable Style/OptionalBooleanParameter
51
68
 
52
69
  def ==(other)
53
70
  other.is_a?(Cell) &&
@@ -57,21 +74,26 @@ module Xlsxrb
57
74
  formula == other.formula &&
58
75
  style_index == other.style_index &&
59
76
  unmapped_data == other.unmapped_data &&
60
- errors == other.errors
77
+ errors == other.errors &&
78
+ hyperlink == other.hyperlink &&
79
+ comment == other.comment &&
80
+ raw_value == other.raw_value &&
81
+ format_code == other.format_code
61
82
  end
62
83
  alias eql? ==
63
84
 
64
85
  def hash
65
- [row_index, column_index, value, formula, style_index, unmapped_data, errors].hash
86
+ [row_index, column_index, value, formula, style_index, unmapped_data, errors, hyperlink, comment, raw_value, format_code].hash
66
87
  end
67
88
 
68
89
  def deconstruct
69
- [row_index, column_index, value, formula, style_index, unmapped_data, errors]
90
+ [row_index, column_index, value, formula, style_index, unmapped_data, errors, hyperlink, comment, raw_value, format_code]
70
91
  end
71
92
 
72
93
  def deconstruct_keys(keys)
73
94
  h = { row_index: row_index, column_index: column_index, value: value, formula: formula,
74
- style_index: style_index, unmapped_data: unmapped_data, errors: errors }
95
+ style_index: style_index, unmapped_data: unmapped_data, errors: errors, hyperlink: hyperlink,
96
+ comment: comment, raw_value: raw_value, format_code: format_code }
75
97
  keys ? h.slice(*keys) : h
76
98
  end
77
99
 
@@ -83,7 +105,12 @@ module Xlsxrb
83
105
  formula: changes.fetch(:formula, formula),
84
106
  style_index: changes.fetch(:style_index, style_index),
85
107
  unmapped_data: changes.fetch(:unmapped_data, unmapped_data),
86
- errors: changes.fetch(:errors, errors)
108
+ errors: changes.fetch(:errors, errors),
109
+ hyperlink: changes.fetch(:hyperlink, hyperlink),
110
+ comment: changes.fetch(:comment, comment),
111
+ raw_value: changes.fetch(:raw_value, raw_value),
112
+ format_code: changes.fetch(:format_code, format_code),
113
+ date1904: changes.fetch(:date1904, @date1904 || false)
87
114
  )
88
115
  end
89
116
 
@@ -104,9 +131,120 @@ module Xlsxrb
104
131
  "#{self.class.column_letter(column_index)}#{row_index + 1}"
105
132
  end
106
133
 
134
+ # Returns whether the cell contains a formula.
135
+ #
136
+ # @return [Boolean]
137
+ # @api public
138
+ #: () -> bool
139
+ def formula?
140
+ if formula.is_a?(Formula)
141
+ !formula.expression.nil? && !formula.expression.empty?
142
+ else
143
+ !formula.nil? && !formula.to_s.empty?
144
+ end
145
+ end
146
+
147
+ # Returns the formula expression string without leading '=', or nil.
148
+ #
149
+ # @return [String, nil]
150
+ # @api public
151
+ #: () -> String?
152
+ def formula_expression
153
+ expr = formula.is_a?(Formula) ? formula.expression : formula
154
+ return nil if expr.nil?
155
+
156
+ str = expr.to_s
157
+ str.empty? ? nil : str
158
+ end
159
+
160
+ # Returns whether the cell contains a hyperlink.
161
+ #
162
+ # @return [Boolean]
163
+ # @api public
164
+ #: () -> bool
165
+ def link?
166
+ case hyperlink
167
+ when nil then false
168
+ when Hash, String then !hyperlink.empty?
169
+ else true
170
+ end
171
+ end
172
+ alias hyperlink? link?
173
+
174
+ # Returns the hyperlink target URL or location string, or nil.
175
+ #
176
+ # @return [String, nil]
177
+ # @api public
178
+ #: () -> String?
179
+ def url
180
+ case hyperlink
181
+ when Hash
182
+ u = hyperlink[:url] || hyperlink[:location]
183
+ u&.to_s
184
+ when String
185
+ hyperlink.empty? ? nil : hyperlink
186
+ when Elements::Hyperlink
187
+ (hyperlink.url || hyperlink.location)&.to_s
188
+ end
189
+ end
190
+
191
+ # Returns the hyperlink tooltip (ScreenTip) text, or nil.
192
+ #
193
+ # @return [String, nil]
194
+ # @api public
195
+ #: () -> String?
196
+ def tooltip
197
+ case hyperlink
198
+ when Hash
199
+ hyperlink[:tooltip]&.to_s
200
+ when Elements::Hyperlink
201
+ hyperlink.tooltip&.to_s
202
+ end
203
+ end
204
+
205
+ # Returns whether the cell contains a comment or note.
206
+ #
207
+ # @return [Boolean]
208
+ # @api public
209
+ #: () -> bool
210
+ def comment?
211
+ case comment
212
+ when nil then false
213
+ when Hash, String then !comment.empty?
214
+ else true
215
+ end
216
+ end
217
+
218
+ # Returns the plain text of the cell comment or note, or nil.
219
+ #
220
+ # @return [String, nil]
221
+ # @api public
222
+ #: () -> String?
223
+ def comment_text
224
+ case comment
225
+ when Hash
226
+ txt = comment[:text]
227
+ txt&.to_s
228
+ when String
229
+ comment.empty? ? nil : comment
230
+ end
231
+ end
232
+
233
+ # Returns the formatted string representation of the cell value according to its format code.
234
+ #
235
+ # @param custom_format_code [String, nil] Optional format code override.
236
+ # @return [String, nil]
237
+ # @api public
238
+ #: (?String? custom_format_code) -> String?
239
+ def formatted_value(custom_format_code = nil)
240
+ return nil if value.nil?
241
+
242
+ NumberFormatter.format(value, custom_format_code || format_code)
243
+ end
244
+
107
245
  # Access cell attributes by Symbol key.
108
246
  #
109
- # @param key [Symbol] Attribute key (:value, :formula, :style_index, :ref, :column_index, :row_index, :type).
247
+ # @param key [Symbol] Attribute key (:value, :formula, :style_index, :ref, :column_index, :row_index, :type, :raw_value, :format_code, :formatted_value).
110
248
  # @return [Object, nil]
111
249
  # @api public
112
250
  #: (Symbol key) -> untyped
@@ -119,6 +257,17 @@ module Xlsxrb
119
257
  when :ref then ref
120
258
  when :column_index then column_index
121
259
  when :row_index then row_index
260
+ when :hyperlink then hyperlink
261
+ when :url then url
262
+ when :tooltip then tooltip
263
+ when :comment then comment
264
+ when :raw_value then raw_value
265
+ when :format_code then format_code
266
+ when :formatted_value then formatted_value
267
+ when :format_type then format_type
268
+ when :date_format then date_format?
269
+ when :time_format then time_format?
270
+ when :date1904 then date1904?
122
271
  when :type
123
272
  case value
124
273
  when String then "s"
@@ -127,6 +276,42 @@ module Xlsxrb
127
276
  end
128
277
  end
129
278
 
279
+ # Returns the classified format category for this cell (:date, :datetime, :time, :number, :text, :general, or nil).
280
+ #
281
+ # @return [Symbol, nil]
282
+ # @api public
283
+ #: () -> Symbol?
284
+ def format_type
285
+ format_code ? NumberFormatter.format_type(format_code) : nil
286
+ end
287
+
288
+ # Returns whether the cell has a date, datetime, or time format.
289
+ #
290
+ # @return [Boolean]
291
+ # @api public
292
+ #: () -> bool
293
+ def date_format?
294
+ format_code ? NumberFormatter.date_format?(format_code) : false
295
+ end
296
+
297
+ # Returns whether the cell has a time-only format.
298
+ #
299
+ # @return [Boolean]
300
+ # @api public
301
+ #: () -> bool
302
+ def time_format?
303
+ format_code ? NumberFormatter.time_format?(format_code) : false
304
+ end
305
+
306
+ # Returns whether the cell's workbook uses the 1904 date system.
307
+ #
308
+ # @return [Boolean]
309
+ # @api public
310
+ #: () -> bool
311
+ def date1904?
312
+ @date1904 ? true : false
313
+ end
314
+
130
315
  # Returns the cell value.
131
316
  #
132
317
  # @return [Object, nil]
@@ -165,14 +350,16 @@ module Xlsxrb
165
350
 
166
351
  # Converts the cell value (numeric serial date or date string) to Date.
167
352
  #
353
+ # @param date1904 [Boolean, nil] Whether to use the 1904 date system (defaults to cell's date1904 setting).
168
354
  # @return [Date, nil]
169
355
  # @api public
170
- #: () -> Date?
171
- def to_date
356
+ #: (?date1904: bool?) -> Date?
357
+ def to_date(date1904: nil)
172
358
  return value if value.is_a?(Date)
173
359
 
360
+ d1904 = date1904.nil? ? date1904? : date1904
174
361
  if value.is_a?(Numeric)
175
- Ooxml::Utils.serial_to_date(value)
362
+ Ooxml::Utils.serial_to_date(value, date1904: d1904)
176
363
  else
177
364
  begin
178
365
  Date.parse(value.to_s)
@@ -184,14 +371,16 @@ module Xlsxrb
184
371
 
185
372
  # Converts the cell value (numeric serial datetime or datetime string) to Time.
186
373
  #
374
+ # @param date1904 [Boolean, nil] Whether to use the 1904 date system (defaults to cell's date1904 setting).
187
375
  # @return [Time, nil]
188
376
  # @api public
189
- #: () -> Time?
190
- def to_time
377
+ #: (?date1904: bool?) -> Time?
378
+ def to_time(date1904: nil)
191
379
  return value if value.is_a?(Time)
192
380
 
381
+ d1904 = date1904.nil? ? date1904? : date1904
193
382
  if value.is_a?(Numeric)
194
- Ooxml::Utils.serial_to_datetime(value)
383
+ Ooxml::Utils.serial_to_datetime(value, date1904: d1904)
195
384
  else
196
385
  begin
197
386
  Time.parse(value.to_s)
@@ -12,21 +12,23 @@ module Xlsxrb
12
12
  #
13
13
  # @api public
14
14
  # rubocop:disable Style/DataInheritance -- Required as class syntax for mutant subject matcher
15
- class Column < Data.define(:index, :width, :hidden, :custom_width, :outline_level, :unmapped_data, :errors)
15
+ class Column < Data.define(:index, :width, :hidden, :custom_width, :outline_level, :style_index, :unmapped_data, :errors)
16
16
  # rubocop:enable Style/DataInheritance
17
17
  # @param index [Integer] 0-based column index.
18
18
  # @param width [Float, Integer, nil] Column width in characters.
19
19
  # @param hidden [Boolean] Whether the column is hidden.
20
20
  # @param custom_width [Boolean] Whether custom width flag is set.
21
21
  # @param outline_level [Integer, nil] Grouping/outline level.
22
+ # @param style_index [Integer, String, nil] Optional style identifier.
22
23
  # @param unmapped_data [Hash] Additional metadata.
23
24
  # @param errors [Array<String>, nil] Validation errors.
24
- #: (index: Integer, ?width: Float | Integer | nil, ?hidden: bool, ?custom_width: bool, ?outline_level: Integer | nil, ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
25
+ #: (index: Integer, ?width: Float | Integer | nil, ?hidden: bool, ?custom_width: bool, ?outline_level: Integer | nil, ?style_index: (Integer | String)?, ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
25
26
  def initialize(index:, width: nil, hidden: false, custom_width: false, outline_level: nil,
26
- unmapped_data: {}, errors: nil)
27
+ style_index: nil, unmapped_data: {}, errors: nil)
28
+ style_idx = style_index || unmapped_data[:style_index] || unmapped_data[:style]
27
29
  computed_errors = errors || self.class.validate(index)
28
30
  super(index: index, width: width, hidden: hidden, custom_width: custom_width,
29
- outline_level: outline_level, unmapped_data: unmapped_data,
31
+ outline_level: outline_level, style_index: style_idx, unmapped_data: unmapped_data,
30
32
  errors: computed_errors.freeze)
31
33
  end
32
34
 
@@ -76,6 +76,38 @@ module Xlsxrb
76
76
  rows.max_by(&:index)
77
77
  end
78
78
 
79
+ # Returns the 1-based index of the first column containing data, or nil.
80
+ #
81
+ # @return [Integer, nil]
82
+ # @api public
83
+ #: () -> Integer?
84
+ def first_column
85
+ min_c = nil
86
+ rows.each do |r|
87
+ r.cells.each do |c|
88
+ min_c = c.column_index if min_c.nil? || c.column_index < min_c
89
+ end
90
+ end
91
+ min_c ? min_c + 1 : nil
92
+ end
93
+ alias first_col first_column
94
+
95
+ # Returns the 1-based index of the last column containing data, or nil.
96
+ #
97
+ # @return [Integer, nil]
98
+ # @api public
99
+ #: () -> Integer?
100
+ def last_column
101
+ max_c = nil
102
+ rows.each do |r|
103
+ r.cells.each do |c|
104
+ max_c = c.column_index if max_c.nil? || c.column_index > max_c
105
+ end
106
+ end
107
+ max_c ? max_c + 1 : nil
108
+ end
109
+ alias last_col last_column
110
+
79
111
  # Returns the raw cell value at the given Excel-style reference (e.g. "A1").
80
112
  #
81
113
  # @example
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Xlsxrb
6
+ module Elements
7
+ # Represents an embedded image anchored to worksheet cells.
8
+ # Coordinates (from_row, from_col, to_row, to_col) are 0-based.
9
+ #
10
+ # @example Access image properties
11
+ # image = sheet.images.first
12
+ # image.filename #=> "image1.png"
13
+ # image.cell_ref #=> "B3"
14
+ # image.content_type #=> "image/png"
15
+ # image.data #=> "\x89PNG..."
16
+ #
17
+ # @api public
18
+ class Image < Data.define(:name, :filename, :content_type, :data, :from_col, :from_row, :to_col, :to_row, :cx, :cy, :unmapped_data)
19
+ MIME_TYPES = {
20
+ "png" => "image/png",
21
+ "jpg" => "image/jpeg",
22
+ "jpeg" => "image/jpeg",
23
+ "gif" => "image/gif",
24
+ "svg" => "image/svg+xml",
25
+ "tif" => "image/tiff",
26
+ "tiff" => "image/tiff",
27
+ "bmp" => "image/bmp",
28
+ "webp" => "image/webp",
29
+ "emf" => "image/x-emf",
30
+ "wmf" => "image/x-wmf"
31
+ }.freeze
32
+
33
+ # @param filename [String] Target filename (e.g. "image1.png").
34
+ # @param name [String, nil] Image or shape name.
35
+ # @param content_type [String, nil] MIME content type (e.g. "image/png").
36
+ # @param data [String, nil] Raw binary image data.
37
+ # @param from_col [Integer] 0-based starting column index.
38
+ # @param from_row [Integer] 0-based starting row index.
39
+ # @param to_col [Integer, nil] 0-based ending column index (nil for 1-cell anchor).
40
+ # @param to_row [Integer, nil] 0-based ending row index (nil for 1-cell anchor).
41
+ # @param cx [Integer, nil] Extent width in EMUs (English Metric Units).
42
+ # @param cy [Integer, nil] Extent height in EMUs (English Metric Units).
43
+ # @param unmapped_data [Hash] Additional metadata from drawing XML.
44
+ #: (filename: String, ?name: String?, ?content_type: String?, ?data: String?, ?from_col: Integer, ?from_row: Integer, ?to_col: Integer?, ?to_row: Integer?, ?cx: Integer?, ?cy: Integer?, ?unmapped_data: Hash[untyped, untyped]) -> void
45
+ # rubocop:disable Naming/MethodParameterName
46
+ def initialize(filename:, name: nil, content_type: nil, data: nil, from_col: 0, from_row: 0,
47
+ to_col: nil, to_row: nil, cx: nil, cy: nil, unmapped_data: EMPTY_HASH)
48
+ # rubocop:enable Naming/MethodParameterName
49
+ ext = File.extname(filename).delete_prefix(".").downcase
50
+ mime = content_type || MIME_TYPES[ext] || "application/octet-stream"
51
+ super(
52
+ name: name,
53
+ filename: filename,
54
+ content_type: mime,
55
+ data: data&.b,
56
+ from_col: from_col,
57
+ from_row: from_row,
58
+ to_col: to_col,
59
+ to_row: to_row,
60
+ cx: cx,
61
+ cy: cy,
62
+ unmapped_data: unmapped_data
63
+ )
64
+ end
65
+
66
+ # Returns the top-left cell coordinate reference string (e.g. "A1").
67
+ #
68
+ # @return [String]
69
+ # @api public
70
+ #: () -> String
71
+ def cell_ref
72
+ "#{Cell.column_letter(from_col)}#{from_row + 1}"
73
+ end
74
+ alias cell cell_ref
75
+ alias ref cell_ref
76
+
77
+ # Returns the bottom-right cell coordinate reference string (e.g. "B5"), or nil for 1-cell anchors.
78
+ #
79
+ # @return [String, nil]
80
+ # @api public
81
+ #: () -> String?
82
+ def to_cell_ref
83
+ return nil if to_col.nil? || to_row.nil?
84
+
85
+ "#{Cell.column_letter(to_col)}#{to_row + 1}"
86
+ end
87
+
88
+ # Returns whether this image uses a one-cell anchor.
89
+ #
90
+ # @return [Boolean]
91
+ # @api public
92
+ #: () -> bool
93
+ def one_cell_anchor?
94
+ to_col.nil? || to_row.nil?
95
+ end
96
+
97
+ # Returns whether this image uses a two-cell anchor.
98
+ #
99
+ # @return [Boolean]
100
+ # @api public
101
+ #: () -> bool
102
+ def two_cell_anchor?
103
+ !one_cell_anchor?
104
+ end
105
+
106
+ # Returns the size of the binary image data in bytes.
107
+ #
108
+ # @return [Integer]
109
+ # @api public
110
+ #: () -> Integer
111
+ def bytesize
112
+ data ? data.bytesize : 0
113
+ end
114
+
115
+ # Writes the image data directly to a file on disk.
116
+ #
117
+ # @param path [String] Target file path.
118
+ # @return [Integer] Number of bytes written.
119
+ # @api public
120
+ #: (String path) -> Integer
121
+ def write_to(path)
122
+ raise Error, "No image binary data available to write" unless data
123
+
124
+ File.binwrite(path, data)
125
+ end
126
+ end
127
+ end
128
+ end