berkeley_library-tind 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (162) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/build.yml +18 -0
  3. data/.gitignore +388 -0
  4. data/.idea/inspectionProfiles/Project_Default.xml +20 -0
  5. data/.idea/misc.xml +4 -0
  6. data/.idea/modules.xml +8 -0
  7. data/.idea/tind.iml +138 -0
  8. data/.idea/vcs.xml +6 -0
  9. data/.rubocop.yml +334 -0
  10. data/.ruby-version +1 -0
  11. data/.simplecov +8 -0
  12. data/.yardopts +1 -0
  13. data/CHANGES.md +58 -0
  14. data/Dockerfile +57 -0
  15. data/Gemfile +3 -0
  16. data/Jenkinsfile +18 -0
  17. data/LICENSE.md +21 -0
  18. data/README.md +73 -0
  19. data/Rakefile +20 -0
  20. data/berkeley_library-tind.gemspec +50 -0
  21. data/bin/tind-export +14 -0
  22. data/docker-compose.yml +15 -0
  23. data/lib/berkeley_library/tind.rb +3 -0
  24. data/lib/berkeley_library/tind/api.rb +1 -0
  25. data/lib/berkeley_library/tind/api/api.rb +132 -0
  26. data/lib/berkeley_library/tind/api/api_exception.rb +131 -0
  27. data/lib/berkeley_library/tind/api/collection.rb +82 -0
  28. data/lib/berkeley_library/tind/api/date_range.rb +67 -0
  29. data/lib/berkeley_library/tind/api/format.rb +32 -0
  30. data/lib/berkeley_library/tind/api/search.rb +100 -0
  31. data/lib/berkeley_library/tind/config.rb +103 -0
  32. data/lib/berkeley_library/tind/export.rb +1 -0
  33. data/lib/berkeley_library/tind/export/column.rb +54 -0
  34. data/lib/berkeley_library/tind/export/column_group.rb +144 -0
  35. data/lib/berkeley_library/tind/export/column_group_list.rb +131 -0
  36. data/lib/berkeley_library/tind/export/column_width_calculator.rb +76 -0
  37. data/lib/berkeley_library/tind/export/config.rb +154 -0
  38. data/lib/berkeley_library/tind/export/csv_exporter.rb +29 -0
  39. data/lib/berkeley_library/tind/export/export.rb +47 -0
  40. data/lib/berkeley_library/tind/export/export_command.rb +168 -0
  41. data/lib/berkeley_library/tind/export/export_exception.rb +8 -0
  42. data/lib/berkeley_library/tind/export/export_format.rb +67 -0
  43. data/lib/berkeley_library/tind/export/exporter.rb +105 -0
  44. data/lib/berkeley_library/tind/export/filter.rb +52 -0
  45. data/lib/berkeley_library/tind/export/no_results_error.rb +7 -0
  46. data/lib/berkeley_library/tind/export/ods_exporter.rb +138 -0
  47. data/lib/berkeley_library/tind/export/row.rb +24 -0
  48. data/lib/berkeley_library/tind/export/row_metrics.rb +18 -0
  49. data/lib/berkeley_library/tind/export/table.rb +175 -0
  50. data/lib/berkeley_library/tind/export/table_metrics.rb +116 -0
  51. data/lib/berkeley_library/tind/marc.rb +1 -0
  52. data/lib/berkeley_library/tind/marc/xml_reader.rb +144 -0
  53. data/lib/berkeley_library/tind/module_info.rb +14 -0
  54. data/lib/berkeley_library/util/arrays.rb +178 -0
  55. data/lib/berkeley_library/util/logging.rb +1 -0
  56. data/lib/berkeley_library/util/ods/spreadsheet.rb +170 -0
  57. data/lib/berkeley_library/util/ods/xml/content_doc.rb +26 -0
  58. data/lib/berkeley_library/util/ods/xml/document_node.rb +57 -0
  59. data/lib/berkeley_library/util/ods/xml/element_node.rb +106 -0
  60. data/lib/berkeley_library/util/ods/xml/loext/table_protection.rb +26 -0
  61. data/lib/berkeley_library/util/ods/xml/manifest/file_entry.rb +42 -0
  62. data/lib/berkeley_library/util/ods/xml/manifest/manifest.rb +73 -0
  63. data/lib/berkeley_library/util/ods/xml/manifest_doc.rb +26 -0
  64. data/lib/berkeley_library/util/ods/xml/namespace.rb +46 -0
  65. data/lib/berkeley_library/util/ods/xml/office/automatic_styles.rb +181 -0
  66. data/lib/berkeley_library/util/ods/xml/office/body.rb +17 -0
  67. data/lib/berkeley_library/util/ods/xml/office/document_content.rb +98 -0
  68. data/lib/berkeley_library/util/ods/xml/office/document_styles.rb +39 -0
  69. data/lib/berkeley_library/util/ods/xml/office/font_face_decls.rb +30 -0
  70. data/lib/berkeley_library/util/ods/xml/office/scripts.rb +17 -0
  71. data/lib/berkeley_library/util/ods/xml/office/spreadsheet.rb +37 -0
  72. data/lib/berkeley_library/util/ods/xml/office/styles.rb +39 -0
  73. data/lib/berkeley_library/util/ods/xml/style/cell_style.rb +58 -0
  74. data/lib/berkeley_library/util/ods/xml/style/column_style.rb +36 -0
  75. data/lib/berkeley_library/util/ods/xml/style/default_style.rb +31 -0
  76. data/lib/berkeley_library/util/ods/xml/style/family.rb +85 -0
  77. data/lib/berkeley_library/util/ods/xml/style/font_face.rb +46 -0
  78. data/lib/berkeley_library/util/ods/xml/style/paragraph_properties.rb +30 -0
  79. data/lib/berkeley_library/util/ods/xml/style/row_style.rb +37 -0
  80. data/lib/berkeley_library/util/ods/xml/style/style.rb +44 -0
  81. data/lib/berkeley_library/util/ods/xml/style/table_cell_properties.rb +40 -0
  82. data/lib/berkeley_library/util/ods/xml/style/table_column_properties.rb +30 -0
  83. data/lib/berkeley_library/util/ods/xml/style/table_properties.rb +25 -0
  84. data/lib/berkeley_library/util/ods/xml/style/table_row_properties.rb +28 -0
  85. data/lib/berkeley_library/util/ods/xml/style/table_style.rb +27 -0
  86. data/lib/berkeley_library/util/ods/xml/style/text_properties.rb +52 -0
  87. data/lib/berkeley_library/util/ods/xml/styles_doc.rb +26 -0
  88. data/lib/berkeley_library/util/ods/xml/table/named_expressions.rb +17 -0
  89. data/lib/berkeley_library/util/ods/xml/table/repeatable.rb +38 -0
  90. data/lib/berkeley_library/util/ods/xml/table/table.rb +193 -0
  91. data/lib/berkeley_library/util/ods/xml/table/table_cell.rb +46 -0
  92. data/lib/berkeley_library/util/ods/xml/table/table_column.rb +43 -0
  93. data/lib/berkeley_library/util/ods/xml/table/table_row.rb +136 -0
  94. data/lib/berkeley_library/util/ods/xml/text/p.rb +118 -0
  95. data/lib/berkeley_library/util/paths.rb +111 -0
  96. data/lib/berkeley_library/util/stringios.rb +30 -0
  97. data/lib/berkeley_library/util/strings.rb +42 -0
  98. data/lib/berkeley_library/util/sys_exits.rb +15 -0
  99. data/lib/berkeley_library/util/times.rb +22 -0
  100. data/lib/berkeley_library/util/uris.rb +44 -0
  101. data/lib/berkeley_library/util/uris/appender.rb +162 -0
  102. data/lib/berkeley_library/util/uris/requester.rb +62 -0
  103. data/lib/berkeley_library/util/uris/validator.rb +32 -0
  104. data/rakelib/bundle.rake +8 -0
  105. data/rakelib/coverage.rake +11 -0
  106. data/rakelib/gem.rake +54 -0
  107. data/rakelib/rubocop.rake +18 -0
  108. data/rakelib/spec.rake +2 -0
  109. data/spec/.rubocop.yml +40 -0
  110. data/spec/berkeley_library/tind/api/api_exception_spec.rb +91 -0
  111. data/spec/berkeley_library/tind/api/api_spec.rb +143 -0
  112. data/spec/berkeley_library/tind/api/collection_spec.rb +74 -0
  113. data/spec/berkeley_library/tind/api/date_range_spec.rb +110 -0
  114. data/spec/berkeley_library/tind/api/format_spec.rb +54 -0
  115. data/spec/berkeley_library/tind/api/search_spec.rb +364 -0
  116. data/spec/berkeley_library/tind/config_spec.rb +86 -0
  117. data/spec/berkeley_library/tind/export/column_group_spec.rb +29 -0
  118. data/spec/berkeley_library/tind/export/column_spec.rb +43 -0
  119. data/spec/berkeley_library/tind/export/config_spec.rb +206 -0
  120. data/spec/berkeley_library/tind/export/export_command_spec.rb +169 -0
  121. data/spec/berkeley_library/tind/export/export_format_spec.rb +59 -0
  122. data/spec/berkeley_library/tind/export/export_matcher.rb +112 -0
  123. data/spec/berkeley_library/tind/export/export_spec.rb +150 -0
  124. data/spec/berkeley_library/tind/export/exporter_spec.rb +125 -0
  125. data/spec/berkeley_library/tind/export/row_spec.rb +118 -0
  126. data/spec/berkeley_library/tind/export/table_spec.rb +322 -0
  127. data/spec/berkeley_library/tind/marc/xml_reader_spec.rb +93 -0
  128. data/spec/berkeley_library/util/arrays_spec.rb +340 -0
  129. data/spec/berkeley_library/util/ods/spreadsheet_spec.rb +124 -0
  130. data/spec/berkeley_library/util/ods/xml/content_doc_spec.rb +121 -0
  131. data/spec/berkeley_library/util/ods/xml/manifest/file_entry_spec.rb +27 -0
  132. data/spec/berkeley_library/util/ods/xml/manifest/manifest_spec.rb +33 -0
  133. data/spec/berkeley_library/util/ods/xml/office/document_content_spec.rb +60 -0
  134. data/spec/berkeley_library/util/ods/xml/style/automatic_styles_spec.rb +37 -0
  135. data/spec/berkeley_library/util/ods/xml/style/family_spec.rb +57 -0
  136. data/spec/berkeley_library/util/ods/xml/table/table_row_spec.rb +179 -0
  137. data/spec/berkeley_library/util/ods/xml/table/table_spec.rb +218 -0
  138. data/spec/berkeley_library/util/paths_spec.rb +90 -0
  139. data/spec/berkeley_library/util/stringios_spec.rb +34 -0
  140. data/spec/berkeley_library/util/strings_spec.rb +27 -0
  141. data/spec/berkeley_library/util/times_spec.rb +39 -0
  142. data/spec/berkeley_library/util/uris_spec.rb +118 -0
  143. data/spec/data/collection-names.txt +438 -0
  144. data/spec/data/collections.json +4827 -0
  145. data/spec/data/disjoint-records.xml +187 -0
  146. data/spec/data/record-184453.xml +58 -0
  147. data/spec/data/record-184458.xml +63 -0
  148. data/spec/data/record-187888.xml +78 -0
  149. data/spec/data/records-api-search-cjk-p1.xml +6381 -0
  150. data/spec/data/records-api-search-cjk-p2.xml +5 -0
  151. data/spec/data/records-api-search-p1.xml +4506 -0
  152. data/spec/data/records-api-search-p2.xml +4509 -0
  153. data/spec/data/records-api-search-p3.xml +4506 -0
  154. data/spec/data/records-api-search-p4.xml +4509 -0
  155. data/spec/data/records-api-search-p5.xml +4506 -0
  156. data/spec/data/records-api-search-p6.xml +2436 -0
  157. data/spec/data/records-api-search-p7.xml +5 -0
  158. data/spec/data/records-api-search.xml +234 -0
  159. data/spec/data/records-manual-search.xml +547 -0
  160. data/spec/spec_helper.rb +30 -0
  161. data/test/profile/table_from_records_profile.rb +46 -0
  162. metadata +585 -0
