Skip to content

Commit c0ee030

Browse files
authored
Merge pull request #18 from PADAS/17-localized-decimal-separator
Normalize decimal separators
2 parents 1579dc2 + 628221a commit c0ee030

File tree

5 files changed

+226
-1
lines changed

5 files changed

+226
-1
lines changed

common/mockData/formatterMockData.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,4 +884,81 @@ export const JSON_SCHEMA_DEFAULT_VALUES = '{\n' +
884884
' "type": "textarea"\n' +
885885
' }\n' +
886886
' ]\n' +
887+
'}';
888+
889+
export const JSON_SCHEMA_COMMA_DECIMAL_NUMBERS = '{\n' +
890+
' "schema": {\n' +
891+
' "type": "object",\n' +
892+
' "properties": {\n' +
893+
' "price_with_comma": {\n' +
894+
' "type": "number",\n' +
895+
' "title": "Price with comma decimal",\n' +
896+
' "default": "12,99",\n' +
897+
' "minimum": "5,50",\n' +
898+
' "maximum": "100,00"\n' +
899+
' },\n' +
900+
' "negative_number": {\n' +
901+
' "type": "number",\n' +
902+
' "title": "Negative number with comma",\n' +
903+
' "default": "-15,75"\n' +
904+
' },\n' +
905+
' "regular_number": {\n' +
906+
' "type": "number",\n' +
907+
' "title": "Regular number with period",\n' +
908+
' "default": 25.50,\n' +
909+
' "minimum": 0.01\n' +
910+
' },\n' +
911+
' "string_field": {\n' +
912+
' "type": "string",\n' +
913+
' "title": "String field (should not be affected)",\n' +
914+
' "default": "12,99 text"\n' +
915+
' }\n' +
916+
' }\n' +
917+
' },\n' +
918+
' "definition": [\n' +
919+
' "price_with_comma",\n' +
920+
' "negative_number",\n' +
921+
' "regular_number",\n' +
922+
' "string_field"\n' +
923+
' ]\n' +
924+
'}';
925+
926+
export const JSON_SCHEMA_EDGE_CASE_NUMBERS = '{\n' +
927+
' "schema": {\n' +
928+
' "type": "object",\n' +
929+
' "properties": {\n' +
930+
' "thousands_separator": {\n' +
931+
' "type": "number",\n' +
932+
' "title": "Number with thousands separator (should not convert)",\n' +
933+
' "default": "1.234,56"\n' +
934+
' },\n' +
935+
' "multiple_commas": {\n' +
936+
' "type": "number",\n' +
937+
' "title": "Multiple commas (should not convert)",\n' +
938+
' "default": "1,234,567"\n' +
939+
' },\n' +
940+
' "empty_default": {\n' +
941+
' "type": "number",\n' +
942+
' "title": "Empty default",\n' +
943+
' "default": ""\n' +
944+
' },\n' +
945+
' "null_default": {\n' +
946+
' "type": "number",\n' +
947+
' "title": "Null default",\n' +
948+
' "default": null\n' +
949+
' },\n' +
950+
' "zero_comma": {\n' +
951+
' "type": "number",\n' +
952+
' "title": "Zero with comma",\n' +
953+
' "default": "0,00"\n' +
954+
' }\n' +
955+
' }\n' +
956+
' },\n' +
957+
' "definition": [\n' +
958+
' "thousands_separator",\n' +
959+
' "multiple_commas",\n' +
960+
' "empty_default",\n' +
961+
' "null_default",\n' +
962+
' "zero_comma"\n' +
963+
' ]\n' +
887964
'}'

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@earthranger/react-native-jsonforms-formatter",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"type": "module",
55
"description": "Converts JTD into JSON Schema ",
66
"main": "./dist/bundle.js",

