diff --git a/Signum.React/Scripts/Finder.tsx b/Signum.React/Scripts/Finder.tsx
index 2077792a88..24d88c4d4c 100644
--- a/Signum.React/Scripts/Finder.tsx
+++ b/Signum.React/Scripts/Finder.tsx
@@ -1,6 +1,5 @@
import * as React from "react";
import { DateTime } from 'luxon'
-import numbro from "numbro"
import * as AppContext from "./AppContext"
import * as Navigator from "./Navigator"
import { Dic, classes } from './Globals'
@@ -21,7 +20,7 @@ import { TypeEntity, QueryEntity } from './Signum.Entities.Basics';
import {
Type, IType, EntityKind, QueryKey, getQueryNiceName, getQueryKey, isQueryDefined, TypeReference,
- getTypeInfo, tryGetTypeInfos, getEnumInfo, toLuxonFormat, toNumbroFormat, PseudoType, EntityData,
+ getTypeInfo, tryGetTypeInfos, getEnumInfo, toLuxonFormat, toNumberFormat, PseudoType, EntityData,
TypeInfo, PropertyRoute, QueryTokenString, getTypeInfos, tryGetTypeInfo, onReloadTypesActions
} from './Reflection';
@@ -1642,16 +1641,16 @@ export const formatRules: FormatRule[] = [
name: "Number",
isApplicable: col => col.token!.filterType == "Integer" || col.token!.filterType == "Decimal",
formatter: col => {
- const numbroFormat = toNumbroFormat(col.token!.format);
- return new CellFormatter((cell: number | undefined) => cell == undefined ? "" : {numbro(cell).format(numbroFormat)}, "numeric-cell");
+ const numberFormat = toNumberFormat(col.token!.format);
+ return new CellFormatter((cell: number | undefined) => cell == undefined ? "" : {numberFormat.format(cell)}, "numeric-cell");
}
},
{
name: "Number with Unit",
isApplicable: col => (col.token!.filterType == "Integer" || col.token!.filterType == "Decimal") && !!col.token!.unit,
formatter: col => {
- const numbroFormat = toNumbroFormat(col.token!.format);
- return new CellFormatter((cell: number | undefined) => cell == undefined ? "" : {numbro(cell).format(numbroFormat) + "\u00a0" + col.token!.unit}, "numeric-cell");
+ const numberFormat = toNumberFormat(col.token!.format);
+ return new CellFormatter((cell: number | undefined) => cell == undefined ? "" : {numberFormat.format(cell) + "\u00a0" + col.token!.unit}, "numeric-cell");
}
},
{
diff --git a/Signum.React/Scripts/Lines/ValueLine.tsx b/Signum.React/Scripts/Lines/ValueLine.tsx
index ab570d08b4..de7b1d8047 100644
--- a/Signum.React/Scripts/Lines/ValueLine.tsx
+++ b/Signum.React/Scripts/Lines/ValueLine.tsx
@@ -1,9 +1,8 @@
import * as React from 'react'
import { DateTime } from 'luxon'
-import numbro from 'numbro'
import * as DateTimePicker from 'react-widgets/lib/DateTimePicker'
import { Dic, addClass, classes } from '../Globals'
-import { MemberInfo, getTypeInfo, TypeReference, toLuxonFormat, toDurationFormat, toNumbroFormat, isTypeEnum, durationToString, TypeInfo } from '../Reflection'
+import { MemberInfo, getTypeInfo, TypeReference, toLuxonFormat, toDurationFormat, toNumberFormat, isTypeEnum, durationToString, TypeInfo } from '../Reflection'
import { LineBaseController, LineBaseProps, useController } from '../Lines/LineBase'
import { FormGroup } from '../Lines/FormGroup'
import { FormControlReadonly } from '../Lines/FormControlReadonly'
@@ -12,6 +11,7 @@ import TextArea from '../Components/TextArea';
import 'react-widgets/dist/css/react-widgets.css';
import { KeyCodes } from '../Components/Basic';
import { format } from 'd3';
+import { isPrefix } from '../FindOptions'
export interface ValueLineProps extends LineBaseProps {
valueLineType?: ValueLineType;
@@ -455,14 +455,14 @@ ValueLineRenderers.renderers["Decimal" as ValueLineType] = (vl) => {
function numericTextBox(vl: ValueLineController, validateKey: (e: React.KeyboardEvent) => boolean) {
const s = vl.props
- const numbroFormat = toNumbroFormat(s.formatText);
+ const numberFormat = toNumberFormat(s.formatText);
if (s.ctx.readOnly)
return (
{vl.withItemGroup(
- {s.ctx.value == null ? "" : numbro(s.ctx.value).format(numbroFormat)}
+ {s.ctx.value == null ? "" : numberFormat.format(s.ctx.value)}
)}
);
@@ -498,7 +498,7 @@ function numericTextBox(vl: ValueLineController, validateKey: (e: React.Keyboard
onChange={handleOnChange}
formControlClass={classes(s.ctx.formControlClass, vl.mandatoryClass)}
validateKey={validateKey}
- format={numbroFormat}
+ format={numberFormat}
innerRef={vl.inputElement as React.RefObject}
/>
)}
@@ -510,7 +510,7 @@ export interface NumericTextBoxProps {
value: number | null;
onChange: (newValue: number | null) => void;
validateKey: (e: React.KeyboardEvent) => boolean;
- format?: string;
+ format: Intl.NumberFormat;
formControlClass?: string;
htmlAttributes?: React.HTMLAttributes;
innerRef?: ((ta: HTMLInputElement | null) => void) | React.RefObject;
@@ -522,7 +522,7 @@ export function NumericTextBox(p: NumericTextBoxProps) {
const value = text != undefined ? text :
- p.value != undefined ? numbro(p.value).format(p.format) :
+ p.value != undefined ? p.format?.format(p.value) :
"";
return 0) //Numbro transforms 1.000 to 1,0 in spanish or german
- value = value + ",00";
-
- if (p.format && p.format.endsWith("%")) {
- if (value && !value.endsWith("%"))
- value += "%";
- }
+ //if (numbro.languageData().delimiters.decimal == ',' && !value.contains(",") && value.trim().length > 0) //Numbro transforms 1.000 to 1,0 in spanish or german
+ // value = value + ",00";
- const result = value == undefined || value.length == 0 ? null : numbro.unformat(value, p.format);
+ const result = value == undefined || value.length == 0 ? null : unformat(p.format, value);
setText(undefined);
if (result != p.value)
p.onChange(result);
@@ -569,6 +564,29 @@ export function NumericTextBox(p: NumericTextBoxProps) {
p.htmlAttributes.onBlur(e);
}
+ function unformat(format: Intl.NumberFormat, str: string): number {
+ var isPercentage = format.resolvedOptions().style == "percent";
+ if (isPercentage) {
+ format = new Intl.NumberFormat(format.resolvedOptions().locale);
+ }
+
+ const thousandSeparator = format.format(1111).replace(/1/g, '');
+ const decimalSeparator = format.format(1.1).replace(/1/g, '');
+
+ if (thousandSeparator)
+ str = str.replace(new RegExp('\\' + thousandSeparator, 'g'), '');
+
+ if (decimalSeparator)
+ str = str.replace(new RegExp('\\' + decimalSeparator), '.');
+
+ var result = parseFloat(str);
+
+ if (isPercentage)
+ return result / 100;
+
+ return result;
+ }
+
function handleOnChange(e: React.SyntheticEvent) {
const input = e.currentTarget as HTMLInputElement;
setText(input.value);
diff --git a/Signum.React/Scripts/Reflection.ts b/Signum.React/Scripts/Reflection.ts
index d63a788b89..3244f830f5 100644
--- a/Signum.React/Scripts/Reflection.ts
+++ b/Signum.React/Scripts/Reflection.ts
@@ -1,5 +1,4 @@
import { DateTime} from 'luxon';
-import numbro from 'numbro';
import { Dic } from './Globals';
import { ModifiableEntity, Entity, Lite, MListElement, ModelState, MixinEntity } from './Signum.Entities'; //ONLY TYPES or Cyclic problems in Webpack!
import { ajaxGet } from './Services';
@@ -120,38 +119,80 @@ export function toDurationFormat(format: string | undefined): string | undefined
return format.replace("\\:", ":");
}
-export function toNumbroFormat(format: string | undefined) {
+export namespace NumberFormatSettings {
+ export let defaultNumberFormatLocale: string = null!;
+}
+
+//https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings
+export function toNumberFormat(format: string | undefined, locale?: string): Intl.NumberFormat {
+ return new Intl.NumberFormat(locale ?? NumberFormatSettings.defaultNumberFormatLocale, toNumberFormatOptions(format));
+}
+
+export function toNumberFormatOptions(format: string | undefined): Intl.NumberFormatOptions | undefined {
if (format == undefined)
return undefined;
const f = format.toUpperCase();
- if (f.startsWith("C"))
- return "0." + "0".repeat(parseInt(f.after("C") || "2"));
+ if (f.startsWith("C")) //unit comes separated
+ return {
+ style: "decimal",
+ minimumFractionDigits: parseInt(f.after("C")) || 2,
+ maximumFractionDigits: parseInt(f.after("C")) || 2,
+ useGrouping: true,
+ }
if (f.startsWith("N"))
- return "0,0." + "0".repeat(parseInt(f.after("N") || "2"));
+ return {
+ style: "decimal",
+ minimumFractionDigits: parseInt(f.after("N")) || 2,
+ maximumFractionDigits: parseInt(f.after("N")) || 2,
+ useGrouping: true,
+ }
if (f.startsWith("D"))
- return "0".repeat(parseInt(f.after("D") || "1"));
+ return {
+ style: "decimal",
+ maximumFractionDigits: 0,
+ minimumIntegerDigits: parseInt(f.after("D")) || 1,
+ useGrouping: false,
+ }
if (f.startsWith("F"))
- return "0." + "0".repeat(parseInt(f.after("F") || "2"));
+ return {
+ style: "decimal",
+ minimumFractionDigits: parseInt(f.after("F")) || 2,
+ maximumFractionDigits: parseInt(f.after("F")) || 2,
+ useGrouping: false,
+ }
if (f.startsWith("E"))
- return "0." + "0".repeat(parseInt(f.after("E") || "2"));
+ return {
+ style: "decimal",
+ notation: "scientific",
+ minimumFractionDigits: parseInt(f.after("E")) || 6,
+ maximumFractionDigits: parseInt(f.after("E")) || 6,
+ useGrouping: false,
+ } as any;
if (f.startsWith("P"))
- return "0." + "0".repeat(parseInt(f.after("P") || "2")) + "%";
+ return {
+ style: "percent",
+ minimumFractionDigits: parseInt(f.after("P")) || 2,
+ maximumFractionDigits: parseInt(f.after("P")) || 2,
+ useGrouping: false,
+ }
- if (f.contains("#"))
- format = format
- .replaceAll(".#", "[.]0")
- .replaceAll(",#", "[,]0")
- .replaceAll("#", "0");
- return format;
+ //simple euristic
+ var afterDot = f.tryAfter(".") ?? "";
+ return {
+ style: "decimal",
+ minimumFractionDigits: afterDot.trimStart("#").length,
+ maximumFractionDigits: afterDot.length,
+ useGrouping: f.contains(","),
+ }
}
export function valToString(val: any) {
@@ -165,7 +206,7 @@ export function numberToString(val: any, format?: string) {
if (val == null)
return "";
- return numbro(val).format(toNumbroFormat(format));
+ return toNumberFormat(format).format(val);
}
export function dateToString(val: any, format?: string) {
diff --git a/Signum.React/Scripts/SearchControl/FilterBuilder.tsx b/Signum.React/Scripts/SearchControl/FilterBuilder.tsx
index 4bf0e3249a..41afb8ad58 100644
--- a/Signum.React/Scripts/SearchControl/FilterBuilder.tsx
+++ b/Signum.React/Scripts/SearchControl/FilterBuilder.tsx
@@ -5,7 +5,7 @@ import { FilterOptionParsed, QueryDescription, QueryToken, SubTokensOptions, fil
import { SearchMessage } from '../Signum.Entities'
import { isNumber } from '../Lines/ValueLine'
import { ValueLine, EntityLine, EntityCombo, StyleContext, FormControlReadonly } from '../Lines'
-import { Binding, IsByAll, tryGetTypeInfos, toLuxonFormat, getTypeInfos } from '../Reflection'
+import { Binding, IsByAll, tryGetTypeInfos, toLuxonFormat, getTypeInfos, toNumberFormat } from '../Reflection'
import { TypeContext } from '../TypeContext'
import QueryTokenBuilder from './QueryTokenBuilder'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -582,8 +582,10 @@ export function PinnedFilterEditor(p: PinnedFilterEditorProps) {
if (p.readonly)
return {val};
+ var numberFormat = toNumberFormat("0");
+
return (
- { binding.setValue(n == null ? undefined : n); p.onChange(); }}
+ { binding.setValue(n == null ? undefined : n); p.onChange(); }}
validateKey={isNumber} formControlClass="form-control form-control-xs" htmlAttributes={{ placeholder: title, style: { width: "60px" } }} />
);
}
diff --git a/Signum.React/Scripts/SearchControl/PaginationSelector.tsx b/Signum.React/Scripts/SearchControl/PaginationSelector.tsx
index 7d4691a6e1..8dd5068e48 100644
--- a/Signum.React/Scripts/SearchControl/PaginationSelector.tsx
+++ b/Signum.React/Scripts/SearchControl/PaginationSelector.tsx
@@ -1,10 +1,10 @@
import * as React from 'react'
-import numbro from 'numbro'
import * as Finder from '../Finder'
import { classes } from '../Globals'
import { ResultTable, Pagination, PaginationMode, PaginateMath } from '../FindOptions'
import { SearchMessage } from '../Signum.Entities'
import "./PaginationSelector.css"
+import { toNumberFormat } from '../Reflection'
interface PaginationSelectorProps {
resultTable?: ResultTable;
@@ -32,32 +32,30 @@ export default function PaginationSelector(p: PaginationSelectorProps) {
const pagination = p.pagination;
- function format(num: number): string {
- return numbro(num).format("0,0");
- }
+ var numberFormat = toNumberFormat("0")
switch (pagination.mode) {
case "All":
return (
{SearchMessage._0Results_N.niceToString().forGenderAndNumber(resultTable.totalElements).formatHtml(
- {resultTable.totalElements && format(resultTable.totalElements)})
+ {resultTable.totalElements && numberFormat.format(resultTable.totalElements)})
}
);
case "Firsts":
return (
{SearchMessage.First0Results_N.niceToString().forGenderAndNumber(resultTable.rows.length).formatHtml(
- {format(resultTable.rows.length)})
+ {numberFormat.format(resultTable.rows.length)})
}
);
case "Paginate":
return (
{SearchMessage._01of2Results_N.niceToString().forGenderAndNumber(resultTable.totalElements).formatHtml(
- {format(PaginateMath.startElementIndex(pagination))},
- {format(PaginateMath.endElementIndex(pagination, resultTable.rows.length))},
- {resultTable.totalElements && format(resultTable.totalElements)})
+ {numberFormat.format(PaginateMath.startElementIndex(pagination))},
+ {numberFormat.format(PaginateMath.endElementIndex(pagination, resultTable.rows.length))},
+ {resultTable.totalElements && numberFormat.format(resultTable.totalElements)})
}
);
default:
diff --git a/Signum.React/Scripts/SearchControl/ValueSearchControl.tsx b/Signum.React/Scripts/SearchControl/ValueSearchControl.tsx
index 0c0f713cb3..cd8faaf22d 100644
--- a/Signum.React/Scripts/SearchControl/ValueSearchControl.tsx
+++ b/Signum.React/Scripts/SearchControl/ValueSearchControl.tsx
@@ -1,12 +1,11 @@
import * as React from 'react'
-import numbro from 'numbro'
import { DateTime } from 'luxon'
import { classes } from '../Globals'
import * as Navigator from '../Navigator'
import * as Finder from '../Finder'
import { FindOptions, FindOptionsParsed, SubTokensOptions, QueryToken, QueryValueRequest } from '../FindOptions'
import { Lite, Entity, getToString, EmbeddedEntity } from '../Signum.Entities'
-import { getQueryKey, toNumbroFormat, toLuxonFormat, getEnumInfo, QueryTokenString, getTypeInfo, getTypeName } from '../Reflection'
+import { getQueryKey, toNumberFormat, toLuxonFormat, getEnumInfo, QueryTokenString, getTypeInfo, getTypeName } from '../Reflection'
import { AbortableRequest } from "../Services";
import { SearchControlProps } from "./SearchControl";
import { BsColor } from '../Components';
@@ -282,8 +281,8 @@ export default class ValueSearchControl extends React.Component