Skip to content

Commit 89afc14

Browse files
committed
improved value constraints feature (now handling the excel 255 char limit)
1 parent 3a5895a commit 89afc14

3 files changed

Lines changed: 101 additions & 18 deletions

File tree

src/main/kotlin/io/retable/RetableExcelSupport.kt

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -194,24 +194,85 @@ class RetableExcelSupport<T : RetableColumns>(
194194
private fun String.isUrl(): Boolean =
195195
take(7) == "http://" || take(8) == "https://"
196196

197+
/**
198+
* Sets up Excel data validation (dropdown lists) for all [StringRetableColumn]s that define allowed values.
199+
*
200+
* Tries the standard inline approach first ([setupStdValueConstraints]). If it fails due to the
201+
* Excel 255-character formula limit, falls back to a hidden sheet-based approach
202+
* ([setupHiddenSheetBasedValueConstraints]).
203+
*/
197204
private fun XSSFSheet.setupValueConstraints(columns: T) {
198-
columns.list()
205+
val constrainedColumns = columns.list()
199206
.filterIsInstance<StringRetableColumn>()
200207
.filter { it.allowedValues.orEmpty().isNotEmpty() }
201-
.forEach { column ->
202-
val allowedValues = column.allowedValues.orEmpty()
203-
val constraint = dataValidationHelper.createExplicitListConstraint(allowedValues.toTypedArray())
204-
val cellRange = CellRangeAddressList(
205-
if (options.firstRecordAsHeader) 1 else 0,
206-
workbook.spreadsheetVersion.lastRowIndex,
207-
column.index - 1,
208-
column.index - 1
209-
)
210-
val validation = dataValidationHelper.createValidation(constraint, cellRange)
211-
validation.showPromptBox = true
212-
validation.suppressDropDownArrow = false
213-
addValidationData(validation)
208+
209+
if (constrainedColumns.isNotEmpty()) {
210+
try {
211+
setupStdValueConstraints(constrainedColumns)
212+
} catch (_: IllegalArgumentException) {
213+
// excel formula 255-character limit potentially exceeded,
214+
// fallback to hidden sheet-based value constraints
215+
setupHiddenSheetBasedValueConstraints(constrainedColumns)
214216
}
217+
}
218+
}
219+
220+
private fun XSSFSheet.setupStdValueConstraints(constrainedColumns: List<StringRetableColumn>) {
221+
constrainedColumns.forEach { column ->
222+
val columnIndex = column.index - 1
223+
224+
val allowedValues = column.allowedValues.orEmpty()
225+
val constraint = dataValidationHelper.createExplicitListConstraint(allowedValues.toTypedArray())
226+
val cellRange = CellRangeAddressList(
227+
if (options.firstRecordAsHeader) 1 else 0,
228+
workbook.spreadsheetVersion.lastRowIndex,
229+
columnIndex,
230+
columnIndex
231+
)
232+
val validation = dataValidationHelper.createValidation(constraint, cellRange)
233+
validation.showPromptBox = true
234+
validation.suppressDropDownArrow = false
235+
addValidationData(validation)
236+
}
237+
}
238+
239+
private fun XSSFSheet.setupHiddenSheetBasedValueConstraints(constrainedColumns: List<StringRetableColumn>) {
240+
val constraintSheetName = "_constraints_${this.sheetName}"
241+
val constraintSheet = workbook.createSheet(constraintSheetName)
242+
val constraintSheetIndex = workbook.getSheetIndex(constraintSheet)
243+
workbook.setSheetHidden(constraintSheetIndex, true)
244+
245+
constrainedColumns.forEach { column ->
246+
val columnIndex = column.index - 1
247+
248+
val allowedValues = column.allowedValues.orEmpty()
249+
allowedValues.forEachIndexed { rowIndex, value ->
250+
val row = constraintSheet.getRow(rowIndex)
251+
?: constraintSheet.createRow(rowIndex)
252+
row.createCell(columnIndex).setCellValue(value)
253+
}
254+
255+
val rangeName = "${constraintSheetName}_${columnIndex}"
256+
val lastRow = allowedValues.size
257+
val colLetter = ('A' + columnIndex)
258+
val formulaRef = "'$constraintSheetName'!\$${colLetter}\$1:\$${colLetter}\$$lastRow"
259+
260+
val name = workbook.createName()
261+
name.nameName = rangeName
262+
name.refersToFormula = formulaRef
263+
264+
val constraint = dataValidationHelper.createFormulaListConstraint(rangeName)
265+
val cellRange = CellRangeAddressList(
266+
if (options.firstRecordAsHeader) 1 else 0,
267+
workbook.spreadsheetVersion.lastRowIndex,
268+
columnIndex,
269+
columnIndex
270+
)
271+
val validation = dataValidationHelper.createValidation(constraint, cellRange)
272+
validation.showPromptBox = true
273+
validation.suppressDropDownArrow = false
274+
addValidationData(validation)
275+
}
215276
}
216277
}
217278

src/test/kotlin/io/retable/RetableExcelTest.kt

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,32 @@ class RetableExcelTest {
210210
}
211211

212212
@Test
213-
fun `should export with defined value constraints`() {
214-
val resultFilePath = pathTo("export_with_value_constraints.xlsx")
213+
fun `should export with defined std value constraints`() {
214+
val resultFilePath = pathTo("export_with_std_value_constraints.xlsx")
215215
val columns = object : RetableColumns() {
216216
val COMPANY = string("company_name", index = 1, allowedValues = listOf("Google", "4sh"))
217-
val ENABLED = string("enabled", index = 2)
217+
val ENABLED = string("enabled", index = 2, allowedValues = listOf("true", "false"))
218+
}
219+
Retable(columns)
220+
.data(
221+
listOf(
222+
listOf("Google", "true"),
223+
listOf("4sh", "true")
224+
)
225+
)
226+
.write(Retable.excel(columns) to File(resultFilePath).outputStream())
227+
228+
expectThat(File(resultFilePath)) {
229+
get { exists() }.isTrue()
230+
}
231+
}
232+
233+
@Test
234+
fun `should export with defined sheet based value constraints`() {
235+
val resultFilePath = pathTo("export_with_sheet_value_constraints.xlsx")
236+
val columns = object : RetableColumns() {
237+
val COMPANY = string("company_name", index = 1, allowedValues = (1..500).map { "Company $it" })
238+
val ENABLED = string("enabled", index = 2, allowedValues = listOf("true", "false"))
218239
}
219240
Retable(columns)
220241
.data(

src/test/resources/examples/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export_data_cols.xlsx
66
export_data_cols_date.csv
77
export_data_cols_date.xlsx
88
export_data_indexed_cols.csv
9-
export_with_value_constraints.xlsx
9+
export_with_std_value_constraints.xlsx
10+
export_with_sheet_value_constraints.xlsx
1011
export_with_hyperlink.xlsx
1112
export_with_raw_hyperlink.xlsx
1213
simple_export_test_result.csv

0 commit comments

Comments
 (0)