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