This is an automated email from the ASF dual-hosted git repository.

hansva 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 d8dd72fc66 File Metadata: keep the enclosure with quotes inside fields 
and keep all decimals, fixes #5609 (#8555)
d8dd72fc66 is described below

commit d8dd72fc66616ef2262d31c6652387fea05694da
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Sep 24 10:08:14 2026 +0200

    File Metadata: keep the enclosure with quotes inside fields and keep all 
decimals, fixes #5609 (#8555)
---
 .../org/apache/hop/core/util/StringEvaluator.java  | 110 +++++++++++++++--
 .../apache/hop/core/util/StringEvaluatorTest.java  |  91 ++++++++++++++
 .../pages/pipeline/transforms/filemetadata.adoc    |  23 +++-
 ...055-file-metadata-embedded-enclosure-verify.hpl | 102 ++++++++++++++++
 .../golden-file-metadata-embedded-enclosure.csv    |  12 ++
 .../files/file-metadata-embedded-enclosure.csv     |  21 ++++
 .../transforms/main-0055-file-metadata.hwf         |   3 +
 .../golden-file-metadata-embedded-enclosure.json   | 120 ++++++++++++++++++
 ...le-metadata-embedded-enclosure-verify UNIT.json |  93 ++++++++++++++
 .../util/delimiters/DelimiterDetector.java         |  59 +++++----
 .../filemetadata/DelimiterDetectorTest.java        | 104 ++++++++++++++++
 .../FileMetadataEmbeddedEnclosureTest.java         | 134 +++++++++++++++++++++
 .../filemetadata/delimited/embedded-enclosure.csv  |  21 ++++
 .../fileinput/text/TextFileCsvImportScanTest.java  |  31 ++++-
 14 files changed, 887 insertions(+), 37 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/core/util/StringEvaluator.java 
