Coerce custom CSV column values to number during import
Extends the helper introduced for boolean coercion so numeric values (`42`, `3.14`) become JSON numbers in `Contact.data` instead of strings. With the right JSON primitive in place, post-import inference via `jsonb_typeof()` classifies the field as `number`, the dashboard's segment-filter UI renders a numeric input, and comparison operators (`gt`, `lt`) become available. Use a strict integer-or-decimal regex rather than `Number()` to preserve string-shaped numerics that users intend as identifiers: leading-zero IDs (`"01234"`), zip codes, phone numbers, and signed or scientific-notation forms (`"+42"`, `"1e10"`, `".5"`, `"42."`) are left as strings. Boolean coercion still takes precedence, so `"1"` and `"0"` remain booleans for consistency with how `subscribed` is parsed. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
523bcad602
commit
51ac88b9f5
@@ -292,8 +292,40 @@ describe('coerceCustomValue', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('number coercion', () => {
|
||||
it.each([
|
||||
['42', 42],
|
||||
['-7', -7],
|
||||
['3.14', 3.14],
|
||||
[' 42 ', 42],
|
||||
['0.5', 0.5],
|
||||
['-0.25', -0.25],
|
||||
])('coerces %j to %j', (value, expected) => {
|
||||
expect(coerceCustomValue(value)).toBe(expected);
|
||||
});
|
||||
|
||||
it.each(['01234', '+42', '.5', '42.', '1e10', 'NaN', 'Infinity', '1.2.3', '4-2'])(
|
||||
'leaves %j as a string (preserves IDs / rejects loose formats)',
|
||||
value => {
|
||||
expect(coerceCustomValue(value)).toBe(value);
|
||||
},
|
||||
);
|
||||
|
||||
it('keeps boolean precedence: "1" stays boolean true, not number 1', () => {
|
||||
expect(coerceCustomValue('1')).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps boolean precedence: "0" stays boolean false, not number 0', () => {
|
||||
expect(coerceCustomValue('0')).toBe(false);
|
||||
});
|
||||
|
||||
it('"1.0" is a number (does not match the boolean truthy set)', () => {
|
||||
expect(coerceCustomValue('1.0')).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('passthrough', () => {
|
||||
it.each(['Alice', 'true!', 'yesno', 'maybe', '42', '3.14', '01234'])('leaves %j as a string', value => {
|
||||
it.each(['Alice', 'true!', 'yesno', 'maybe'])('leaves %j as a string', value => {
|
||||
expect(coerceCustomValue(value)).toBe(value);
|
||||
});
|
||||
|
||||
|
||||
@@ -223,16 +223,22 @@ function isValidEmail(email: string): boolean {
|
||||
|
||||
const BOOLEAN_TRUE = new Set(['true', '1', 'yes']);
|
||||
const BOOLEAN_FALSE = new Set(['false', '0', 'no']);
|
||||
// Strict integer-or-decimal pattern. Rejects leading zeros (preserves IDs,
|
||||
// zips, phone numbers), scientific notation, `+` prefix, and `.5` / `42.`.
|
||||
const NUMERIC_RE = /^-?(0|[1-9]\d*)(\.\d+)?$/;
|
||||
|
||||
/**
|
||||
* Coerce a raw CSV cell to its natural JSON primitive so post-import type
|
||||
* inference (ContactService.getAvailableFields) can detect booleans on custom
|
||||
* fields the same way it already does for the reserved `subscribed` column.
|
||||
* Values outside the recognised keyword set are returned unchanged.
|
||||
* inference (ContactService.getAvailableFields) can detect booleans and
|
||||
* numbers on custom fields the same way it already does for the reserved
|
||||
* `subscribed` column. Values that match neither recogniser are returned
|
||||
* unchanged.
|
||||
*/
|
||||
export function coerceCustomValue(value: string): string | boolean {
|
||||
const lower = value.trim().toLowerCase();
|
||||
export function coerceCustomValue(value: string): string | boolean | number {
|
||||
const trimmed = value.trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (BOOLEAN_TRUE.has(lower)) return true;
|
||||
if (BOOLEAN_FALSE.has(lower)) return false;
|
||||
if (NUMERIC_RE.test(trimmed)) return Number(trimmed);
|
||||
return value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user