Parsing numbers and currencies from CSV files in Laravel | Tapix                  [ ![Tapix](/img/tapix-logo-light.svg) ![Tapix](/img/tapix-logo-dark.svg) ](https://tapix.dev) [Features](https://tapix.dev#features) [Pricing](https://tapix.dev#pricing) [Docs](https://docs.tapix.dev) [Blog](https://tapix.dev/blog)

   Try Demo  [ Get Tapix from $99](https://tapix.dev#pricing)

  [Features](https://tapix.dev#features) [Pricing](https://tapix.dev#pricing) [Docs](https://docs.tapix.dev) [Blog](https://tapix.dev/blog)   Try Demo  [ Get Tapix from $99](https://tapix.dev#pricing)

   ![Tapix](https://tapix.dev/img/tapix-logo-light.svg)

 TutorialsParsing numbers and currencies from CSV files in Laravel
========================================================

 tapix.dev/blog

  [    Back to blog ](https://tapix.dev/blog) [ Tutorials ](https://tapix.dev/blog/category/tutorials)

Parsing numbers and currencies from CSV files in Laravel
========================================================

 Manch Minasyan ·  May 22, 2026  · 9 min read

 Open a CSV file exported from a German accounting system and look at the salary column. You will see values like `1.234,56`. Open the same export from a US payroll tool and you will see `1,234.56`. The digits are identical. The separators are reversed. If you parse both the same way, one of them silently becomes a number roughly a thousand times too large or too small.

This is not a niche problem. It is the default state of international CSV data. Any application that accepts file uploads from users across locales -- or even from different departments within the same company using different export tools -- will encounter both formats in production. The comma that means "thousands separator" in New York means "decimal point" in Berlin.

To parse numeric and currency values correctly, you need to handle format detection, currency symbol stripping, normalization to PHP floats, and the edge cases that corrupt data silently. [The hidden cost of building your own CSV importer](/blog/hidden-cost-building-csv-importer) names number and date format localization as one of the iceberg layers most developers underestimate -- this post covers it in full.

[\#](#the-number-format-problem "Permalink")The number format problem
---------------------------------------------------------------------

The core issue is that CSV is a text format with no type metadata. When your application reads a cell, it gets a string. The string `1,234` could mean:

- **One thousand two hundred thirty-four** (US format, comma is thousands separator)
- **One point two three four** (European format, comma is decimal separator)

There is no way to distinguish these by looking at the value alone. The same ambiguity exists with periods: `1.234` is either "one point two three four" or "one thousand two hundred thirty-four" depending on locale.

This ambiguity is why naive approaches like `(float) $value` or `floatval()` produce silently wrong results. PHP's type casting treats commas as string terminators, not separators:

```
(float) '1,234.56';  // 1.0 -- silently wrong
(float) '1.234,56';  // 1.234 -- silently wrong
(float) '1234.56';   // 1234.56 -- correct, but only because there's no ambiguity

```

The first two lines do not throw errors. They do not return null. They return numbers that look plausible enough to slip past a quick glance at the database, only to surface weeks later when financial reports do not add up.

The solution is explicit format declaration: ask the user which decimal convention their file uses, then parse accordingly.

[\#](#stripping-currency-symbols "Permalink")Stripping currency symbols
-----------------------------------------------------------------------

Before you can parse a number, you need to strip the non-numeric noise that surrounds it. Real-world CSV currency columns contain values like:

- `$1,234.56`
- `EUR 1.234,56`
- `1 234,56 EUR`
- `£99.99`
- `$-500.00`

The regex needs to handle symbols at the start or end of the string, with optional whitespace between the symbol and the digits. Here is the pattern Tapix uses internally:

```
// Strip currency symbols ($ € £ ¥) from start/end
$value = preg_replace('/^[$\x{20AC}\x{00A3}\x{00A5}]\s*|\s*[$\x{20AC}\x{00A3}\x{00A5}]$/u', '', $value);

```

The `u` flag enables Unicode matching, which is necessary because `EUR`, `GBP`, and `JPY` symbols are multi-byte characters. The pattern anchors to the start (`^`) and end (`$`) of the string to avoid stripping dollar signs that might appear mid-string in free-text fields (unlikely in a currency column, but defensive parsing matters).

A simpler version that covers the four most common symbols (`$`, `EUR`, `GBP`, `JPY`):

```
$value = preg_replace('/^[$€£¥]\s*|\s*[$€£¥]$/u', '', $value);

```

This handles the vast majority of real-world CSV exports. If your application processes currencies beyond these four, extend the character class. But do not try to build a comprehensive currency symbol stripper -- there are over 100 active currency symbols worldwide, and many of them overlap with letters used in other contexts.

[\#](#point-vs-comma-two-parsing-modes "Permalink")POINT vs COMMA: two parsing modes
------------------------------------------------------------------------------------

Once currency symbols are gone, the remaining problem is separators. Tapix defines two parsing modes as a PHP enum:

```
enum NumberFormat: string
{
    case POINT = 'point';  // 1,000.00 -- US/UK convention
    case COMMA = 'comma';  // 1.000,00 -- EU convention

    public function parse(string $value): ?float
    {
        $value = trim($value);

        if ($value === '') {
            return null;
        }

        $decimalSeparator = match ($this) {
            self::POINT => '.',
            self::COMMA => ',',
        };

        $otherSeparator = match ($this) {
            self::POINT => ',',
            self::COMMA => '.',
        };

        // Strip whitespace (French format uses space as thousands separator)
        $value = str_replace(' ', '', $value);

        // Strip currency symbols
        $value = preg_replace('/^[$€£¥]\s*|\s*[$€£¥]$/u', '', $value);

        // Remove thousands separator
        $value = str_replace($otherSeparator, '', $value);

        // Normalize decimal separator to period for PHP
        if ($decimalSeparator === ',') {
            $value = str_replace(',', '.', $value);
        }

        if (! is_numeric($value)) {
            return null;
        }

        return (float) $value;
    }
}

```

The algorithm is straightforward:

1. Strip whitespace (handles French-style `1 234,56` with space as thousands separator).
2. Strip currency symbols from edges.
3. Remove the "other" separator entirely -- if the decimal is a period, commas are thousands separators, so delete them. If the decimal is a comma, periods are thousands separators, so delete them.
4. Normalize: if the decimal separator is a comma, replace it with a period so PHP can parse the result.
5. Validate: if the cleaned string is not numeric, return null instead of guessing.

Step 5 is critical. Returning null for unparseable values is always safer than returning 0 or throwing an exception mid-import. The null propagates through to validation, where the user can see and correct the value.

Here is how the two modes handle the same input string:

InputPOINT modeCOMMA mode`1,234.56``1234.56``1234.56` (wrong -- treats `.56` as thousands)`1.234,56``1234.56` (wrong -- treats `,56` as thousands)`1234.56``$1,000``1000.0``1.0` (wrong)`€1.000``1.0` (wrong)`1000.0`The table makes the stakes clear. Parsing with the wrong mode does not fail -- it succeeds with the wrong number. This is why the format must be selected explicitly, not auto-detected. Tapix presents the user with a format selector in the mapping step, showing examples of each convention so they can match it to their file. [CSV column mapping UX patterns that reduce support tickets](/blog/csv-column-mapping-ux-patterns) covers how this selector fits into the broader mapping step design.

[\#](#empty-string-handling "Permalink")Empty string handling
-------------------------------------------------------------

CSV files do not have a concept of null. An empty cell is serialized as an empty string (`""`), a pair of quotes with nothing between them. This creates a real problem for typed database columns.

PostgreSQL rejects empty strings for numeric columns with a type error. MySQL silently coerces empty strings to 0. SQLite accepts whatever you throw at it. If your application runs on PostgreSQL (or if you want consistent behavior across databases), empty strings must be normalized to null before database insertion.

The `castValue` method in Tapix handles this at the column data level:

```
public function castValue(mixed $value): mixed
{
    if ($value === null) {
        return null;
    }

    if ($this->getType()->isNumeric() && is_string($value)) {
        $format = $this->numberFormat ?? NumberFormat::POINT;

        return $format->parse($value);
    }

    return $value;
}

```

Notice the delegation chain: `castValue` checks if the field is numeric (which covers both `Number` and `Currency` types via the `FieldType` enum), then delegates to `NumberFormat::parse()`. The `parse()` method already returns null for empty strings (the `$value === ''` check on line 3 of the parse method). So the empty-string-to-null normalization happens naturally through the parsing pipeline without any special-case code.

This matters because the alternative -- adding `if ($value === '') return null;` checks scattered throughout the import pipeline -- is fragile and easy to miss in one code path while catching it in another.

[\#](#float-precision "Permalink")Float precision
-------------------------------------------------

PHP floats are IEEE 754 double-precision numbers. They have 52 bits of mantissa, which gives you about 15-17 significant decimal digits of precision. For most CSV import scenarios, this is more than enough. But there are edge cases worth understanding.

The classic example: `(float) '1234.56'` might internally represent as `1234.5599999999999` due to binary floating-point representation. You will never see this in a `var_dump` or database column because PHP and most databases round the display. But if you compare floats for equality, or if you chain arithmetic operations on parsed values, the imprecision can accumulate.

When this matters in practice:

- **Financial calculations**: If parsed values feed into invoice totals, tax calculations, or balance reconciliation, use `bcmath` for arithmetic after parsing. Parse to float for storage, then use `bcadd()`, `bcsub()`, `bcmul()` for derived calculations.
- **Display formatting**: If you round-trip a float back to a formatted string for user review, use `round($parsed, 2)` to avoid displaying trailing noise digits.
- **Database storage**: If the target column is `DECIMAL(10,2)`, the database handles precision. MySQL and PostgreSQL both round floats to the column's declared scale on insert.

A pragmatic rule: parse to float, store in a `DECIMAL` column, use `bcmath` only for derived calculations. This covers 99% of real-world scenarios without over-engineering the parsing layer.

[\#](#putting-it-together "Permalink")Putting it together
---------------------------------------------------------

The full parsing flow for a numeric CSV value looks like this:

1. The user uploads a CSV and maps columns in the wizard UI.
2. For columns mapped to `Number` or `Currency` field types, the mapping step shows a format selector (POINT or COMMA) with examples.
3. The selected format is stored on the `ColumnData` object as a `NumberFormat` enum value.
4. During validation and execution, `castValue()` delegates to `NumberFormat::parse()`, which strips symbols, removes thousands separators, normalizes the decimal, and returns a float or null.
5. Null values (from empty cells or unparseable strings) propagate to validation, where the user can correct or skip the row.
6. Valid floats are inserted into the database, where `DECIMAL` column types handle final precision.

No silent data corruption. No exceptions mid-import. No special-case code for different locales. The entire complexity is encapsulated in a single enum with a 20-line parse method.

[\#](#further-reading "Permalink")Further reading
-------------------------------------------------

If you are building CSV imports in Laravel, these related posts cover the broader pipeline:

- [The complete guide to CSV imports in Laravel](/blog/complete-guide-csv-imports-laravel) covers the full landscape of import approaches, from raw `fgetcsv` to dedicated packages.
- [Handling CSV validation errors before they hit your database](/blog/handling-csv-validation-errors) explains the validate-and-correct pattern that lets users fix errors inline instead of re-uploading.
- [Auto-detecting CSV column types in Laravel](/blog/auto-detecting-csv-column-types) covers how to infer field types from CSV headers and sample data.

If you want a CSV import wizard that handles number format parsing, currency stripping, validation, and all the edge cases described here out of the box, [Tapix](/) ships all of this as a drop-in Laravel package.

 ### Enjoyed this post?

Get notified when we publish new articles about Laravel imports and data handling.

  Email address   Subscribe

Almost there — confirm your subscription via email.

 Related posts
-------------

 [  Tutorials   May 19, 2026

 Multi-tenant CSV imports in Laravel
-------------------------------------

Tenant context disappears in queue jobs. Here's how to preserve it across the entire import pipeline -- from upload to entity resolution.

 ](https://tapix.dev/blog/multi-tenant-csv-imports-laravel) [  Tutorials   May 12, 2026

 Auto-detecting CSV column types in Laravel
--------------------------------------------

From emails to currencies, dates to booleans -- how to infer column types from CSV sample values and configure format-specific parsing.

 ](https://tapix.dev/blog/auto-detecting-csv-column-types) [  Tutorials   May 1, 2026

 Queue-powered imports: processing 100K rows in Laravel
--------------------------------------------------------

Direct CSV processing breaks at scale. Here's how to use Laravel queues with chunked batches, unique jobs, and progress tracking for large imports.

 ](https://tapix.dev/blog/queue-powered-imports-100k-rows)

   [ ![Tapix](/img/tapix-logo-light.svg) ![Tapix](/img/tapix-logo-dark.svg) ](https://tapix.dev)CSV and Excel import wizard for Laravel.

  Product [Pricing](https://tapix.dev#pricing) [Docs](https://docs.tapix.dev) [Blog](https://tapix.dev/blog) [Contact](mailto:hello@tapix.dev)

 Compare [vs Laravel Excel](https://tapix.dev/vs/laravel-excel) [vs Filament Import](https://tapix.dev/vs/filament-import)

 Legal [Privacy](https://tapix.dev/privacy-policy) [Terms](https://tapix.dev/terms-of-service)

© 2026 Tapix. All rights reserved.