src/utils/utils.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,44 @@ export const isSchemaFieldSet = (definition: any[]) => {
9393

9494
export const isString = (item: any) => typeof item === STRING_TYPE;
9595

96+
export const normalizeDecimalSeparators = (value: string | number): string | number => {
97+
// If it's already a number, return as-is
98+
if (typeof value === 'number') {
99+
return value;
100+
}
101+
102+
// If it's not a string, return as-is
103+
if (typeof value !== 'string') {
104+
return value;
105+
}
106+
107+
// Trim whitespace
108+
const trimmedValue = value.trim();
109+
110+
// If empty string, return as-is
111+
if (trimmedValue === '') {
112+
return value;
113+
}
114+
115+
// Check if it looks like a number with comma decimal separator
116+
// Pattern: optional minus, digits, comma, digits (e.g., "12,34", "-5,678")
117+
const commaDecimalPattern = /^-?\d+,\d+$/;
118+
119+
if (commaDecimalPattern.test(trimmedValue)) {
120+
// Replace comma with period for decimal separator
121+
const normalizedValue = trimmedValue.replace(',', '.');
122+
123+
// Validate that the result is a valid number
124+
const numValue = parseFloat(normalizedValue);
125+
if (!isNaN(numValue)) {
126+
return normalizedValue;
127+
}
128+
}
129+
130+
// Return original value if no conversion needed or possible
131+
return value;
132+
};
133+
96134
// Helper function to recursively traverse and process the schema
97135
export const traverseSchema = (
98136
schema: any,

src/validateJsonSchema.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
isRequiredProperty,
1717
isSchemaFieldSet,
1818
isString,
19+
normalizeDecimalSeparators,
1920
REQUIRED_PROPERTY,
2021
traverseSchema,
2122
} from './utils/utils';
@@ -226,6 +227,33 @@ const validateDefinition = (validations: any, item: any, schema: any, parentItem
226227
}
227228
};
228229