b/core/src/main/java/org/apache/hop/core/util/StringEvaluator.java
index 606c89a8d5..cb28bf5be3 100644
--- a/core/src/main/java/org/apache/hop/core/util/StringEvaluator.java
+++ b/core/src/main/java/org/apache/hop/core/util/StringEvaluator.java
@@ -67,6 +67,8 @@ public class StringEvaluator {
 
   protected static final Pattern PRECISION_PATTERN = 
Pattern.compile("[^0-9#]");
 
+  private static final String NUMBER_CURRENCY = "number-currency";
+
   public StringEvaluator() {
     this(true);
   }
@@ -245,7 +247,11 @@ public class StringEvaluator {
   }
 
   private void evaluatePrecision(String value) {
-    int p = determinePrecision(value);
+    char decimalSymbol =
+        ((DecimalFormat) NumberFormat.getInstance())
+            .getDecimalFormatSymbols()
+            .getDecimalSeparator();
+    int p = determinePrecision(value, decimalSymbol);
     if (p > maxPrecision) {
       maxPrecision = p;
     }
@@ -367,17 +373,96 @@ public class StringEvaluator {
 
       StringEvaluationResult result = evaluationResults.get(0);
       IValueMeta conversionMeta = result.getConversionMeta();
-      if (conversionMeta.isNumber() && conversionMeta.getCurrencySymbol() == 
null) {
-        conversionMeta.setPrecision(maxPrecision);
-        if (maxPrecision > 0 && maxLength > 0) {
-          conversionMeta.setLength(maxLength);
+      if (conversionMeta.isNumber() && !isCurrencyResult(result)) {
+        // Every decimal mask parses every decimal value, so the shortest mask 
(#.#) always wins
+        // the sort. Its precision would round away the decimals the data 
actually has.
+        int dataPrecision = determineDataPrecision(conversionMeta);
+        result = findMaskKeepingPrecision(result, dataPrecision);
+        IValueMeta advised = result.getConversionMeta();
+        if (determinePrecision(advised.getConversionMask()) < dataPrecision) {
+          advised.setConversionMask(widenMask(advised.getConversionMask(), 
dataPrecision));
         }
+        advised.setPrecision(dataPrecision);
       }
 
       return result;
     }
   }
 
+  private static boolean isCurrencyResult(StringEvaluationResult result) {
+    return NUMBER_CURRENCY.equals(result.getConversionMeta().getName());
+  }
+
+  /** The largest number of decimals in the evaluated values, read with the 
mask's symbols. */
+  private int determineDataPrecision(IValueMeta conversionMeta) {
+    char decimalSymbol = conversionMeta.getDecimalSymbol().charAt(0);
+    int precision = 0;
+    for (String value : values) {
+      if (value != null) {
+        precision = Math.max(precision, determinePrecision(value.trim(), 
decimalSymbol));
+      }
+    }
+    return precision;
+  }
+
+  /**
+   * Picks the shortest mask with the same decimal symbol as the given one 
that keeps all the
+   * decimals. When no mask keeps them all, the one keeping the most decimals 
is used.
+   */
+  private StringEvaluationResult findMaskKeepingPrecision(
+      StringEvaluationResult shortest, int dataPrecision) {
+    IValueMeta shortestMeta = shortest.getConversionMeta();
+    StringEvaluationResult best = shortest;
+    int bestPrecision = determinePrecision(shortestMeta.getConversionMask());
+    // the results are sorted on mask length, so the first one that keeps the 
decimals wins
+    for (StringEvaluationResult candidate : evaluationResults) {
+      if (bestPrecision >= dataPrecision) {
+        break;
+      }
+      IValueMeta meta = candidate.getConversionMeta();
+      if (meta.isNumber()
+          && !isCurrencyResult(candidate)
+          && meta.getTrimType() == shortestMeta.getTrimType()
+          && meta.getDecimalSymbol().equals(shortestMeta.getDecimalSymbol())) {
+        int maskPrecision = determinePrecision(meta.getConversionMask());
+        if (maskPrecision > bestPrecision) {
+          best = candidate;
+          bestPrecision = maskPrecision;
+        }
+      }
+    }
+    return best;
+  }
+
+  /**
+   * Gives a mask the given number of decimals. The mask parsed all the values 
already, the number
+   * of decimals in a mask only changes how values are formatted.
+   */
+  static String widenMask(String mask, int precision) {
+    // a mask can have a positive and a negative pattern
+    StringBuilder widened = new StringBuilder();
+    for (String pattern : mask.split(";", -1)) {
+      if (!widened.isEmpty()) {
+        widened.append(';');
+      }
+      int dot = pattern.lastIndexOf('.');
+      if (dot < 0) {
+        widened.append(pattern).append('.').append("0".repeat(precision));
+      } else {
+        int end = pattern.length();
+        Matcher m = PRECISION_PATTERN.matcher(pattern.substring(dot + 1));
+        if (m.find()) {
+          end = dot + 1 + m.start();
+        }
+        widened
+            .append(pattern, 0, dot + 1)
+            .append("0".repeat(precision))
+            .append(pattern.substring(end));
+      }
+    }
+    return widened.toString();
+  }
+
   public String[] getDateFormats() {
     return dateFormats;
   }
@@ -430,7 +515,7 @@ public class StringEvaluator {
       // Try the locale's Currency
       DecimalFormat currencyFormat = ((DecimalFormat) 
NumberFormat.getCurrencyInstance());
 
-      IValueMeta conversionMeta = new ValueMetaNumber("number-currency");
+      IValueMeta conversionMeta = new ValueMetaNumber(NUMBER_CURRENCY);
       // replace the universal currency symbol with the locale's currency 
symbol for user
       // recognition
       String currencyMask =
@@ -497,12 +582,17 @@ public class StringEvaluator {
     }
   }
 
+  /**
+   * The number of decimals a number format mask describes. Masks are 
non-localized {@link
+   * DecimalFormat} patterns, so the decimal separator is always a dot, 
whatever the locale.
+   */
   protected static int determinePrecision(String numericFormat) {
+    return determinePrecision(numericFormat, '.');
+  }
+
+  /** The number of digits (or mask digits) after the last decimal symbol. */
+  protected static int determinePrecision(String numericFormat, char 
decimalSymbol) {
     if (numericFormat != null) {
-      char decimalSymbol =
-          ((DecimalFormat) NumberFormat.getInstance())
-              .getDecimalFormatSymbols()
-              .getDecimalSeparator();
       int loc = numericFormat.lastIndexOf(decimalSymbol);
       if (loc >= 0 && loc < numericFormat.length()) {
         Matcher m = PRECISION_PATTERN.matcher(numericFormat.substring(loc + 
1));
diff --git 
a/core/src/test/java/org/apache/hop/core/util/StringEvaluatorTest.java 
b/core/src/test/java/org/apache/hop/core/util/StringEvaluatorTest.java
index 9d69e64fcd..81d73bf5ca 100644
--- a/core/src/test/java/org/apache/hop/core/util/StringEvaluatorTest.java
+++ b/core/src/test/java/org/apache/hop/core/util/StringEvaluatorTest.java
@@ -30,6 +30,7 @@ import java.util.List;
 import java.util.Locale;
 import org.apache.hop.core.HopClientEnvironment;
 import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.row.IValueMeta;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
@@ -187,6 +188,96 @@ class StringEvaluatorTest {
     assertTrue(evaluator.getAdvicedResult().getConversionMeta().isString());
   }
 
+  /** #5609: the shortest mask (#.#) used to win and round the amounts to one 
decimal. */
+  @Test
+  void testNumberKeepsAllDecimals() {
+    for (String string : Arrays.asList("123.58", "90524.10", "9872.52", 
"130011.63")) {
+      evaluator.evaluateString(string);
+    }
+    IValueMeta advised = evaluator.getAdvicedResult().getConversionMeta();
+    assertTrue(advised.isNumber());
+    assertEquals("#.00", advised.getConversionMask());
+    assertEquals(".", advised.getDecimalSymbol());
+    assertEquals(2, advised.getPrecision());
+  }
+
+  @Test
+  void testNumberPrecisionFollowsTheMostDecimals() {
+    for (String string : Arrays.asList("1.5", "2.125", "3")) {
+      evaluator.evaluateString(string);
+    }
+    IValueMeta advised = evaluator.getAdvicedResult().getConversionMeta();
+    assertTrue(advised.isNumber());
+    assertEquals("#.000", advised.getConversionMask());
+    assertEquals(3, advised.getPrecision());
+  }
+
+  @Test
+  void testNumberWithMoreDecimalsThanAnyMask() {
+    evaluator.evaluateString("1.123456789");
+    evaluator.evaluateString(null);
+    IValueMeta advised = evaluator.getAdvicedResult().getConversionMeta();
+    assertTrue(advised.isNumber());
+    assertEquals("#.000000000", advised.getConversionMask());
+    assertEquals(9, advised.getPrecision());
+  }
+
+  /** Grouped values only parse with a grouped mask, none of which has three 
decimals. */
+  @Test
+  void testGroupedNumberKeepsAllDecimals() {
+    evaluator.evaluateString("1,234.567");
+    IValueMeta advised = evaluator.getAdvicedResult().getConversionMeta();
+    assertTrue(advised.isNumber());
+    assertEquals("#,##0.000", advised.getConversionMask());
+    assertEquals(3, advised.getPrecision());
+  }
+
+  /** A currency mask keeps the precision of the currency. */
+  @Test
+  void testCurrencyKeepsItsMask() {
+    StringEvaluator usEvaluator = new StringEvaluator();
+    usEvaluator.evaluateString("$1,234.5");
+    usEvaluator.evaluateString("$12.50");
+    IValueMeta advised = usEvaluator.getAdvicedResult().getConversionMeta();
+    assertTrue(advised.isNumber());
+    assertEquals("$#,##0.00", advised.getConversionMask());
+    assertEquals(2, advised.getPrecision());
+  }
+
+  @Test
+  void testWidenMask() {
+    assertEquals("#.000", StringEvaluator.widenMask("#.#", 3));
+    assertEquals("#,##0.000", StringEvaluator.widenMask("#,##0.00", 3));
+    assertEquals(" #.0000", StringEvaluator.widenMask(" #.0#", 4));
+    assertEquals("#,##0.00;-#,##0.00", 
StringEvaluator.widenMask("#,##0.0;-#,##0.0", 2));
+    assertEquals("#.00", StringEvaluator.widenMask("#", 2));
+    assertEquals("#.00 EUR", StringEvaluator.widenMask("#.0 EUR", 2));
+  }
+
+  /** Masks always use a dot, the locale's decimal separator must not change 
the precision. */
+  @Test
+  void testNumberPrecisionDoesNotDependOnLocale() {
+    Locale.setDefault(Locale.GERMANY);
+    StringEvaluator germanEvaluator = new StringEvaluator();
+    for (String string : Arrays.asList("123.58", "9872.52")) {
+      germanEvaluator.evaluateString(string);
+    }
+    IValueMeta advised = 
germanEvaluator.getAdvicedResult().getConversionMeta();
+    assertTrue(advised.isNumber());
+    assertEquals("#.00", advised.getConversionMask());
+    assertEquals(2, advised.getPrecision());
+  }
+
+  @Test
+  void testDeterminePrecision() {
+    assertEquals(2, StringEvaluator.determinePrecision("#.00"));
+    assertEquals(2, StringEvaluator.determinePrecision(" #.0#"));
+    assertEquals(0, StringEvaluator.determinePrecision("#"));
+    assertEquals(0, StringEvaluator.determinePrecision(null));
+    assertEquals(3, StringEvaluator.determinePrecision("1.234,567", ','));
+    assertEquals(0, StringEvaluator.determinePrecision("1.234", ','));
+  }
+
   private void testNumber(String mask, String... strings) {
     for (String string : strings) {
       evaluator.evaluateString(string);
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filemetadata.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filemetadata.adoc
index c2fe0d928d..b5a8a9cdf9 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filemetadata.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filemetadata.adoc
@@ -48,16 +48,37 @@ Use 0 rows to scan the entire file.
 |Fallback charset|Charset to use while scanning the file
 |Delimiter candidates|List of delimiters to try while detecting the file 
layout.
 Tab, semicolon, comma are provided by default.
-|Enclosure candidates|List of delimiters to try while detecting the file 
layout.
+|Enclosure candidates|List of enclosure characters to try while detecting the 
file layout.
 Double and single quote are provided by default.
 |===
 
+== Detection rules
+
+=== Enclosures
+
+An enclosure opens a field when it is the first character of the field.
+It closes the field when it is followed by the delimiter or the end of the 
line.
+Inside an enclosed field:
+
+* delimiters are part of the value: `"Smith, Bob"` is one field.
+* a doubled enclosure is an escaped enclosure: `"BOB ""ROBERT"" SMITH"`.
+* any other enclosure is part of the value as well: `"BOB "ROBERT" SMITH"` is 
one field.
+
+An enclosure in the middle of a field that is not enclosed, such as `5" pipe`, 
does not open a field, and the line does not fit that enclosure.
+The same goes for a line that ends inside an enclosed field: fields spanning 
several lines are not supported.
+
+=== Numbers
+
+A number column gets the precision of the value with the most decimals, read 
with the detected decimal symbol.
+The suggested mask keeps all of those decimals: `123.58` and `90524.10` give 
the mask `#.00` and a precision of 2.
+
 == Output Fields
 
 The fields returned by this transform are
 
 * charset
 * delimiter
+* enclosure
 * field_count
 * skip_header_lines
 * skip_footer_lines
diff --git 
a/integration-tests/transforms/0055-file-metadata-embedded-enclosure-verify.hpl 
b/integration-tests/transforms/0055-file-metadata-embedded-enclosure-verify.hpl
new file mode 100644
index 0000000000..b40cc36deb
--- /dev/null
+++ 
b/integration-tests/transforms/0055-file-metadata-embedded-enclosure-verify.hpl
@@ -0,0 +1,102 @@
+<?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>
+    <name>0055-file-metadata-embedded-enclosure-verify</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <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>
+    <created_user>-</created_user>
+    <created_date>2022/11/28 12:13:32.297</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2022/11/28 12:13:32.297</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>files/file-metadata-embedded-enclosure.csv</from>
+      <to>Verify</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Verify</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>432</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>files/file-metadata-embedded-enclosure.csv</name>
+    <type>FileMetadataPlugin</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    
<fileName>${PROJECT_HOME}/files/file-metadata-embedded-enclosure.csv</fileName>
+    <limitRows>10000</limitRows>
+    <defaultCharset>ISO-8859-1</defaultCharset>
+    <delimiterCandidate>
+      <candidate>      </candidate>
+    </delimiterCandidate>
+    <delimiterCandidate>
+      <candidate>;</candidate>
+    </delimiterCandidate>
+    <delimiterCandidate>
+      <candidate>,</candidate>
+    </delimiterCandidate>
+    <enclosureCandidate>
+      <candidate>"</candidate>
+    </enclosureCandidate>
+    <enclosureCandidate>
+      <candidate>'</candidate>
+    </enclosureCandidate>
+    <attributes/>
+    <GUI>
+      <xloc>128</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git 
a/integration-tests/transforms/datasets/golden-file-metadata-embedded-enclosure.csv
 
b/integration-tests/transforms/datasets/golden-file-metadata-embedded-enclosure.csv
new file mode 100644
index 0000000000..7218aac9c9
--- /dev/null
+++ 
b/integration-tests/transforms/datasets/golden-file-metadata-embedded-enclosure.csv
@@ -0,0 +1,12 @@
+charset,delimiter,enclosure,field_count,skip_header_lines,skip_footer_lines,header_line_present,name,type,length,precision,mask,decimal_symbol,grouping_symbol
+US-ASCII,",","""",11,0,0,Y,FY_DC,Integer,15,0,#,".",","
+US-ASCII,",","""",11,0,0,Y,DEPT_CD,String,3,,,".",","
+US-ASCII,",","""",11,0,0,Y,FUND_CD,Integer,15,0,#,".",","
+US-ASCII,",","""",11,0,0,Y,UNIT_CD,Integer,15,0,#,".",","
+US-ASCII,",","""",11,0,0,Y,UNIT_NM,String,28,,,".",","
+US-ASCII,",","""",11,0,0,Y,VEND_CUST_CD,String,13,,,".",","
+US-ASCII,",","""",11,0,0,Y,EDU_VENDOR,Boolean,,,,".",","
+US-ASCII,",","""",11,0,0,Y,LGL_NM_UP,String,18,,,".",","
+US-ASCII,",","""",11,0,0,Y,OBJ_CD,Integer,15,0,#,".",","
+US-ASCII,",","""",11,0,0,Y,OBJ_NM,String,16,,,".",","
+US-ASCII,",","""",11,0,0,Y,SUM_PSTNG_AM,Number,15,2,#.00,".",","
diff --git 
a/integration-tests/transforms/files/file-metadata-embedded-enclosure.csv 
b/integration-tests/transforms/files/file-metadata-embedded-enclosure.csv
new file mode 100644
index 0000000000..e65316b88f
--- /dev/null
+++ b/integration-tests/transforms/files/file-metadata-embedded-enclosure.csv
@@ -0,0 +1,21 @@
+"FY_DC","DEPT_CD","FUND_CD","UNIT_CD","UNIT_NM","VEND_CUST_CD","EDU_VENDOR","LGL_NM_UP","OBJ_CD","OBJ_NM",SUM_PSTNG_AM
+"2023","99B","999","1111","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY one","9876","GRANTS FOR TEST1","123.58"
+"2023","99B","999","2222","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY two","8765","GRANTS FOR TEST2","90524.10"
+"2023","99B","999","3333","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY three","7654","GRANTS FOR TEST3","9872.52"
+"2023","99B","999","4444","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY four","6543","GRANTS FOR TEST4","130011.63"
+"2023","99B","999","5555","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY asides","5432","GRANTS FOR TEST5","20514.57"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","1111","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY one","9876","GRANTS FOR TEST1","123.58"
+"2023","99B","999","2222","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY two","8765","GRANTS FOR TEST2","90524.10"
+"2023","99B","999","3333","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY three","7654","GRANTS FOR TEST3","9872.52"
+"2023","99B","999","4444","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY four","6543","GRANTS FOR TEST4","130011.63"
+"2023","99B","999","5555","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY asides","5432","GRANTS FOR TEST5","20514.57"
\ No newline at end of file
diff --git a/integration-tests/transforms/main-0055-file-metadata.hwf 
b/integration-tests/transforms/main-0055-file-metadata.hwf
index e9e24572f9..8f91faed89 100644
--- a/integration-tests/transforms/main-0055-file-metadata.hwf
+++ b/integration-tests/transforms/main-0055-file-metadata.hwf
@@ -60,6 +60,9 @@ limitations under the License.
         <test_name>
           <name>0055-file-metadata-filename-fromfield-verify UNIT</name>
         </test_name>
+        <test_name>
+          <name>0055-file-metadata-embedded-enclosure-verify UNIT</name>
+        </test_name>
       </test_names>
       <parallel>N</parallel>
       <xloc>336</xloc>
diff --git 
a/integration-tests/transforms/metadata/dataset/golden-file-metadata-embedded-enclosure.json
 
b/integration-tests/transforms/metadata/dataset/golden-file-metadata-embedded-enclosure.json
new file mode 100644
index 0000000000..667541cf41
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/dataset/golden-file-metadata-embedded-enclosure.json
@@ -0,0 +1,120 @@
+{
+  "base_filename": "golden-file-metadata-embedded-enclosure.csv",
+  "name": "golden-file-metadata-embedded-enclosure",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "charset",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "delimiter",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "enclosure",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": 0,
+      "field_name": "field_count",
+      "field_format": "####0;-####0"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": 0,
+      "field_name": "skip_header_lines",
+      "field_format": "####0;-####0"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": 0,
+      "field_name": "skip_footer_lines",
+      "field_format": "####0;-####0"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 4,
+      "field_precision": -1,
+      "field_name": "header_line_present",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "name",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "type",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": 0,
+      "field_name": "length",
+      "field_format": "####0;-####0"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": 0,
+      "field_name": "precision",
+      "field_format": "####0;-####0"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "mask",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "decimal_symbol",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "grouping_symbol",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
\ No newline at end of file
diff --git 
a/integration-tests/transforms/metadata/unit-test/0055-file-metadata-embedded-enclosure-verify
 UNIT.json 
b/integration-tests/transforms/metadata/unit-test/0055-file-metadata-embedded-enclosure-verify
 UNIT.json
new file mode 100644
index 0000000000..ba5a98efc9
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/unit-test/0055-file-metadata-embedded-enclosure-verify
 UNIT.json    
@@ -0,0 +1,93 @@
+{
+  "variableValues": [],
+  "database_replacements": [],
+  "autoOpening": true,
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "charset",
+          "data_set_field": "charset"
+        },
+        {
+          "transform_field": "delimiter",
+          "data_set_field": "delimiter"
+        },
+        {
+          "transform_field": "enclosure",
+          "data_set_field": "enclosure"
+        },
+        {
+          "transform_field": "field_count",
+          "data_set_field": "field_count"
+        },
+        {
+          "transform_field": "skip_header_lines",
+          "data_set_field": "skip_header_lines"
+        },
+        {
+          "transform_field": "skip_footer_lines",
+          "data_set_field": "skip_footer_lines"
+        },
+        {
+          "transform_field": "header_line_present",
+          "data_set_field": "header_line_present"
+        },
+        {
+          "transform_field": "name",
+          "data_set_field": "name"
+        },
+        {
+          "transform_field": "type",
+          "data_set_field": "type"
+        },
+        {
+          "transform_field": "length",
+          "data_set_field": "length"
+        },
+        {
+          "transform_field": "precision",
+          "data_set_field": "precision"
+        },
+        {
+          "transform_field": "mask",
+          "data_set_field": "mask"
+        },
+        {
+          "transform_field": "decimal_symbol",
+          "data_set_field": "decimal_symbol"
+        },
+        {
+          "transform_field": "grouping_symbol",
+          "data_set_field": "grouping_symbol"
+        }
+      ],
+      "field_order": [
+        "charset",
+        "delimiter",
+        "enclosure",
+        "field_count",
+        "skip_header_lines",
+        "skip_footer_lines",
+        "header_line_present",
+        "name",
+        "type",
+        "length",
+        "precision",
+        "mask",
+        "decimal_symbol",
+        "grouping_symbol"
+      ],
+      "data_set_name": "golden-file-metadata-embedded-enclosure",
+      "transform_name": "Verify"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0055-file-metadata-embedded-enclosure-verify UNIT",
+  "description": "",
+  "persist_filename": "",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0055-file-metadata-embedded-enclosure-verify.hpl",
+  "test_type": "UNIT_TEST"
+}
\ No newline at end of file
diff --git 
a/plugins/transforms/filemetadata/src/main/java/org/apache/hop/pipeline/transforms/filemetadata/util/delimiters/DelimiterDetector.java
 
b/plugins/transforms/filemetadata/src/main/java/org/apache/hop/pipeline/transforms/filemetadata/util/delimiters/DelimiterDetector.java
index 2c433c918c..9ec366e361 100644
--- 
a/plugins/transforms/filemetadata/src/main/java/org/apache/hop/pipeline/transforms/filemetadata/util/delimiters/DelimiterDetector.java
+++ 
b/plugins/transforms/filemetadata/src/main/java/org/apache/hop/pipeline/transforms/filemetadata/util/delimiters/DelimiterDetector.java
@@ -238,6 +238,8 @@ public class DelimiterDetector {
     boolean[] enclosureOpen = new boolean[potentialResults.size()];
     boolean[] enclosureSeen = new boolean[potentialResults.size()];
     boolean[] enclosureConsistent = new boolean[potentialResults.size()];
+    boolean[] fieldStart = new boolean[potentialResults.size()];
+    boolean[] skipNext = new boolean[potentialResults.size()];
 
     String s = "";
     try {
@@ -247,11 +249,14 @@ public class DelimiterDetector {
 
         int remainingResults = potentialResults.size();
 
-        // clear occurrences for each char
+        // clear the per-line state for each result
         for (int j = 0; j < remainingResults; j++) {
           frequencies[j] = 0;
           enclosureOpen[j] = false;
+          enclosureSeen[j] = false;
           enclosureConsistent[j] = true;
+          fieldStart[j] = true;
+          skipNext[j] = false;
         }
 
         // find occurrences for each char
@@ -262,37 +267,41 @@ public class DelimiterDetector {
 
             char c = detectionResult.getDelimiter();
             Character enc = detectionResult.getEnclosure();
-            boolean hasEnclosure = enc != null;
 
-            // if enclosure is involved, ignore enclosed delimiters
-            if (hasEnclosure) {
-
-              if (!enclosureOpen[j] && sc == c) {
+            if (enc == null) {
+              // no enclosure logic, just delimiters
+              if (sc == c) {
                 frequencies[j] += 1;
               }
-
+            } else if (skipNext[j]) {
+              // the second enclosure of an escaped pair
+              skipNext[j] = false;
+            } else if (enclosureOpen[j]) {
+              // enclosed delimiters are data. The enclosure only closes the 
field when followed
+              // by a delimiter or the end of the line, a doubled one is 
escaped. Any other
+              // enclosure in the field (BOB "ROBERT" SMITH) is data too.
               if (sc == enc) {
-                enclosureSeen[j] = true;
-                enclosureConsistent[j] =
-                    enclosureConsistent[j]
-                        && (i == 0 && !enclosureOpen[j]
-                            || i == s.length() - 1 && enclosureOpen[j]
-                            || i > 0 && s.charAt(i - 1) == c && 
!enclosureOpen[j]
-                            || i > 0 && s.charAt(i - 1) == enc && 
!enclosureOpen[j]
-                            || s.length() > i + 1 && s.charAt(i + 1) == c && 
enclosureOpen[j]
-                            || s.length() > i + 1 && s.charAt(i + 1) == enc && 
enclosureOpen[j]);
-
-                enclosureOpen[j] = !enclosureOpen[j];
+                boolean lastChar = i == s.length() - 1;
+                if (lastChar || s.charAt(i + 1) == c) {
+                  enclosureOpen[j] = false;
+                } else if (s.charAt(i + 1) == enc) {
+                  skipNext[j] = true;
+                }
               }
-
-            }
-            // no enclosure logic, just delimiters
-            else {
-
-              if (sc == c) {
-                frequencies[j] += 1;
+            } else if (sc == c) {
+              frequencies[j] += 1;
+              fieldStart[j] = true;
+              continue;
+            } else if (sc == enc) {
+              // an enclosure opens a field, anywhere else in an unenclosed 
field it is suspect
+              enclosureSeen[j] = true;
+              if (fieldStart[j]) {
+                enclosureOpen[j] = true;
+              } else {
+                enclosureConsistent[j] = false;
               }
             }
+            fieldStart[j] = false;
           }
         }
 
diff --git 
a/plugins/transforms/filemetadata/src/test/java/org/apache/hop/pipeline/transforms/filemetadata/DelimiterDetectorTest.java
 
b/plugins/transforms/filemetadata/src/test/java/org/apache/hop/pipeline/transforms/filemetadata/DelimiterDetectorTest.java
index 7ff84f798c..3c41a36462 100644
--- 
a/plugins/transforms/filemetadata/src/test/java/org/apache/hop/pipeline/transforms/filemetadata/DelimiterDetectorTest.java
+++ 
b/plugins/transforms/filemetadata/src/test/java/org/apache/hop/pipeline/transforms/filemetadata/DelimiterDetectorTest.java
@@ -24,8 +24,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.google.common.base.Charsets;
 import java.io.BufferedReader;
+import java.io.StringReader;
 import java.nio.file.Files;
 import java.nio.file.Paths;
+import java.util.Arrays;
 import 
org.apache.hop.pipeline.transforms.filemetadata.util.delimiters.DelimiterDetector;
 import 
org.apache.hop.pipeline.transforms.filemetadata.util.delimiters.DelimiterDetectorBuilder;
 import org.junit.jupiter.api.Test;
@@ -432,4 +434,106 @@ class DelimiterDetectorTest {
       assertEquals(2, result.getDataLineFrequency());
     }
   }
+
+  /**
+   * #5609: a quote inside an enclosed field (BOB "ROBERT" SMITH) is data. 
Once more than 10 such
+   * lines were scanned, the enclosure was dropped and every value kept its 
quotes.
+   */
+  @Test
+  void keepsEnclosureWithEnclosuresInsideFields() throws Exception {
+
+    try (BufferedReader f =
+        Files.newBufferedReader(
+            Paths.get(
+                getClass()
+                    .getResource(
+                        '/'
+                            + 
this.getClass().getPackage().getName().replace('.', '/')
+                            + "/delimited/embedded-enclosure.csv")
+                    .toURI()),
+            Charsets.UTF_8)) {
+      DelimiterDetector detector =
+          new DelimiterDetectorBuilder()
+              .withDelimiterCandidates('\t', ';', ',')
+              .withEnclosureCandidates('"', '\'')
+              .withInput(f)
+              .withRowLimit(10000)
+              .build();
+      DelimiterDetector.DetectionResult result = detector.detectDelimiters();
+      assertNotNull(result);
+      assertEquals(',', (char) result.getDelimiter());
+      assertEquals('"', (char) result.getEnclosure());
+      assertTrue(result.isConsistentEnclosure());
+      assertEquals(21, result.getDataLines());
+      assertEquals(0, result.getBadHeaders());
+      assertEquals(0, result.getBadFooters());
+      assertEquals(10, result.getDataLineFrequency());
+    }
+  }
+
+  /** Every such line used to be a streak of its own, a large file dropped the 
enclosure early. */
+  @Test
+  void keepsEnclosureWithManyEnclosuresInsideFields() throws Exception {
+    StringBuilder csv = new StringBuilder("\"id\",\"name\",\"amount\"\n");
+    for (int i = 0; i < 1000; i++) {
+      csv.append('"')
+          .append(i)
+          .append("\",\"BOB \"ROBERT\" SMITH\",\"")
+          .append(i)
+          .append(".25\"\n");
+    }
+
+    DelimiterDetector.DetectionResult result = detect(csv.toString(), ',', 
'"');
+
+    assertNotNull(result);
+    assertEquals('"', (char) result.getEnclosure());
+    assertEquals(1001, result.getDataLines());
+    assertEquals(0, result.getBadFooters());
+    assertEquals(2, result.getDataLineFrequency());
+  }
+
+  @Test
+  void keepsEnclosureWithEscapedEnclosures() throws Exception {
+    String csv =
+        "\"id\",\"name\",\"note\"\n"
+            + "\"1\",\"BOB \"\"ROBERT\"\" SMITH\",\"a, b\"\n"
+            + "\"2\",\"\"\"quoted\"\"\",\"\"\n"
+            + "\"3\",\"ends with \"\"\",\"x\"\n";
+
+    DelimiterDetector.DetectionResult result = detect(csv, ',', '"');
+
+    assertNotNull(result);
+    assertEquals('"', (char) result.getEnclosure());
+    assertTrue(result.isConsistentEnclosure());
+    assertEquals(4, result.getDataLines());
+    assertEquals(2, result.getDataLineFrequency());
+  }
+
+  /** Another delimiter candidate between the enclosures inside a field is 
data. */
+  @Test
+  void countsOnlyDelimitersOutsideEnclosedFields() throws Exception {
+    String csv = "\"id\",\"name\"\n" + "\"1\",\"a \"b; c\" d\"\n" + 
"\"2\",\"e\"\n";
+
+    DelimiterDetector.DetectionResult result = detect(csv, ';', ',', '"');
+
+    assertNotNull(result);
+    assertEquals(',', (char) result.getDelimiter());
+    assertEquals('"', (char) result.getEnclosure());
+    assertEquals(3, result.getDataLines());
+    assertEquals(1, result.getDataLineFrequency());
+  }
+
+  private static DelimiterDetector.DetectionResult detect(String csv, char... 
candidates)
+      throws Exception {
+    // the last candidate is the enclosure
+    char[] delimiters = Arrays.copyOf(candidates, candidates.length - 1);
+    try (BufferedReader reader = new BufferedReader(new StringReader(csv))) {
+      return new DelimiterDetectorBuilder()
+          .withDelimiterCandidates(delimiters)
+          .withEnclosureCandidates(candidates[candidates.length - 1])
+          .withInput(reader)
+          .build()
+          .detectDelimiters();
+    }
+  }
 }
diff --git 
a/plugins/transforms/filemetadata/src/test/java/org/apache/hop/pipeline/transforms/filemetadata/FileMetadataEmbeddedEnclosureTest.java
 
b/plugins/transforms/filemetadata/src/test/java/org/apache/hop/pipeline/transforms/filemetadata/FileMetadataEmbeddedEnclosureTest.java
new file mode 100644
index 0000000000..d2850e1750
--- /dev/null
+++ 
b/plugins/transforms/filemetadata/src/test/java/org/apache/hop/pipeline/transforms/filemetadata/FileMetadataEmbeddedEnclosureTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.pipeline.transforms.filemetadata;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.apache.hop.core.BlockingRowSet;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * #5609: a file with enclosures inside enclosed fields (BOB "ROBERT" SMITH) 
and amounts with two
+ * decimals. The enclosure was dropped once more than 15 rows were scanned, 
and the amounts were
+ * given a precision of 1.
+ */
+class FileMetadataEmbeddedEnclosureTest {
+
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  private static final int ENCLOSURE = 2;
+  private static final int FIELD_COUNT = 3;
+  private static final int SKIP_FOOTER_LINES = 5;
+  private static final int NAME = 7;
+  private static final int TYPE = 8;
+  private static final int LENGTH = 9;
+  private static final int PRECISION = 10;
+  private static final int MASK = 11;
+
+  private TransformMockHelper<FileMetadataMeta, FileMetadataData> mockHelper;
+
+  @BeforeEach
+  void setUp() {
+    mockHelper =
+        new TransformMockHelper<>("FileMetadata", FileMetadataMeta.class, 
FileMetadataData.class);
+    when(mockHelper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+        .thenReturn(mockHelper.iLogChannel);
+    when(mockHelper.pipeline.isRunning()).thenReturn(true);
+  }
+
+  @AfterEach
+  void tearDown() {
+    mockHelper.cleanUp();
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"15", "16", "10000", ""})
+  void detectsEnclosureAndDecimals(String limitRows) throws Exception {
+    List<Object[]> rows = detect(limitRows);
+
+    assertEquals(11, rows.size(), "one row per column");
+    Object[] first = rows.get(0);
+    assertEquals("\"", first[ENCLOSURE]);
+    assertEquals(11L, first[FIELD_COUNT]);
+    assertEquals(0L, first[SKIP_FOOTER_LINES]);
+
+    assertEquals("FY_DC", first[NAME]);
+    assertEquals("Integer", first[TYPE]);
+
+    Object[] name = rows.get(7);
+    assertEquals("LGL_NM_UP", name[NAME]);
+    assertEquals("String", name[TYPE]);
+    // BOB "ROBERT" SMITH is read as one value, enclosures inside included
+    assertEquals(18L, name[LENGTH]);
+
+    Object[] amount = rows.get(10);
+    assertEquals("SUM_PSTNG_AM", amount[NAME]);
+    assertEquals("Number", amount[TYPE]);
+    assertEquals(2L, amount[PRECISION]);
+    assertEquals("#.00", amount[MASK]);
+  }
+
+  private List<Object[]> detect(String limitRows) throws Exception {
+    FileMetadataMeta meta = new FileMetadataMeta();
+    meta.setDefault();
+    meta.setLimitRows(limitRows);
+    meta.setFileName(
+        getClass()
+            .getResource(
+                '/'
+                    + getClass().getPackage().getName().replace('.', '/')
+                    + "/delimited/embedded-enclosure.csv")
+            .toURI()
+            .toString());
+
+    when(mockHelper.transformMeta.getTransform()).thenReturn(meta);
+    FileMetadata transform =
+        new FileMetadata(
+            mockHelper.transformMeta,
+            meta,
+            new FileMetadataData(),
+            0,
+            mockHelper.pipelineMeta,
+            mockHelper.pipeline);
+    BlockingRowSet output = new BlockingRowSet(100);
+    transform.addRowSetToOutputRowSets(output);
+
+    assertFalse(transform.processRow(), "the transform generates its rows and 
is then done");
+
+    List<Object[]> rows = new ArrayList<>();
+    Object[] row;
+    while ((row = output.getRowWait(100, TimeUnit.MILLISECONDS)) != null) {
+      rows.add(row);
+    }
+    return rows;
+  }
+}
diff --git 
a/plugins/transforms/filemetadata/src/test/resources/org/apache/hop/pipeline/transforms/filemetadata/delimited/embedded-enclosure.csv
 
b/plugins/transforms/filemetadata/src/test/resources/org/apache/hop/pipeline/transforms/filemetadata/delimited/embedded-enclosure.csv
new file mode 100644
index 0000000000..e65316b88f
--- /dev/null
+++ 
b/plugins/transforms/filemetadata/src/test/resources/org/apache/hop/pipeline/transforms/filemetadata/delimited/embedded-enclosure.csv
@@ -0,0 +1,21 @@
+"FY_DC","DEPT_CD","FUND_CD","UNIT_CD","UNIT_NM","VEND_CUST_CD","EDU_VENDOR","LGL_NM_UP","OBJ_CD","OBJ_NM",SUM_PSTNG_AM
+"2023","99B","999","1111","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY one","9876","GRANTS FOR TEST1","123.58"
+"2023","99B","999","2222","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY two","8765","GRANTS FOR TEST2","90524.10"
+"2023","99B","999","3333","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY three","7654","GRANTS FOR TEST3","9872.52"
+"2023","99B","999","4444","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY four","6543","GRANTS FOR TEST4","130011.63"
+"2023","99B","999","5555","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY asides","5432","GRANTS FOR TEST5","20514.57"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","6666","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","BOB "ROBERT" SMITH","5433","GRANTS FOR BOB","123456.78"
+"2023","99B","999","1111","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY one","9876","GRANTS FOR TEST1","123.58"
+"2023","99B","999","2222","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY two","8765","GRANTS FOR TEST2","90524.10"
+"2023","99B","999","3333","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY three","7654","GRANTS FOR TEST3","9872.52"
+"2023","99B","999","4444","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY four","6543","GRANTS FOR TEST4","130011.63"
+"2023","99B","999","5555","TEST DATA TO BE ENTERED HERE","AB10000000001","Y 
","ENTITY asides","5432","GRANTS FOR TEST5","20514.57"
\ No newline at end of file
diff --git 
a/plugins/transforms/textfile/src/test/java/org/apache/hop/pipeline/transforms/fileinput/text/TextFileCsvImportScanTest.java
 
b/plugins/transforms/textfile/src/test/java/org/apache/hop/pipeline/transforms/fileinput/text/TextFileCsvImportScanTest.java
index 4dd95df5fb..da5f9bfad1 100644
--- 
a/plugins/transforms/textfile/src/test/java/org/apache/hop/pipeline/transforms/fileinput/text/TextFileCsvImportScanTest.java
+++ 
b/plugins/transforms/textfile/src/test/java/org/apache/hop/pipeline/transforms/fileinput/text/TextFileCsvImportScanTest.java
@@ -142,6 +142,31 @@ class TextFileCsvImportScanTest {
     }
   }
 
+  /** #5609: the amounts were suggested with the mask #.# and a precision of 
1. */
+  @Test
+  void suggestsAMaskKeepingAllDecimals() throws Exception {
+    Path file = Files.createTempFile("amounts", ".csv");
+    Files.writeString(file, "id;amount\n1;123.58\n2;90524.10\n3;9872.52\n", 
StandardCharsets.UTF_8);
+    TextFileInputMeta meta = newMeta("mixed", List.of("id", "amount"));
+    InputFile inputFile = new InputFile();
+    inputFile.setFileName(file.toAbsolutePath().toString());
+    inputFile.setFileRequired(true);
+    meta.getFileInput().getInputFiles().add(inputFile);
+    Variables variables = new Variables();
+    variables.initializeFrom(null);
+    try (InputStreamReader reader =
+        new InputStreamReader(Files.newInputStream(file), 
StandardCharsets.UTF_8)) {
+      new CountingDialog(variables, meta, reader, 100)
+          .doScan(new ProgressNullMonitorListener(), false);
+    }
+
+    TextFileInputField amount = field(meta, "amount");
+    assertEquals(IValueMeta.TYPE_NUMBER, amount.getType());
+    assertEquals("#.00", amount.getFormat());
+    assertEquals(2, amount.getPrecision());
+    assertEquals(".", amount.getDecimalSymbol());
+  }
+
   @Test
   void missingFileFailsOnceInsteadOfPerLine() throws Exception {
     TextFileInputMeta meta = newMeta("mixed");
@@ -176,6 +201,10 @@ class TextFileCsvImportScanTest {
   }
 
   private static TextFileInputMeta newMeta(String format) {
+    return newMeta(format, FIELD_NAMES);
+  }
+
+  private static TextFileInputMeta newMeta(String format, List<String> 
fieldNames) {
     TextFileInputMeta meta = new TextFileInputMeta();
     meta.getContent().setFileType("CSV");
     meta.getContent().setFileFormat(format);
@@ -185,7 +214,7 @@ class TextFileCsvImportScanTest {
     meta.getContent().setHeader(true);
     meta.getContent().setNrHeaderLines(1);
     meta.getContent().setNoEmptyLines(true);
-    for (String name : FIELD_NAMES) {
+    for (String name : fieldNames) {
       TextFileInputField field = new TextFileInputField();
       field.setName(name);
       meta.getInputFields().add(field);

Reply via email to