This is an automated email from the ASF dual-hosted git repository.
mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new eb1582adde Add format mask support to boolean fields and update
documentation, f… (#8557)
eb1582adde is described below
commit eb1582adde9b0c99dca7a46ebcabe76ad4ce48de
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Sep 24 12:50:48 2026 +0200
Add format mask support to boolean fields and update documentation, f…
(#8557)
* Add format mask support to boolean fields and update documentation, fixes
#5958
* fix remarks
---
core/src/main/java/org/apache/hop/core/Const.java | 11 +
.../apache/hop/core/row/value/ValueMetaBase.java | 83 +++-
.../hop/core/row/value/ValueMetaBaseTest.java | 149 ++++++++
.../ROOT/pages/pipeline/formatting-values.adoc | 57 +++
.../pages/pipeline/transforms/textfileoutput.adoc | 5 +-
.../snippets/field-metadata/field-columns.adoc | 3 +-
.../pages/snippets/hop-concepts/data-types.adoc | 33 ++
.../transforms/0118-boolean-format-read-back.hpl | 261 +++++++++++++
.../0118-boolean-format-text-file-output.hpl | 417 +++++++++++++++++++++
.../transforms/datasets/golden-boolean-formats.csv | 5 +
.../transforms/files/boolean-formats.csv | 5 +
.../transforms/main-0118-boolean-format.hwf | 115 ++++++
.../metadata/dataset/golden-boolean-formats.json | 63 ++++
.../0118-boolean-format-read-back UNIT.json | 52 +++
.../concatfields/ConcatFieldsDialog.java | 4 +-
.../hop/ui/core/dialog/EnterValueDialog.java | 9 +
.../org/apache/hop/ui/core/widget/TableView.java | 1 +
17 files changed, 1267 insertions(+), 6 deletions(-)
diff --git a/core/src/main/java/org/apache/hop/core/Const.java
b/core/src/main/java/org/apache/hop/core/Const.java
index 33fe4cbf5e..e99cc8509c 100644
--- a/core/src/main/java/org/apache/hop/core/Const.java
+++ b/core/src/main/java/org/apache/hop/core/Const.java
@@ -356,6 +356,9 @@ public class Const {
/** An array of number conversion formats */
private static String[] numberFormats;
+ /** The Boolean conversion formats: the text for true and for false,
separated by a slash */
+ private static final String[] BOOLEAN_FORMATS = {"true/false", "Y/N", "1/0",
"yes/no"};
+
/**
* Generalized date/time format: Wherever dates are used, date and time
values are organized from
* the most to the least significant. see also method
StringUtil.getFormattedDateTime()
@@ -2994,6 +2997,14 @@ public class Const {
return (String[]) ArrayUtils.addAll(Const.getDateFormats(),
Const.getNumberFormats());
}
+ /**
+ * @return The Boolean conversion formats, each holding the text for true
and for false separated
+ * by a slash (for example {@code Y/N})
+ */
+ public static String[] getBooleanFormats() {
+ return BOOLEAN_FORMATS.clone();
+ }
+
/**
* Return the input string trimmed as specified.
*
diff --git
a/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java
b/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java
index 652a9d0583..5fb5417fa1 100644
--- a/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java
+++ b/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java
@@ -1698,10 +1698,44 @@ public class ValueMetaBase implements IValueMeta {
// BOOLEAN + STRING
+ /**
+ * A Boolean format mask holds the text for true and the text for false,
separated by a single
+ * slash, for example {@code true/false}, {@code Y/N} or {@code 1/0}. Spaces
around either text
+ * are not part of it: {@code Ja / Nein} is {@code Ja} and {@code Nein}.
+ *
+ * <p>A mask with no slash, or with more than one (a date mask like {@code
yyyy/MM/dd}), is not a
+ * Boolean mask. Neither is one with an empty side, or with the same text on
both sides ignoring
+ * case: reading ignores case, so {@code Yes/yes} could not tell the two
apart.
+ *
+ * @param mask the conversion mask
+ * @return the text for true and the text for false, or null when the mask
is not a Boolean mask
+ */
+ static String[] getBooleanMaskTexts(String mask) {
+ if (mask == null) {
+ return null;
+ }
+ int slash = mask.indexOf('/');
+ if (slash < 0 || mask.indexOf('/', slash + 1) >= 0) {
+ return null;
+ }
+ String trueText = mask.substring(0, slash).trim();
+ String falseText = mask.substring(slash + 1).trim();
+ if (trueText.isEmpty() || falseText.isEmpty() ||
trueText.equalsIgnoreCase(falseText)) {
+ return null;
+ }
+ return new String[] {trueText, falseText};
+ }
+
protected String convertBooleanToString(Boolean bool) {
if (bool == null) {
return null;
}
+ String[] texts = getBooleanMaskTexts(conversionMask);
+ if (texts != null) {
+ return bool ? texts[0] : texts[1];
+ }
+ // Without a mask the length decides, a legacy rule kept for compatibility
+ //
if (length >= 3) {
return bool ? "true" : CONST_FALSE;
} else {
@@ -1720,6 +1754,38 @@ public class ValueMetaBase implements IValueMeta {
|| "1".equals(string);
}
+ /**
+ * Converts a String to a Boolean, first matching the true and false text of
a Boolean conversion
+ * mask (ignoring case and surrounding spaces). Text that matches neither
falls back to {@link
+ * #convertStringToBoolean(String)}.
+ *
+ * <p>The mask is this value's own or, when that is not a Boolean mask, the
one of its conversion
+ * metadata: converting typed text to a Boolean value goes through a String
value whose conversion
+ * metadata is the Boolean, the way a date pattern is found for dates.
+ *
+ * @param string the string to convert
+ * @return the Boolean, or null for an empty string
+ */
+ protected Boolean convertMaskedStringToBoolean(String string) {
+ if (Utils.isEmpty(string)) {
+ return null;
+ }
+ String[] texts = getBooleanMaskTexts(conversionMask);
+ if (texts == null && conversionMetadata != null) {
+ texts = getBooleanMaskTexts(conversionMetadata.getConversionMask());
+ }
+ if (texts != null) {
+ String text = string.trim();
+ if (text.equalsIgnoreCase(texts[0])) {
+ return true;
+ }
+ if (text.equalsIgnoreCase(texts[1])) {
+ return false;
+ }
+ }
+ return convertStringToBoolean(string);
+ }
+
// BOOLEAN + NUMBER
protected Double convertBooleanToNumber(Boolean bool) {
@@ -2703,12 +2769,12 @@ public class ValueMetaBase implements IValueMeta {
};
case TYPE_STRING:
return switch (storageType) {
- case STORAGE_TYPE_NORMAL -> convertStringToBoolean(trim((String)
object));
+ case STORAGE_TYPE_NORMAL ->
convertMaskedStringToBoolean(trim((String) object));
case STORAGE_TYPE_BINARY_STRING ->
- convertStringToBoolean(
+ convertMaskedStringToBoolean(
trim((String) convertBinaryStringToNativeType((byte[])
object)));
case STORAGE_TYPE_INDEXED ->
- convertStringToBoolean(trim((String) index[(Integer) object]));
+ convertMaskedStringToBoolean(trim((String) index[(Integer)
object]));
default ->
throw new HopValueException(
this
@@ -4730,6 +4796,17 @@ public class ValueMetaBase implements IValueMeta {
(getConversionMask() != null
&&
getConversionMask().equals(storageMetadata.getConversionMask()))
|| (getConversionMask() == null &&
storageMetadata.getConversionMask() == null);
+ } else if (isBoolean()) {
+ // The Boolean mask decides the text, and without one the length
does (Y/N or
+ // true/false). The stored bytes can only be written as they are
if both agree.
+ //
+ String[] texts = getBooleanMaskTexts(getConversionMask());
+ String[] storageTexts =
getBooleanMaskTexts(storageMetadata.getConversionMask());
+ if (texts == null && storageTexts == null) {
+ identicalFormat = getLength() == storageMetadata.getLength();
+ } else {
+ identicalFormat = Arrays.equals(texts, storageTexts);
+ }
} else if (isNumeric()) {
// Check the lengths first
//
diff --git
a/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseTest.java
b/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseTest.java
index a5bc242c96..b590e398b3 100644
--- a/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseTest.java
+++ b/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseTest.java
@@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -303,6 +304,154 @@ class ValueMetaBaseTest {
assertFalse(ValueMetaBase.convertStringToBoolean("NOT false"));
}
+ @Test
+ void testBooleanToStringWithoutMaskFollowsLength() throws HopValueException {
+ // Without a mask, the length decides (#5958): 3 or more prints
true/false, anything else Y/N
+ assertEquals("Y", new ValueMetaBoolean("b").getString(true));
+ assertEquals("N", new ValueMetaBoolean("b", 1, -1).getString(false));
+ assertEquals("true", new ValueMetaBoolean("b", 4, -1).getString(true));
+ assertEquals("false", new ValueMetaBoolean("b", 4, -1).getString(false));
+ }
+
+ @Test
+ void testBooleanToStringWithMask() throws HopValueException {
+ ValueMetaBoolean shortField = new ValueMetaBoolean("b");
+ ValueMetaBoolean longField = new ValueMetaBoolean("b", 4, -1);
+ for (String mask : Const.getBooleanFormats()) {
+ String[] texts = mask.split("/");
+ shortField.setConversionMask(mask);
+ longField.setConversionMask(mask);
+ // The mask wins over the length, so both fields print the same
+ assertEquals(texts[0], shortField.getString(true), mask);
+ assertEquals(texts[1], shortField.getString(false), mask);
+ assertEquals(texts[0], longField.getString(true), mask);
+ assertEquals(texts[1], longField.getString(false), mask);
+ }
+
+ ValueMetaBoolean custom = new ValueMetaBoolean("b");
+ custom.setConversionMask("Ja/Nee");
+ assertEquals("Ja", custom.getString(true));
+ assertEquals("Nee", custom.getString(false));
+ assertNull(custom.getString(null));
+ }
+
+ @Test
+ void testBooleanToBinaryStringWithMask() throws HopValueException {
+ ValueMetaBoolean field = new ValueMetaBoolean("b");
+ field.setConversionMask("yes/no");
+ assertArrayEquals("yes".getBytes(), field.getBinaryString(true));
+ assertArrayEquals("no".getBytes(), field.getBinaryString(false));
+ }
+
+ @Test
+ void testNonBooleanMaskIsIgnored() throws HopValueException {
+ ValueMetaBoolean field = new ValueMetaBoolean("b");
+ // Yes/yes: the same word twice ignoring case, which reading could not
tell apart
+ for (String mask : new String[] {"yyyy/MM/dd", "#.##", "/N", "Y/", "/", "
/ ", "Yes/yes"}) {
+ field.setConversionMask(mask);
+ assertEquals("Y", field.getString(true), mask);
+ assertEquals("N", field.getString(false), mask);
+ }
+ }
+
+ @Test
+ void testStringToBooleanWithMask() throws HopValueException {
+ ValueMetaString string = new ValueMetaString("s");
+ string.setConversionMask("Ja/Nee");
+ assertTrue(string.getBoolean("Ja"));
+ assertTrue(string.getBoolean("ja"));
+ assertFalse(string.getBoolean("NEE"));
+ // Text outside the mask falls back to the standard rules
+ assertTrue(string.getBoolean("Y"));
+ assertTrue(string.getBoolean("true"));
+ assertFalse(string.getBoolean("maybe"));
+ assertNull(string.getBoolean(""));
+
+ // A mask whose true text is not in the standard rules, e.g. T/F
+ string.setConversionMask("T/F");
+ assertTrue(string.getBoolean("t"));
+ assertFalse(string.getBoolean("F"));
+ }
+
+ @Test
+ void testBooleanMaskTextsAreTrimmed() throws HopValueException {
+ ValueMetaBoolean field = new ValueMetaBoolean("b");
+ field.setConversionMask("Ja / Nein");
+ assertEquals("Ja", field.getString(true));
+ assertEquals("Nein", field.getString(false));
+
+ // What was written reads back, also once a reader trimmed it
+ ValueMetaString string = new ValueMetaString("s");
+ string.setConversionMask("Ja / Nein");
+ assertTrue(string.getBoolean("Ja"));
+ assertFalse(string.getBoolean("Nein"));
+ assertFalse(string.getBoolean(" nein "));
+ }
+
+ /**
+ * Typed text becomes a Boolean through a String value whose conversion
metadata is the Boolean:
+ * the Enter Value dialog, a condition's value, a sorted table cell. The
mask is on the Boolean.
+ */
+ @Test
+ void testStringToBooleanUsesTheMaskOfTheConversionMetadata() throws
HopValueException {
+ for (String[] mask :
+ new String[][] {{"Ja/Nee", "Ja", "Nee"}, {"T/F", "T", "F"}, {"on/off",
"on", "off"}}) {
+ ValueMetaBoolean booleanMeta = new ValueMetaBoolean("b");
+ booleanMeta.setConversionMask(mask[0]);
+ ValueMetaString stringMeta = new ValueMetaString("s");
+ stringMeta.setConversionMetadata(booleanMeta);
+
+ assertEquals(Boolean.TRUE,
stringMeta.convertDataUsingConversionMetaData(mask[1]), mask[0]);
+ assertEquals(Boolean.FALSE,
stringMeta.convertDataUsingConversionMetaData(mask[2]), mask[0]);
+ }
+ }
+
+ /**
+ * A Boolean read lazily keeps the bytes of the file. They are only written
as they are while the
+ * format still matches: another mask, or another length without one,
converts them.
+ */
+ @Test
+ void testBinaryStringBooleanFollowsAChangedMask() throws Exception {
+ ValueMetaBoolean field = new ValueMetaBoolean("b");
+ field.setConversionMask("Y/N");
+ IValueMeta storage = ValueMetaFactory.cloneValueMeta(field,
IValueMeta.TYPE_STRING);
+ field.setStorageMetadata(storage);
+ field.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
+
+ // Same mask as when it was read: the bytes go out unchanged
+ byte[] yes = "Y".getBytes();
+ assertSame(yes, field.getBinaryString(yes));
+
+ field.setConversionMask("Ja/Nee");
+ assertArrayEquals("Ja".getBytes(), field.getBinaryString("Y".getBytes()));
+ assertArrayEquals("Nee".getBytes(), field.getBinaryString("N".getBytes()));
+
+ // No mask on either side: the length decides the text, so a new length
converts too
+ ValueMetaBoolean read = new ValueMetaBoolean("b", 1, -1);
+ IValueMeta readStorage = ValueMetaFactory.cloneValueMeta(read,
IValueMeta.TYPE_STRING);
+ read.setStorageMetadata(readStorage);
+ read.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
+ assertSame(yes, read.getBinaryString(yes));
+
+ ValueMetaBoolean longer = new ValueMetaBoolean("b", 5, -1);
+ longer.setStorageMetadata(readStorage);
+ longer.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
+ assertArrayEquals("true".getBytes(),
longer.getBinaryString("Y".getBytes()));
+ }
+
+ @Test
+ void testConvertDataFromStringToBooleanWithMask() throws Exception {
+ ValueMetaBoolean target = new ValueMetaBoolean("b");
+ target.setConversionMask("on/off");
+ IValueMeta convertMeta = ValueMetaFactory.cloneValueMeta(target,
IValueMeta.TYPE_STRING);
+ assertEquals(
+ Boolean.TRUE,
+ target.convertDataFromString("on", convertMeta, null, null,
IValueMeta.TRIM_TYPE_BOTH));
+ assertEquals(
+ Boolean.FALSE,
+ target.convertDataFromString(" OFF ", convertMeta, null, null,
IValueMeta.TRIM_TYPE_BOTH));
+ }
+
@Test
void testConvertDataFromStringToString() throws HopValueException {
ValueMetaBase inValueMetaString = new ValueMetaString();
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/formatting-values.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/formatting-values.adoc
index 3d17cdcc30..ce87f0c7d6 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/formatting-values.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/formatting-values.adoc
@@ -194,6 +194,63 @@ Fixes, in order of preference:
When you *write* dates to text, CSV, JSON or logs from a DST-observing JVM,
use a mask with an offset, or set the output field's Date Time Zone to `UTC`.
Otherwise a reader in another zone — or the same pipeline after a DST weekend
— will not reconstruct the instant you had.
+== Boolean formats
+
+A Boolean format is the text for true, a slash, and the text for false.
+It is used both to print a Boolean and to read one from text.
+
+[options="header"]
+|===
+|Format|true|false
+|`true/false`|`true`|`false`
+|`Y/N`|`Y`|`N`
+|`1/0`|`1`|`0`
+|`yes/no`|`yes`|`no`
+|===
+
+Without a format, the Length of the field decides how a Boolean is printed.
+See xref:concepts.adoc#boolean-type[Boolean] for the details.
+
+[[boolean-own-format]]
+=== Your own true and false text
+
+The list above only holds suggestions.
+You can type any pair into the Format column, so a file can use the words of
its readers' language:
+
+[options="header"]
+|===
+|Format|true|false
+|`Ja/Nee`|`Ja`|`Nee`
+|`Ja/Nein`|`Ja`|`Nein`
+|`Oui/Non`|`Oui`|`Non`
+|`Sí/No`|`Sí`|`No`
+|`on/off`|`on`|`off`
+|===
+
+For example, to write German Booleans with
xref:pipeline/transforms/textfileoutput.adoc[Text File Output]:
+
+. Open the *Fields* tab and click *Get Fields*.
+. Check that the *Type* of the field is `Boolean`.
+Text File Output does not convert types, so a field that arrives as a String
has to be converted first, for example with
xref:pipeline/transforms/selectvalues.adoc[Select Values].
+. Type `Ja/Nein` in the *Format* column.
+
+Every `true` is then written as `Ja` and every `false` as `Nein`, whatever
format or length the field had before.
+A null stays empty.
+
+To read such a file again, give the field the same Format in
xref:pipeline/transforms/textfileinput.adoc[Text File Input] or in the
Meta-data tab of xref:pipeline/transforms/selectvalues.adoc[Select Values].
+Reading ignores case, so `ja`, `JA` and `Ja` are all true.
+Text that matches neither word falls back to the standard rules: `Y`, `yes`,
`true` and `1` are true, anything else is false.
+
+Keep these rules in mind when you define your own format:
+
+* Use exactly one slash: the text before it means true, the text after it
means false.
+Neither word can contain a slash, and a mask with more than one slash (such as
the date mask `yyyy/MM/dd`) is not treated as a Boolean format.
+* Both words must be non-empty.
+* Spaces around the slash are not part of the words: `Ja / Nein` writes `Ja`
and `Nein`.
+* Pick two words that differ in more than upper and lower case.
+Reading ignores case, so a format like `Yes/yes` could never tell true from
false and is not treated as a Boolean format.
+* Text File Output ignores all formats when *Fast data dump (no formatting)*
is enabled, and then writes `true` or `false`.
+
== Rounding
include::../snippets/rounding-types.adoc[]
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileoutput.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileoutput.adoc
index a35e1a7aa5..6a14d2e6d5 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileoutput.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileoutput.adoc
@@ -99,6 +99,7 @@ To use Unicode specify UTF-8 or UTF-16.
Note: Only one file is placed in a single compressed archive.
|Right pad fields|Add spaces to the end of the fields (or remove characters at
the end) until they have the length specific on the Fields tab.
|Fast data dump (no formatting)|Improves the performance when dumping large
amounts of data to a text file by not including any formatting information.
+Field formats are ignored, so a Boolean is written as `true` or `false`.
|Split every ... rows|Specify a number of rows to split the file into chunks
of that many rows.
|Add Ending line of file|Specify an alternate ending row to the output file.
|===
@@ -114,7 +115,9 @@ The table below describes each of the options for
configuring the field properti
|Schema Definition|Name of the
xref:metadata-types/static-schema-definition.adoc[Schema Definition] that we
want to reference.
|Name|The name of the field.
|Type|Data type of the field. This can be either String, Date or Number.
-|Format|Select a format to apply to the field value. Only applies to numbers
and dates.
+|Format|Select a format to apply to the field value. Applies to numbers, dates
and Booleans.
+For a Boolean, the format is the text for true, a slash and the text for
false, so `Ja/Nein` writes German words instead of `Y` / `N`.
+You can type your own pair, see
xref:pipeline/formatting-values.adoc#boolean-own-format[Your own true and false
text].
See xref:pipeline/formatting-values.adoc[Formatting numbers and dates].
|Length a| The length option depends on the field type:
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/snippets/field-metadata/field-columns.adoc
b/docs/hop-user-manual/modules/ROOT/pages/snippets/field-metadata/field-columns.adoc
index 0fa8423835..e519630019 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/snippets/field-metadata/field-columns.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/snippets/field-metadata/field-columns.adoc
@@ -22,12 +22,13 @@ They do *not* change the value stored in a database.
[options="header"]
|===
|Column|Description
-|Format / Conversion mask|Number or date pattern used to parse and print the
value.
+|Format / Conversion mask|Number, date or Boolean pattern used to parse and
print the value.
See xref:pipeline/formatting-values.adoc[Formatting numbers and dates].
|Length|String: maximum characters.
Number / BigNumber: significant digits.
Integer: display width (leading zeros when writing).
Date: length of the printed string (for example `4` prints only the year).
+Boolean without a Format: `3` or more prints `true` / `false`, anything else
prints `Y` / `N`.
Use `-1` when unset.
|Precision|Decimal digits after the radix for Number and BigNumber.
Not used for String, Date or Boolean.
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/snippets/hop-concepts/data-types.adoc
b/docs/hop-user-manual/modules/ROOT/pages/snippets/hop-concepts/data-types.adoc
index 7b10aeb891..5ee1754cea 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/snippets/hop-concepts/data-types.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/snippets/hop-concepts/data-types.adoc
@@ -56,6 +56,39 @@ A common issue is the decimal separator, which differs
between environments: an
See xref:pipeline/formatting-values.adoc[Formatting numbers and dates] for
Format, Length, Precision, Decimal, Group and Currency.
+[[boolean-type]]
+=== Boolean
+
+A Boolean field holds `true`, `false` or null.
+
+*Reading text as a Boolean*
+
+- Without a Format, `Y`, `yes`, `true` (in any case) and `1` are read as true.
Any other text is read as false, and empty text is null.
+- With a Boolean Format such as `on/off`, text that matches the true or the
false part of the mask (ignoring case) is read as that value.
+Text that matches neither part falls back to the rules above.
+
+*Writing a Boolean as text*
+
+This applies wherever Hop turns a Boolean into text: Write to log, the preview
grid, text files, Concat fields and so on.
+
+- With a Boolean Format, Hop prints the true or the false part of the mask.
+- Without a Format, the field's *Length* decides: a length of 3 or more prints
`true` / `false`, any other length (including no length) prints `Y` / `N`.
+
+A Boolean Format is the text for true, a slash, and the text for false, for
example `true/false`, `Y/N`, `1/0` or `yes/no`.
+The mask must contain exactly one slash, so neither text can contain a slash
itself.
+Spaces around the slash are ignored, and the two texts must differ in more
than upper and lower case.
+The Format column offers the common masks for Boolean fields, and you can type
any other pair, for example `Ja/Nein` to write German words.
+See xref:pipeline/formatting-values.adoc#boolean-own-format[Your own true and
false text].
+
+*Why two Boolean fields can print differently*
+
+The length is often set for you rather than by you.
+Converting a String field to a Boolean, for example in the Meta-data tab of
xref:pipeline/transforms/selectvalues.adoc[Select Values], keeps the String's
length.
+A `true`/`false` column read from a database with length 4 therefore becomes a
Boolean of length 4 and prints `true`, while a Boolean read by JSON Input has
no length and prints `Y`.
+Both values are the same `true`.
+
+Set a Boolean Format on the fields to get the same text everywhere, whatever
their length.
+
=== Timestamp
A Timestamp value can be created from a Date, String, Number, or another
Timestamp.
diff --git a/integration-tests/transforms/0118-boolean-format-read-back.hpl
b/integration-tests/transforms/0118-boolean-format-read-back.hpl
new file mode 100644
index 0000000000..523149143e
--- /dev/null
+++ b/integration-tests/transforms/0118-boolean-format-read-back.hpl
@@ -0,0 +1,261 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+ <info>
+ <pipeline_version/>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <pipeline_type>Normal</pipeline_type>
+ <pipeline_status>0</pipeline_status>
+ <parameters/>
+ <name>0118-boolean-format-read-back</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Issue #5958: reads the file written by
0118-boolean-format-text-file-output as plain text, so the unit test sees
exactly what Text File Output wrote for each Boolean format.</description>
+ <extended_description/>
+ <created_user>-</created_user>
+ <modified_user>-</modified_user>
+ <created_date>2026/09/23 12:00:00.000</created_date>
+ <modified_date>2026/09/23 12:00:00.000</modified_date>
+ </info>
+ <transform>
+ <type>TextFileInput2</type>
+ <name>Read written text</name>
+ <file_type>CSV</file_type>
+ <compression>None</compression>
+ <separator>;</separator>
+ <enclosure>"</enclosure>
+ <enclosure_breaks>N</enclosure_breaks>
+ <escapechar/>
+ <header>Y</header>
+ <prependFileName>N</prependFileName>
+ <nr_headerlines>1</nr_headerlines>
+ <footer>N</footer>
+ <nr_footerlines>1</nr_footerlines>
+ <line_wrapped>N</line_wrapped>
+ <nr_wraps>1</nr_wraps>
+ <layout_paged>N</layout_paged>
+ <nr_lines_per_page>80</nr_lines_per_page>
+ <nr_lines_doc_header>0</nr_lines_doc_header>
+ <noempty>Y</noempty>
+ <include>N</include>
+ <include_field/>
+ <rownum>N</rownum>
+ <rownum_field/>
+ <rownumByFile>N</rownumByFile>
+ <format>mixed</format>
+ <encoding>UTF-8</encoding>
+ <limit>0</limit>
+ <date_format_lenient>Y</date_format_lenient>
+ <date_format_locale>en_US</date_format_locale>
+ <length>Characters</length>
+ <filters/>
+ <error_count_field/>
+ <error_fields_field/>
+ <error_text_field/>
+ <error_line_skipped>N</error_line_skipped>
+ <schema_definition/>
+ <ignore_fields>N</ignore_fields>
+ <shortFileFieldName/>
+ <extensionFieldName/>
+ <pathFieldName/>
+ <sizeFieldName/>
+ <hiddenFieldName/>
+ <lastModificationTimeFieldName/>
+ <uriNameFieldName/>
+ <rootUriNameFieldName/>
+ <files>
+ <file>
+ <name>${java.io.tmpdir}/0118/boolean-formats-out.csv</name>
+ <filemask/>
+ <exclude_filemask/>
+ <file_required>Y</file_required>
+ <include_subfolders>N</include_subfolders>
+ </file>
+ </files>
+ <accept_filenames>N</accept_filenames>
+ <accept_transform_name/>
+ <passing_through_fields>N</passing_through_fields>
+ <accept_field/>
+ <add_to_result_filenames>N</add_to_result_filenames>
+ <error_ignored>N</error_ignored>
+ <file_error_field/>
+ <file_error_message_field/>
+ <skip_bad_files>N</skip_bad_files>
+ <bad_line_files_destination_directory/>
+ <bad_line_files_extension>warning</bad_line_files_extension>
+ <error_line_files_destination_directory/>
+ <error_line_files_extension>error</error_line_files_extension>
+ <line_number_files_destination_directory/>
+ <line_number_files_extension>line</line_number_files_extension>
+ <fields>
+ <field>
+ <name>id</name>
+ <position>-1</position>
+ <length>-1</length>
+ <type>String</type>
+ <ignore>N</ignore>
+ <format/>
+ <trim_type>none</trim_type>
+ <precision>-1</precision>
+ <currency/>
+ <decimal/>
+ <group/>
+ <repeat>N</repeat>
+ <null_string/>
+ <if_null_value/>
+ </field>
+ <field>
+ <name>one_zero</name>
+ <position>-1</position>
+ <length>-1</length>
+ <type>String</type>
+ <ignore>N</ignore>
+ <format/>
+ <trim_type>none</trim_type>
+ <precision>-1</precision>
+ <currency/>
+ <decimal/>
+ <group/>
+ <repeat>N</repeat>
+ <null_string/>
+ <if_null_value/>
+ </field>
+ <field>
+ <name>y_n</name>
+ <position>-1</position>
+ <length>-1</length>
+ <type>String</type>
+ <ignore>N</ignore>
+ <format/>
+ <trim_type>none</trim_type>
+ <precision>-1</precision>
+ <currency/>
+ <decimal/>
+ <group/>
+ <repeat>N</repeat>
+ <null_string/>
+ <if_null_value/>
+ </field>
+ <field>
+ <name>true_false</name>
+ <position>-1</position>
+ <length>-1</length>
+ <type>String</type>
+ <ignore>N</ignore>
+ <format/>
+ <trim_type>none</trim_type>
+ <precision>-1</precision>
+ <currency/>
+ <decimal/>
+ <group/>
+ <repeat>N</repeat>
+ <null_string/>
+ <if_null_value/>
+ </field>
+ <field>
+ <name>yes_no</name>
+ <position>-1</position>
+ <length>-1</length>
+ <type>String</type>
+ <ignore>N</ignore>
+ <format/>
+ <trim_type>none</trim_type>
+ <precision>-1</precision>
+ <currency/>
+ <decimal/>
+ <group/>
+ <repeat>N</repeat>
+ <null_string/>
+ <if_null_value/>
+ </field>
+ <field>
+ <name>ja_nee</name>
+ <position>-1</position>
+ <length>-1</length>
+ <type>String</type>
+ <ignore>N</ignore>
+ <format/>
+ <trim_type>none</trim_type>
+ <precision>-1</precision>
+ <currency/>
+ <decimal/>
+ <group/>
+ <repeat>N</repeat>
+ <null_string/>
+ <if_null_value/>
+ </field>
+ <field>
+ <name>no_format</name>
+ <position>-1</position>
+ <length>-1</length>
+ <type>String</type>
+ <ignore>N</ignore>
+ <format/>
+ <trim_type>none</trim_type>
+ <precision>-1</precision>
+ <currency/>
+ <decimal/>
+ <group/>
+ <repeat>N</repeat>
+ <null_string/>
+ <if_null_value/>
+ </field>
+ </fields>
+ <distribute>Y</distribute>
+ <copies>1</copies>
+ <GUI>
+ <xloc>128</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ <description/>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ </transform>
+ <transform>
+ <type>Dummy</type>
+ <name>OUTPUT</name>
+ <distribute>Y</distribute>
+ <copies>1</copies>
+ <GUI>
+ <xloc>320</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ <description/>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ </transform>
+ <order>
+ <hop>
+ <from>Read written text</from>
+ <to>OUTPUT</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <notepads/>
+ <attributes/>
+ <transform_error_handling/>
+</pipeline>
diff --git
a/integration-tests/transforms/0118-boolean-format-text-file-output.hpl
b/integration-tests/transforms/0118-boolean-format-text-file-output.hpl
new file mode 100644
index 0000000000..1e36ffce6e
--- /dev/null
+++ b/integration-tests/transforms/0118-boolean-format-text-file-output.hpl
@@ -0,0 +1,417 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+ <info>
+ <pipeline_version/>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <pipeline_type>Normal</pipeline_type>
+ <pipeline_status>0</pipeline_status>
+ <parameters/>
+ <name>0118-boolean-format-text-file-output</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Issue #5958: reads Booleans written as 1/0, Y/N, true/false
and yes/no with a Boolean format on each Text File Input field, converts a
Ja/Nee String to Boolean in Select Values, then writes every field in a
different Boolean format with Text File Output. The no_format field has neither
a format nor a length, so it keeps the Y/N default.
0118-boolean-format-read-back verifies the written text.</description>
+ <extended_description/>
+ <created_user>-</created_user>
+ <modified_user>-</modified_user>
+ <created_date>2026/09/23 12:00:00.000</created_date>
+ <modified_date>2026/09/23 12:00:00.000</modified_date>
+ </info>
+ <transform>
+ <type>TextFileInput2</type>
+ <name>Read Booleans</name>
+ <file_type>CSV</file_type>
+ <compression>None</compression>
+ <separator>;</separator>
+ <enclosure>"</enclosure>
+ <enclosure_breaks>N</enclosure_breaks>
+ <escapechar/>
+ <header>Y</header>
+ <prependFileName>N</prependFileName>
+ <nr_headerlines>1</nr_headerlines>
+ <footer>N</footer>
+ <nr_footerlines>1</nr_footerlines>
+ <line_wrapped>N</line_wrapped>
+ <nr_wraps>1</nr_wraps>
+ <layout_paged>N</layout_paged>
+ <nr_lines_per_page>80</nr_lines_per_page>
+ <nr_lines_doc_header>0</nr_lines_doc_header>
+ <noempty>Y</noempty>
+ <include>N</include>
+ <include_field/>
+ <rownum>N</rownum>
+ <rownum_field/>
+ <rownumByFile>N</rownumByFile>
+ <format>mixed</format>
+ <encoding>UTF-8</encoding>
+ <limit>0</limit>
+ <date_format_lenient>Y</date_format_lenient>
+ <date_format_locale>en_US</date_format_locale>
+ <length>Characters</length>
+ <filters/>
+ <error_count_field/>
+ <error_fields_field/>
+ <error_text_field/>
+ <error_line_skipped>N</error_line_skipped>
+ <schema_definition/>
+ <ignore_fields>N</ignore_fields>
+ <shortFileFieldName/>
+ <extensionFieldName/>
+ <pathFieldName/>
+ <sizeFieldName/>
+ <hiddenFieldName/>
+ <lastModificationTimeFieldName/>
+ <uriNameFieldName/>
+ <rootUriNameFieldName/>
+ <files>
+ <file>
+ <name>${PROJECT_HOME}/files/boolean-formats.csv</name>
+ <filemask/>
+ <exclude_filemask/>
+ <file_required>Y</file_required>
+ <include_subfolders>N</include_subfolders>
+ </file>
+ </files>
+ <accept_filenames>N</accept_filenames>
+ <accept_transform_name/>
+ <passing_through_fields>N</passing_through_fields>
+ <accept_field/>
+ <add_to_result_filenames>N</add_to_result_filenames>
+ <error_ignored>N</error_ignored>
+ <file_error_field/>
+ <file_error_message_field/>
+ <skip_bad_files>N</skip_bad_files>
+ <bad_line_files_destination_directory/>
+ <bad_line_files_extension>warning</bad_line_files_extension>
+ <error_line_files_destination_directory/>
+ <error_line_files_extension>error</error_line_files_extension>
+ <line_number_files_destination_directory/>
+ <line_number_files_extension>line</line_number_files_extension>
+ <fields>
+ <field>
+ <name>id</name>
+ <position>-1</position>
+ <length>-1</length>
+ <type>Integer</type>
+ <ignore>N</ignore>
+ <format>#</format>
+ <trim_type>none</trim_type>
+ <precision>-1</precision>
+ <currency/>
+ <decimal/>
+ <group/>
+ <repeat>N</repeat>
+ <null_string/>
+ <if_null_value/>
+ </field>
+ <field>
+ <name>one_zero</name>
+ <position>-1</position>
+ <length>-1</length>
+ <type>Boolean</type>
+ <ignore>N</ignore>
+ <format>1/0</format>
+ <trim_type>none</trim_type>
+ <precision>-1</precision>
+ <currency/>
+ <decimal/>
+ <group/>
+ <repeat>N</repeat>
+ <null_string/>
+ <if_null_value/>
+ </field>
+ <field>
+ <name>y_n</name>
+ <position>-1</position>
+ <length>-1</length>
+ <type>Boolean</type>
+ <ignore>N</ignore>
+ <format>Y/N</format>
+ <trim_type>none</trim_type>
+ <precision>-1</precision>
+ <currency/>
+ <decimal/>
+ <group/>
+ <repeat>N</repeat>
+ <null_string/>
+ <if_null_value/>
+ </field>
+ <field>
+ <name>true_false</name>
+ <position>-1</position>
+ <length>-1</length>
+ <type>Boolean</type>
+ <ignore>N</ignore>
+ <format>true/false</format>
+ <trim_type>none</trim_type>
+ <precision>-1</precision>
+ <currency/>
+ <decimal/>
+ <group/>
+ <repeat>N</repeat>
+ <null_string/>
+ <if_null_value/>
+ </field>
+ <field>
+ <name>yes_no</name>
+ <position>-1</position>
+ <length>-1</length>
+ <type>Boolean</type>
+ <ignore>N</ignore>
+ <format>yes/no</format>
+ <trim_type>none</trim_type>
+ <precision>-1</precision>
+ <currency/>
+ <decimal/>
+ <group/>
+ <repeat>N</repeat>
+ <null_string/>
+ <if_null_value/>
+ </field>
+ <field>
+ <name>ja_nee</name>
+ <position>-1</position>
+ <length>3</length>
+ <type>String</type>
+ <ignore>N</ignore>
+ <format/>
+ <trim_type>none</trim_type>
+ <precision>-1</precision>
+ <currency/>
+ <decimal/>
+ <group/>
+ <repeat>N</repeat>
+ <null_string/>
+ <if_null_value/>
+ </field>
+ <field>
+ <name>no_format</name>
+ <position>-1</position>
+ <length>-1</length>
+ <type>Boolean</type>
+ <ignore>N</ignore>
+ <format/>
+ <trim_type>none</trim_type>
+ <precision>-1</precision>
+ <currency/>
+ <decimal/>
+ <group/>
+ <repeat>N</repeat>
+ <null_string/>
+ <if_null_value/>
+ </field>
+ </fields>
+ <distribute>Y</distribute>
+ <copies>1</copies>
+ <GUI>
+ <xloc>128</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ <description/>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ </transform>
+ <transform>
+ <type>SelectValues</type>
+ <name>ja_nee to Boolean</name>
+ <fields>
+ <select_unspecified>N</select_unspecified>
+ <meta>
+ <name>ja_nee</name>
+ <rename>ja_nee</rename>
+ <type>Boolean</type>
+ <length>-2</length>
+ <precision>-2</precision>
+ <storage_type/>
+ <conversion_mask>Ja/Nee</conversion_mask>
+ <date_format_lenient>N</date_format_lenient>
+ <date_format_locale/>
+ <date_format_timezone/>
+ <lenient_string_to_number>N</lenient_string_to_number>
+ <decimal_symbol/>
+ <grouping_symbol/>
+ <currency_symbol/>
+ <encoding/>
+ </meta>
+ </fields>
+ <distribute>Y</distribute>
+ <copies>1</copies>
+ <GUI>
+ <xloc>320</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ <description/>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ </transform>
+ <transform>
+ <type>TextFileOutput</type>
+ <name>Write other formats</name>
+ <separator>;</separator>
+ <enclosure>"</enclosure>
+ <enclosure_forced>N</enclosure_forced>
+ <enclosure_fix_disabled>N</enclosure_fix_disabled>
+ <header>Y</header>
+ <footer>N</footer>
+ <format>UNIX</format>
+ <compression>None</compression>
+ <encoding>UTF-8</encoding>
+ <endedLine/>
+ <fileNameInField>N</fileNameInField>
+ <fileNameField/>
+ <create_parent_folder>Y</create_parent_folder>
+ <file>
+ <name>${java.io.tmpdir}/0118/boolean-formats-out</name>
+ <do_not_open_new_file_init>N</do_not_open_new_file_init>
+ <extension>csv</extension>
+ <append>N</append>
+ <split>N</split>
+ <haspartno>N</haspartno>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <SpecifyFormat>N</SpecifyFormat>
+ <date_time_format/>
+ <add_to_result_filenames>N</add_to_result_filenames>
+ <pad>N</pad>
+ <fast_dump>N</fast_dump>
+ <splitevery>0</splitevery>
+ </file>
+ <fields>
+ <field>
+ <name>id</name>
+ <type>Integer</type>
+ <format>#</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <trim_type>none</trim_type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ <field>
+ <name>one_zero</name>
+ <type>Boolean</type>
+ <format>true/false</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <trim_type>none</trim_type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ <field>
+ <name>y_n</name>
+ <type>Boolean</type>
+ <format>1/0</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <trim_type>none</trim_type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ <field>
+ <name>true_false</name>
+ <type>Boolean</type>
+ <format>Y/N</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <trim_type>none</trim_type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ <field>
+ <name>yes_no</name>
+ <type>Boolean</type>
+ <format>Ja/Nee</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <trim_type>none</trim_type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ <field>
+ <name>ja_nee</name>
+ <type>Boolean</type>
+ <format>yes/no</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <trim_type>none</trim_type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ <field>
+ <name>no_format</name>
+ <type>Boolean</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif/>
+ <trim_type>none</trim_type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ </fields>
+ <distribute>Y</distribute>
+ <copies>1</copies>
+ <GUI>
+ <xloc>512</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ <description/>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ </transform>
+ <order>
+ <hop>
+ <from>Read Booleans</from>
+ <to>ja_nee to Boolean</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>ja_nee to Boolean</from>
+ <to>Write other formats</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <notepads/>
+ <attributes/>
+ <transform_error_handling/>
+</pipeline>
diff --git a/integration-tests/transforms/datasets/golden-boolean-formats.csv
b/integration-tests/transforms/datasets/golden-boolean-formats.csv
new file mode 100644
index 0000000000..dd5a06b4f2
--- /dev/null
+++ b/integration-tests/transforms/datasets/golden-boolean-formats.csv
@@ -0,0 +1,5 @@
+id,one_zero,y_n,true_false,yes_no,ja_nee,no_format
+1,true,1,Y,Ja,yes,Y
+2,false,0,N,Nee,no,N
+3,true,0,Y,Nee,yes,Y
+4,,,,,,
diff --git a/integration-tests/transforms/files/boolean-formats.csv
b/integration-tests/transforms/files/boolean-formats.csv
new file mode 100644
index 0000000000..09d2f461ca
--- /dev/null
+++ b/integration-tests/transforms/files/boolean-formats.csv
@@ -0,0 +1,5 @@
+id;one_zero;y_n;true_false;yes_no;ja_nee;no_format
+1;1;Y;true;yes;Ja;Y
+2;0;N;false;no;Nee;N
+3;1;n;TRUE;No;ja;true
+4;;;;;;
diff --git a/integration-tests/transforms/main-0118-boolean-format.hwf
b/integration-tests/transforms/main-0118-boolean-format.hwf
new file mode 100644
index 0000000000..f808d9ffb7
--- /dev/null
+++ b/integration-tests/transforms/main-0118-boolean-format.hwf
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+ <name>main-0118-boolean-format</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/09/23 12:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/09/23 12:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>START</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <intervalSeconds>0</intervalSeconds>
+ <intervalMinutes>60</intervalMinutes>
+ <hour>12</hour>
+ <minutes>0</minutes>
+ <weekDay>1</weekDay>
+ <DayOfMonth>1</DayOfMonth>
+ <parallel>N</parallel>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>0118-boolean-format-text-file-output</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+
<filename>${PROJECT_HOME}/0118-boolean-format-text-file-output.hpl</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <set_append_logfile>N</set_append_logfile>
+ <wait_until_finished>Y</wait_until_finished>
+ <follow_abort_remote>N</follow_abort_remote>
+ <create_parent_folder>N</create_parent_folder>
+ <run_configuration>local</run_configuration>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <parallel>N</parallel>
+ <xloc>288</xloc>
+ <yloc>80</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>0118-boolean-format-read-back UNIT</name>
+ <description/>
+ <type>RunPipelineTests</type>
+ <attributes/>
+ <test_names>
+ <test_name>
+ <name>0118-boolean-format-read-back UNIT</name>
+ </test_name>
+ </test_names>
+ <parallel>N</parallel>
+ <xloc>544</xloc>
+ <yloc>80</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>START</from>
+ <to>0118-boolean-format-text-file-output</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>0118-boolean-format-text-file-output</from>
+ <to>0118-boolean-format-read-back UNIT</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git
a/integration-tests/transforms/metadata/dataset/golden-boolean-formats.json
b/integration-tests/transforms/metadata/dataset/golden-boolean-formats.json
new file mode 100644
index 0000000000..320f54bb12
--- /dev/null
+++ b/integration-tests/transforms/metadata/dataset/golden-boolean-formats.json
@@ -0,0 +1,63 @@
+{
+ "base_filename": "golden-boolean-formats.csv",
+ "name": "golden-boolean-formats",
+ "description": "Text written by Text File Output after re-formatting
Booleans read in other formats (#5958).",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "id",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "one_zero",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "y_n",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "true_false",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "yes_no",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "ja_nee",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "no_format",
+ "field_format": ""
+ }
+ ]
+}
diff --git
a/integration-tests/transforms/metadata/unit-test/0118-boolean-format-read-back
UNIT.json
b/integration-tests/transforms/metadata/unit-test/0118-boolean-format-read-back
UNIT.json
new file mode 100644
index 0000000000..4e790340dc
--- /dev/null
+++
b/integration-tests/transforms/metadata/unit-test/0118-boolean-format-read-back
UNIT.json
@@ -0,0 +1,52 @@
+{
+ "variableValues": [],
+ "database_replacements": [],
+ "autoOpening": false,
+ "basePath": "",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "id",
+ "data_set_field": "id"
+ },
+ {
+ "transform_field": "one_zero",
+ "data_set_field": "one_zero"
+ },
+ {
+ "transform_field": "y_n",
+ "data_set_field": "y_n"
+ },
+ {
+ "transform_field": "true_false",
+ "data_set_field": "true_false"
+ },
+ {
+ "transform_field": "yes_no",
+ "data_set_field": "yes_no"
+ },
+ {
+ "transform_field": "ja_nee",
+ "data_set_field": "ja_nee"
+ },
+ {
+ "transform_field": "no_format",
+ "data_set_field": "no_format"
+ }
+ ],
+ "field_order": [
+ "id"
+ ],
+ "transform_name": "OUTPUT",
+ "data_set_name": "golden-boolean-formats"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "0118-boolean-format-read-back UNIT",
+ "description": "Reads back the file written by
0118-boolean-format-text-file-output as text and compares it to the expected
Boolean formats",
+ "trans_test_tweaks": [],
+ "persist_filename": "",
+ "pipeline_filename": "./0118-boolean-format-read-back.hpl",
+ "test_type": "UNIT_TEST"
+}
diff --git
a/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsDialog.java
b/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsDialog.java
index 70190a90e8..979bcd563b 100644
---
a/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsDialog.java
+++
b/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsDialog.java
@@ -284,10 +284,12 @@ public class ConcatFieldsDialog extends
BaseTransformDialog {
// Prepare a list of possible formats...
String[] dats = Const.getDateFormats();
String[] nums = Const.getNumberFormats();
- int totalSize = dats.length + nums.length;
+ String[] bools = Const.getBooleanFormats();
+ int totalSize = dats.length + nums.length + bools.length;
String[] formats = new String[totalSize];
System.arraycopy(dats, 0, formats, 0, dats.length);
System.arraycopy(nums, 0, formats, dats.length, nums.length);
+ System.arraycopy(bools, 0, formats, dats.length + nums.length,
bools.length);
fieldColumns = new ColumnInfo[FieldsCols];
fieldColumns[0] =
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterValueDialog.java
b/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterValueDialog.java
index ae58de38fa..768b880e5b 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterValueDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterValueDialog.java
@@ -311,6 +311,15 @@ public class EnterValueDialog extends Dialog {
case IValueMeta.TYPE_BIGNUMBER:
wFormat.setItems(new String[] {});
break;
+ case IValueMeta.TYPE_BOOLEAN:
+ wFormat.setItems(Const.getBooleanFormats());
+ if (!Utils.isEmpty(formatString)) {
+ if (wFormat.indexOf(formatString) < 0) {
+ wFormat.add(formatString);
+ }
+ wFormat.select(wFormat.indexOf(formatString));
+ }
+ break;
default:
wFormat.setItems(new String[] {});
break;
diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
index 0b0e127eb5..bccae01587 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
@@ -3574,6 +3574,7 @@ public class TableView extends Composite {
case IValueMeta.TYPE_INTEGER, IValueMeta.TYPE_BIGNUMBER,
IValueMeta.TYPE_NUMBER ->
Const.getNumberFormats();
case IValueMeta.TYPE_STRING -> Const.getConversionFormats();
+ case IValueMeta.TYPE_BOOLEAN -> Const.getBooleanFormats();
default -> new String[0];
};
}