@@ -0,0 +1,7 @@
1
+ module BerkeleyLibrary
2
+ module TIND
3
+ module Export
4
+ class NoResultsError < StandardError; end
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,138 @@
1
+ require 'berkeley_library/util/arrays'
2
+ require 'berkeley_library/tind/export/exporter'
3
+ require 'berkeley_library/tind/export/table_metrics'
4
+ require 'berkeley_library/util/ods/spreadsheet'
5
+
6
+ module BerkeleyLibrary
7
+ module TIND
8
+ module Export
9
+ # Exporter for OpenOffice/LibreOffice format
10
+ class ODSExporter < Exporter
11
+
12
+ LOCKED_CELL_COLOR = '#c0362c'.freeze
13
+
14
+ # Round column widths up to nearest quarter inch
15
+ EIGHTH = '1/8'.to_r
16
+
17
+ # Max column width before wrapping
18
+ MAX_COLUMN_WIDTH_INCHES = 3.0
19
+
20
+ # Exports {ExportBase#collection} as an OpenOffice/LibreOffice spreadsheet
21
+ # @overload export
22
+ # Exports to a new string.
23
+ # @return [String] a binary string containing the spreadsheet data.
24
+ # @overload export(out)
25
+ # Exports to the specified output stream.
26
+ # @param out [IO] the output stream
27
+ # @return[void]
28
+ # @overload export(path)
29
+ # Exports to the specified file. If `path` denotes a directory, the
30
+ # spreadsheet will be written as exploded, pretty-printed XML.
31
+ # @param path [String, Pathname] the path to the output file or directory
32
+ # @return[void]
33
+ # @see BerkeleyLibrary::Util::ODS::Spreadsheet#write_exploded_to
34
+ def export(out = nil)
35
+ populate_spreadsheet!
36
+ spreadsheet.write_to(out)
37
+ end
38
+
39
+ # ------------------------------------------------------------
40
+ # Private methods
41
+
42
+ private
43
+
44
+ # ------------------------------
45
+ # Private accessors
46
+
47
+ def spreadsheet
48
+ @spreadsheet ||= BerkeleyLibrary::Util::ODS::Spreadsheet.new
49
+ end
50
+
51
+ def table
52
+ @table ||= spreadsheet.add_table(collection, protected: false)
53
+ end
54
+
55
+ def table_metrics
56
+ @table_metrics ||= TableMetrics.new(export_table)
57
+ end
58
+
59
+ # @return [BerkeleyLibrary::Util::ODS::XML::Office::AutomaticStyles] the styles
60
+ def styles
61
+ spreadsheet.auto_styles
62
+ end
63
+
64
+ def color_for(col_index)
65
+ can_edit?(col_index) ? nil : LOCKED_CELL_COLOR
66
+ end
67
+
68
+ def header_cell_style_for(col_index)
69
+ @header_cell_styles ||= []
70
+ @header_cell_styles[col_index] ||= begin
71
+ find_or_create_cell_style(
72
+ color: color_for(col_index),
73
+ font_weight: 'bold'
74
+ )
75
+ end
76
+ end
77
+
78
+ def find_or_create_cell_style(color:, font_weight: nil, wrap: false)
79
+ styles.find_or_create_cell_style(color: color, font_weight: font_weight, wrap: wrap)
80
+ end
81
+
82
+ def column_style_for(col_index)
83
+ column_width = table_metrics.formatted_width(col_index)
84
+ @column_styles_by_width ||= {}
85
+ @column_styles_by_width[column_width] ||= styles.find_or_create_column_style(column_width)
86
+ end
87
+
88
+ # ------------------------------
89
+ # Private utility methods
90
+
91
+ def populate_spreadsheet!
92
+ logger.info("Populating spreadsheet for #{collection}")
93
+ populate_columns!
94
+ populate_rows!
95
+ end
96
+
97
+ def can_edit?(col_index)
98
+ raise ArgumentError, "Invalid column index: #{col_index.inspect}" unless (column = export_table.columns[col_index])
99
+
100
+ column.can_edit?
101
+ end
102
+
103
+ # @param table [BerkeleyLibrary::Util::ODS::XML::Table::Table] the table
104
+ def populate_columns!
105
+ export_table.columns.each_with_index do |export_column, col_index|
106
+ table.add_column_with_styles(
107
+ export_column.header,
108
+ column_style: column_style_for(col_index),
109
+ header_cell_style: header_cell_style_for(col_index)
110
+ )
111
+ end
112
+ end
113
+
114
+ # @param table [BerkeleyLibrary::Util::ODS::XML::Table::Table] the table
115
+ def populate_rows!
116
+ export_table.row_count.times(&method(:populate_row))
117
+ end
118
+
119
+ def populate_row(row_index)
120
+ export_row = export_table.rows[row_index]
121
+ row_metrics = table_metrics.row_metrics_for(row_index)
122
+ row_height = row_metrics.formatted_row_height
123
+ row = table.add_row(row_height)
124
+ populate_values(export_row, row_metrics, row)
125
+ end
126
+
127
+ def populate_values(export_row, row_metrics, row)
128
+ export_row.each_value.with_index do |v, col_index|
129
+ wrap = row_metrics.wrap?(col_index)
130
+ cell_style = find_or_create_cell_style(color: color_for(col_index), wrap: wrap)
131
+ row.set_value_at(col_index, v, cell_style)
132
+ end
133
+ end
134
+
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,24 @@
1
+ module BerkeleyLibrary
2
+ module TIND
3
+ module Export
4
+ class Row
5
+
6
+ attr_reader :columns
7
+ attr_reader :row_index
8
+
9
+ def initialize(columns, row_index)
10
+ @columns = columns
11
+ @row_index = row_index
12
+ end
13
+
14
+ def values
15
+ columns.map { |c| c.value_at(row_index) }
16
+ end
17
+
18
+ def each_value(&block)
19
+ columns.map { |c| c.value_at(row_index) }.each(&block)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ module BerkeleyLibrary
2
+ module TIND
3
+ module Export
4
+ class RowMetrics
5
+ attr_reader :formatted_row_height
6
+
7
+ def initialize(formatted_row_height, wrap_columns)
8
+ @formatted_row_height = formatted_row_height
9
+ @wrap_columns = wrap_columns
10
+ end
11
+
12
+ def wrap?(col_index)
13
+ @wrap_columns.include?(col_index)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,175 @@
1
+ require 'csv'
2
+ require 'stringio'
3
+
4
+ require 'marc_extensions'
5
+ require 'berkeley_library/util/logging'
6
+ require 'berkeley_library/tind/export/column_group_list'
7
+
8
+ module BerkeleyLibrary
9
+ module TIND
10
+ module Export
11
+ class Table
12
+ include BerkeleyLibrary::Logging
13
+
14
+ # ------------------------------------------------------------
15
+ # Accessors
16
+
17
+ attr_reader :column_groups
18
+
19
+ # ------------------------------------------------------------
20
+ # Initializer
21
+
22
+ # Initializes a new Table
23
+ #
24
+ # @param exportable_only [Boolean] whether to filter out non-exportable fields
25
+ # @see Tags
26
+ def initialize(exportable_only: false)
27
+ @column_groups = ColumnGroupList.new(exportable_only: exportable_only)
28
+ end
29
+
30
+ # ------------------------------------------------------------
31
+ # Factory method
32
+
33
+ class << self
34
+ # Returns a new Table for the provided MARC records.
35
+ #
36
+ # @param records [Enumerable<MARC::Record>] the records
37
+ # @param freeze [Boolean] whether to freeze the table
38
+ # @param exportable_only [Boolean] whether to include only exportable fields
39
+ # @return [Table] the table
40
+ def from_records(records, freeze: false, exportable_only: false)
41
+ Table.new(exportable_only: exportable_only).tap do |table|
42
+ records.each { |r| table << r }
43
+ table.freeze if freeze
44
+ end
45
+ end
46
+ end
47
+
48
+ # ------------------------------------------------------------
49
+ # Cell accessors
50
+
51
+ def value_at(row, col)
52
+ return unless (column = columns[col])
53
+
54
+ column.value_at(row)
55
+ end
56
+
57
+ # ------------------------------------------------------------
58
+ # Column accessors
59
+
60
+ # The column headers
61
+ #
62
+ # @return [Array<String>] the column headers
63
+ def headers
64
+ columns.map(&:header)
65
+ end
66
+
67
+ # The columns
68
+ #
69
+ # @return [Array<Column>] the columns.
70
+ def columns
71
+ # NOTE: this isn't ||= because we only cache on #freeze
72
+ @columns || column_groups.map(&:columns).flatten
73
+ end
74
+
75
+ def column_count
76
+ columns.size
77
+ end
78
+
79
+ # ------------------------------------------------------------
80
+ # Row / MARC::Record accessors
81
+
82
+ # The rows
83
+ #
84
+ # @return [Array<Row>] the rows
85
+ def rows
86
+ # NOTE: this isn't ||= because we only cache on #freeze
87
+ # noinspection RubyYardReturnMatch
88
+ @rows || each_row.to_a
89
+ end
90
+
91
+ # @yieldparam row [Row] each row
92
+ def each_row
93
+ return to_enum(:each_row) unless block_given?
94
+
95
+ (0...row_count).each { |row| yield Row.new(columns, row) }
96
+ end
97
+
98
+ # The number of rows (records)
99
+ #
100
+ # @return [Integer] the number of rows
101
+ def row_count
102
+ marc_records.size
103
+ end
104
+
105
+ # The MARC records
106
+ #
107
+ # @return [Array<MARC::Record>] the records
108
+ def marc_records
109
+ @marc_records ||= []
110
+ end
111
+
112
+ def empty?
113
+ marc_records.empty?
114
+ end
115
+
116
+ # ------------------------------------------------------------
117
+ # Modifiers
118
+
119
+ # Adds the specified record
120
+ #
121
+ # @param marc_record [MARC::Record] the record to add
122
+ def <<(marc_record)
123
+ raise FrozenError, "can't modify frozen MARCTable" if frozen?
124
+
125
+ logger.warn('MARC record is not frozen') unless marc_record.frozen?
126
+ column_groups.add_data_fields(marc_record, marc_records.size)
127
+ marc_records << marc_record
128
+ log_record_added(marc_record)
129
+
130
+ self
131
+ end
132
+
133
+ # ------------------------------------------------------------
134
+ # Object overrides
135
+
136
+ def frozen?
137
+ [marc_records, column_groups].all?(&:frozen?) &&
138
+ [@rows, @columns].all? { |d| !d.nil? && d.frozen? }
139
+ end
140
+
141
+ def freeze
142
+ [marc_records, column_groups].each(&:freeze)
143
+ @columns ||= columns.freeze
144
+ @rows ||= rows.freeze
145
+ self
146
+ end
147
+
148
+ # ------------------------------------------------------------
149
+ # Misc. instance methods
150
+
151
+ # TODO: move to BerkeleyLibrary::TIND::Export::CSVExporter
152
+ def to_csv(out = nil)
153
+ return write_csv(out) if out
154
+
155
+ StringIO.new.tap { |io| write_csv(io) }.string
156
+ end
157
+
158
+ # ------------------------------------------------------------
159
+ # Private methods
160
+
161
+ private
162
+
163
+ def log_record_added(marc_record)
164
+ return logger.debug("Added record no. #{marc_record.record_id}: #{row_count} records total") if marc_record
165
+ end
166
+
167
+ def write_csv(out)
168
+ csv = out.respond_to?(:write) ? CSV.new(out) : CSV.open(out, 'wb')
169
+ csv << headers
170
+ each_row { |row| csv << row.values }
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,116 @@
1
+ require 'berkeley_library/util/logging'
2
+ require 'berkeley_library/tind/export/row_metrics'
3
+ require 'berkeley_library/tind/export/config'
4
+
5
+ module BerkeleyLibrary
6
+ module TIND
7
+ module Export
8
+ class TableMetrics
9
+ include BerkeleyLibrary::Logging
10
+ include Config
11
+
12
+ # @return [Table] the table
13
+ attr_reader :table
14
+
15
+ # Initializes a new set of metrics for the specified table.
16
+ #
17
+ # @param table [Table] the table
18
+ def initialize(table)
19
+ @table = table
20
+ end
21
+
22
+ def formatted_width(col_index)
23
+ inches_numeric = numeric_column_width(col_index)
24
+ format_inches(inches_numeric)
25
+ end
26
+
27
+ def row_metrics_for(row_index)
28
+ @metrics_by_row ||= []
29
+ @metrics_by_row[row_index] ||= calc_row_metrics(row_index)
30
+ end
31
+
32
+ # ------------------------------------------------------------
33
+ # Private methods
34
+
35
+ private
36
+
37
+ # ------------------------------
38
+ # Private accessors
39
+
40
+ def inch_format
41
+ @inch_format ||= "%0.#{format_digits}fin"
42
+ end
43
+
44
+ def numeric_column_width(col_index)
45
+ @numeric_column_widths ||= []
46
+ @numeric_column_widths[col_index] ||= calc_max_width(col_index)
47
+ end
48
+
49
+ def cell_widths_for_column(col_index)
50
+ @cell_widths_by_column ||= []
51
+ @cell_widths_by_column[col_index] ||= calc_cell_widths(col_index)
52
+ end
53
+
54
+ # ------------------------------
55
+ # Private utility methods
56
+
57
+ def calc_max_width(col_index)
58
+ header = table.columns[col_index].header
59
+ w_header = ColumnWidthCalculator.width_inches(header)
60
+ w_cell_max = cell_widths_for_column(col_index).max
61
+ w_max_actual = [w_header, w_cell_max].max
62
+ w_max_rounded = (w_max_actual / w_incr_in).ceil * w_incr_in
63
+ [w_max_rounded, max_col_width_in].min
64
+ end
65
+
66
+ def calc_cell_widths(col_index)
67
+ column = table.columns[col_index]
68
+ column.each_value.map { |v| ColumnWidthCalculator.width_inches(v) }
69
+ end
70
+
71
+ def format_inches(inches_numeric)
72
+ format(inch_format, inches_numeric)
73
+ end
74
+
75
+ def calc_row_metrics(row_index)
76
+ columns_to_wrap = Set.new
77
+ max_wrapped_lines = 1
78
+
79
+ table.column_count.times do |col_index|
80
+ wrapped_lines = wrapped_lines_for_cell(row_index, col_index)
81
+ max_wrapped_lines = [wrapped_lines, max_wrapped_lines].max
82
+ columns_to_wrap << col_index if wrapped_lines > 1
83
+ end
84
+
85
+ formatted_height = format_row_height(max_wrapped_lines)
86
+ RowMetrics.new(formatted_height, columns_to_wrap)
87
+ end
88
+
89
+ # TODO: something smarter, maybe guess at break locations?
90
+ def wrapped_lines_for_cell(row_index, col_index)
91
+ w_col = numeric_column_width(col_index)
92
+ w_cell = numeric_cell_width(row_index, col_index)
93
+ (w_cell / w_col).ceil
94
+ end
95
+
96
+ def numeric_cell_width(row_index, col_index)
97
+ cell_widths_by_row = cell_widths_for_column(col_index)
98
+ cell_widths_by_row[row_index] || 0
99
+ end
100
+
101
+ def numeric_line_height_inches
102
+ @numeric_line_height_inches ||= begin
103
+ h_exact_pts = font_size_pt * line_height_em
104
+ h_rounded_pts = (h_exact_pts / h_incr_pt).ceil * h_incr_pt
105
+ h_rounded_pts / 72.0
106
+ end
107
+ end
108
+
109
+ def format_row_height(lines)
110
+ @formatted_row_heights ||= []
111
+ @formatted_row_heights[lines] ||= format_inches(lines * numeric_line_height_inches)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end