230+
const normalizeNumberFields = (schema: any) => {
231+
traverseSchema(schema, (node) => {
232+
if (node.type === 'object' && node.properties) {
233+
Object.entries(node.properties).forEach(([, property]: [string, any]) => {
234+
if (property.type === 'number') {
235+
// Normalize default values
236+
if (property.default !== undefined) {
237+
property.default = normalizeDecimalSeparators(property.default);
238+
}
239+
240+
// Normalize minimum values
241+
if (property.minimum !== undefined) {
242+
property.minimum = normalizeDecimalSeparators(property.minimum);
243+
}
244+
245+
// Normalize maximum values
246+
if (property.maximum !== undefined) {
247+
property.maximum = normalizeDecimalSeparators(property.maximum);
248+
}
249+
}
250+
});
251+
}
252+
});
253+
254+
return schema;
255+
};
256+
229257
const validateSchema = (validations: any, schema: any) => {
230258
const { hasInactiveChoices, hasEnums } = validations;
231259
if (hasInactiveChoices) {
@@ -272,6 +300,9 @@ export const validateJSONSchema = (stringSchema: string) => {
272300

273301
const schemaValidations = getSchemaValidations(stringSchema);
274302

303+
// Normalize decimal separators in number fields
304+
normalizeNumberFields(schema.schema);
305+
275306
validateSchema(schemaValidations, schema);
276307

277308
// JSON forms library does not support JSON Type Definition JTD

test/JsonFormatter.test.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import {
33
COLLECTION_FIELD_HEADER_FAKE_DATA,
44
FIELD_SET_HEADER_FAKE_DATA,
55
JSON_SCHEMA_COLLECTION_FIELD_FAKE_DATA,
6+
JSON_SCHEMA_COMMA_DECIMAL_NUMBERS,
67
JSON_SCHEMA_DATE_TIME_FIELD_SETS,
78
JSON_SCHEMA_DEFAULT_VALUES,
89
JSON_SCHEMA_DUPLICATED_CHOICES_SINGLE_SELECT_FAKE_DATA,
910
JSON_SCHEMA_DUPLICATED_REQUIRED_PROPERTIES_FAKE_DATA,
11+
JSON_SCHEMA_EDGE_CASE_NUMBERS,
1012
JSON_SCHEMA_EMPTY_CHOICES_FAKE_DATA,
1113
JSON_SCHEMA_FIELD_SETS_FAKE_DATA,
1214
JSON_SCHEMA_ID_$SCHEMA_FAKE_DATA,
@@ -26,6 +28,7 @@ import expectedUISchema from "../common/mockData/uiSchemaExpectedMock.json";
2628
import expectedFieldSetUISchema from "../common/mockData/uiSchemaFielSetExpectedMock.json";
2729
import { generateUISchema } from "../src/generateUISchema";
2830
import { validateJSONSchema } from "../src/validateJsonSchema";
31+
import { normalizeDecimalSeparators } from "../src/utils/utils";
2932

3033
describe("JSON Schema validation", () => {
3134
it("Special chars should throw an exception", () => {
@@ -170,6 +173,82 @@ describe("JSON Schema validation", () => {
170173
const validSchema = validateJSONSchema(JSON.stringify(jsonSchema));
171174
expect(validSchema).toMatchObject(expectedSchema);
172175
});
176+
177+
it("Validate comma decimal separators are converted to periods", () => {
178+
const validSchema = validateJSONSchema(JSON_SCHEMA_COMMA_DECIMAL_NUMBERS);
179+
180+
// Check that comma decimals are converted to periods
181+
expect(validSchema.schema.properties.price_with_comma.default).toBe("12.99");
182+
expect(validSchema.schema.properties.price_with_comma.minimum).toBe("5.50");
183+
expect(validSchema.schema.properties.price_with_comma.maximum).toBe("100.00");
184+
185+
// Check negative numbers
186+
expect(validSchema.schema.properties.negative_number.default).toBe("-15.75");
187+
188+
// Check that regular numbers are unchanged
189+
expect(validSchema.schema.properties.regular_number.default).toBe(25.50);
190+
expect(validSchema.schema.properties.regular_number.minimum).toBe(0.01);
191+
192+
// Check that string fields are not affected
193+
expect(validSchema.schema.properties.string_field.default).toBe("12,99 text");
194+
});
195+
196+
it("Validates edge cases for decimal separator conversion", () => {
197+
const validSchema = validateJSONSchema(JSON_SCHEMA_EDGE_CASE_NUMBERS);
198+
199+
// Should NOT convert numbers with thousands separators (European format: 1.234,56)
200+
expect(validSchema.schema.properties.thousands_separator.default).toBe("1.234,56");
201+
202+
// Should NOT convert numbers with multiple commas (thousands: 1,234,567)
203+
expect(validSchema.schema.properties.multiple_commas.default).toBe("1,234,567");
204+
205+
// Should leave empty defaults unchanged
206+
expect(validSchema.schema.properties.empty_default.default).toBe("");
207+
208+
// Should leave null defaults unchanged
209+
expect(validSchema.schema.properties.null_default.default).toBe(null);
210+
211+
// Should convert simple comma decimal (0,00 -> 0.00)
212+
expect(validSchema.schema.properties.zero_comma.default).toBe("0.00");
213+
});
214+
});
215+
216+
describe("Decimal separator normalization", () => {
217+
it("Converts comma decimal separators to periods", () => {
218+
expect(normalizeDecimalSeparators("12,99")).toBe("12.99");
219+
expect(normalizeDecimalSeparators("-15,75")).toBe("-15.75");
220+
expect(normalizeDecimalSeparators("0,01")).toBe("0.01");
221+
expect(normalizeDecimalSeparators("1000,50")).toBe("1000.50");
222+
});
223+
224+
it("Leaves period decimal separators unchanged", () => {
225+
expect(normalizeDecimalSeparators("12.99")).toBe("12.99");
226+
expect(normalizeDecimalSeparators("-15.75")).toBe("-15.75");
227+
expect(normalizeDecimalSeparators("0.01")).toBe("0.01");
228+
});
229+
230+
it("Leaves numbers unchanged", () => {
231+
expect(normalizeDecimalSeparators(12.99)).toBe(12.99);
232+
expect(normalizeDecimalSeparators(-15.75)).toBe(-15.75);
233+
expect(normalizeDecimalSeparators(0)).toBe(0);
234+
});
235+
236+
it("Does not affect strings that are not decimal numbers", () => {
237+
expect(normalizeDecimalSeparators("12,99 text")).toBe("12,99 text");
238+
expect(normalizeDecimalSeparators("text 12,99")).toBe("text 12,99");
239+
expect(normalizeDecimalSeparators("12,99,00")).toBe("12,99,00"); // Multiple commas
240+
expect(normalizeDecimalSeparators("abc")).toBe("abc");
241+
expect(normalizeDecimalSeparators("")).toBe("");
242+
expect(normalizeDecimalSeparators(" ")).toBe(" ");
243+
});
244+
245+
it("Handles edge cases properly", () => {
246+
expect(normalizeDecimalSeparators(null)).toBe(null);
247+
expect(normalizeDecimalSeparators(undefined)).toBe(undefined);
248+
expect(normalizeDecimalSeparators(" 12,99 ")).toBe("12.99"); // Trims whitespace
249+
expect(normalizeDecimalSeparators("12,")).toBe("12,"); // No digits after comma
250+
expect(normalizeDecimalSeparators(",99")).toBe(",99"); // No digits before comma
251+
});
173252
});
174253

175254
describe("JSON UI Schema generation", () => {

0 commit comments

Comments
 (0)