1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-07 14:25:37 +00:00

Code cleanup for advanced number formatting

This commit is contained in:
Przemyslaw Kukulski 2025-01-10 15:32:38 +00:00
parent 731cc55956
commit f26595c060
10 changed files with 227 additions and 35 deletions
backend/src/baserow/contrib/database
changelog/entries/unreleased/refactor
web-frontend
modules/database
test/unit/database/mixins

View file

@ -247,11 +247,3 @@ EXCLUDE_FIELDS_API_PARAM = OpenApiParameter(
"response. "
),
)
NUMBER_SEPARATOR_MAPPING = {
"": ("", "."),
"SPACE_COMMA": (" ", ","),
"SPACE_PERIOD": (" ", "."),
"COMMA_PERIOD": (",", "."),
"PERIOD_COMMA": (".", ","),
}

View file

@ -7,13 +7,17 @@ from django.db.models import QuerySet
from rest_framework import serializers
from baserow.config.settings.utils import str_to_bool
from baserow.contrib.database.api.constants import NUMBER_SEPARATOR_MAPPING
from baserow.contrib.database.api.rows.exceptions import InvalidJoinParameterException
from baserow.contrib.database.fields.exceptions import (
FieldDoesNotExist,
IncompatibleField,
)
from baserow.contrib.database.fields.models import Field
from baserow.contrib.database.fields.models import (
DEFAULT_DECIMAL_SEPARATOR,
DEFAULT_THOUSAND_SEPARATOR,
NUMBER_SEPARATORS,
Field,
)
from baserow.contrib.database.fields.utils import get_field_id_from_field_key
from baserow.core.db import specific_iterator
from baserow.core.utils import split_comma_separated_string
@ -294,4 +298,10 @@ def extract_link_row_joins_from_request(
def get_thousand_and_decimal_separator(value):
return NUMBER_SEPARATOR_MAPPING.get(value, None) or NUMBER_SEPARATOR_MAPPING[""]
thousand_sep, decimal_sep = NUMBER_SEPARATORS.get(value, {}).get("separators", None)
if not thousand_sep or not decimal_sep:
thousand_sep, decimal_sep = (
DEFAULT_THOUSAND_SEPARATOR,
DEFAULT_DECIMAL_SEPARATOR,
)
return thousand_sep.value, decimal_sep.value

View file

@ -1,4 +1,5 @@
import typing
from enum import Enum
from typing import NewType
from django.contrib.contenttypes.models import ContentType
@ -71,12 +72,49 @@ RATING_STYLE_CHOICES = [
("smile", "Smile"),
]
# We use these constants to map the separators to the values used in the database.
# The same variables are used in the frontend
class THOUSAND_SEPARATORS(Enum):
SPACE = " "
COMMA = ","
PERIOD = "."
NONE = ""
class DECIMAL_SEPARATORS(Enum):
COMMA = ","
PERIOD = "."
DEFAULT_THOUSAND_SEPARATOR = THOUSAND_SEPARATORS.NONE
DEFAULT_DECIMAL_SEPARATOR = DECIMAL_SEPARATORS.PERIOD
NUMBER_SEPARATORS = {
"": {
"label": "No formatting",
"separators": (DEFAULT_THOUSAND_SEPARATOR, DEFAULT_DECIMAL_SEPARATOR),
},
"SPACE_COMMA": {
"label": "Space, comma",
"separators": (THOUSAND_SEPARATORS.SPACE, DECIMAL_SEPARATORS.COMMA),
},
"SPACE_PERIOD": {
"label": "Space, period",
"separators": (THOUSAND_SEPARATORS.SPACE, DECIMAL_SEPARATORS.PERIOD),
},
"COMMA_PERIOD": {
"label": "Comma, period",
"separators": (THOUSAND_SEPARATORS.COMMA, DECIMAL_SEPARATORS.PERIOD),
},
"PERIOD_COMMA": {
"label": "Period, comma",
"separators": (DECIMAL_SEPARATORS.PERIOD, THOUSAND_SEPARATORS.COMMA),
},
}
NUMBER_SEPARATOR_CHOICES = [
("", "No formatting"),
("SPACE_COMMA", "Space, comma"),
("SPACE_PERIOD", "Space, period"),
("COMMA_PERIOD", "Comma, period"),
("PERIOD_COMMA", "Period, comma"),
(key, value["label"]) for key, value in NUMBER_SEPARATORS.items()
]
DURATION_FORMAT_CHOICES = [(k, v["name"]) for k, v in DURATION_FORMATS.items()]

View file

@ -0,0 +1,7 @@
{
"type": "refactor",
"message": "Code cleanup for advanced number formatting",
"issue_number": 3293,
"bullet_points": [],
"created_at": "2025-01-08"
}

View file

@ -1,23 +1,29 @@
<template functional>
<div v-if="props.value !== null" class="array-field__item">
<div
v-if="props.value !== null"
class="array-field__item"
:class="{
'cell-error': props.value === 'NaN',
}"
>
<div class="array-field__ellipsis">
{{ $options.methods.format(props.field, props.value) }}
{{
props.value === 'NaN'
? parent.$t('fieldErrors.invalidNumber')
: $options.methods.formatFrontendNumber(props.field, props.value)
}}
</div>
</div>
</template>
<script>
import BigNumber from 'bignumber.js'
import { formatNumberValue } from '@baserow/modules/database/utils/number'
import { formatFrontendNumber } from '@baserow/modules/database/utils/number'
export default {
name: 'FunctionalFormulaArrayNumberItem',
methods: {
format(field, value) {
if (value == null || value === '') {
return ''
}
return formatNumberValue(field, new BigNumber(value))
formatFrontendNumber(field, value) {
return formatFrontendNumber(field, value)
},
},
}

View file

@ -39,6 +39,7 @@ export default {
mixins: [rowEditField, rowEditFieldInput, numberField],
watch: {
field: {
immediate: true,
handler() {
this.initCopy(this.value)
},
@ -47,9 +48,11 @@ export default {
handler(newValue) {
this.initCopy(newValue)
},
immediate: true,
},
},
created() {
this.updateFormattedValue(this.field, this.value)
},
methods: {
initCopy(value) {
this.copy = this.prepareCopy(value ?? '')

View file

@ -9,24 +9,24 @@
}"
>
<div class="grid-field-number">
{{ $options.methods.formatNumberValue(props.field, props.value) }}
{{
props.value === 'NaN'
? parent.$t('fieldErrors.invalidNumber')
: $options.methods.formatFrontendNumber(props.field, props.value)
}}
</div>
</div>
</template>
<script>
import { formatNumberValue } from '@baserow/modules/database/utils/number'
import BigNumber from 'bignumber.js'
import { formatFrontendNumber } from '@baserow/modules/database/utils/number'
export default {
name: 'FunctionalGridViewFieldNumber',
functional: true,
methods: {
formatNumberValue(field, value) {
if (value == null || value === '') {
return ''
}
return formatNumberValue(field, new BigNumber(value))
formatFrontendNumber(field, value) {
return formatFrontendNumber(field, value)
},
},
}

View file

@ -13,6 +13,9 @@ export default {
data() {
return {
copy: null,
// This can be used to avoid changing the value if the user is editing it
// Or can be set i.e. by onFocus event
focused: false,
}
},
watch: {

View file

@ -1,5 +1,8 @@
import BigNumber from 'bignumber.js'
// We use these constants to map the separators to the values used in the database.
// The same variables are used in the backend.
const THOUSAND_SEPARATORS = {
SPACE: ' ',
COMMA: ',',
@ -205,5 +208,16 @@ export const parseNumberValue = (field, value, roundDecimals = true) => {
}
const parsedNumber = toBigNumber(result)
return parsedNumber.isNaN() ? null : isNegative ? -parsedNumber : parsedNumber
return parsedNumber.isNaN()
? null
: isNegative
? parsedNumber.negated()
: parsedNumber
}
export const formatFrontendNumber = (field, value) => {
if (value == null || value === '') {
return ''
}
return formatNumberValue(field, new BigNumber(value))
}

View file

@ -0,0 +1,119 @@
/**
* @jest-environment jsdom
*/
import {
formatNumberValue,
parseNumberValue,
} from '@baserow/modules/database/utils/number'
describe('test number formatting and parsing', () => {
const baseField = {
number_decimal_places: 2,
number_negative: true,
number_prefix: '',
number_suffix: '',
number_separator: '',
}
test('test basic number formatting', () => {
const field = { ...baseField }
expect(formatNumberValue(field, 1234.56)).toBe('1234.56')
expect(formatNumberValue(field, -1234.56)).toBe('-1234.56')
expect(formatNumberValue(field, null)).toBe('')
expect(formatNumberValue(field, '')).toBe('')
expect(formatNumberValue(field, '1234.56789')).toBe('1234.57')
})
test('test number formatting with different separators', () => {
const field = { ...baseField }
field.number_separator = 'SPACE_COMMA'
expect(formatNumberValue(field, 1234.56)).toBe('1 234,56')
expect(formatNumberValue(field, -1000000.99)).toBe('-1 000 000,99')
field.number_separator = 'PERIOD_COMMA'
expect(formatNumberValue(field, 1234.56)).toBe('1.234,56')
expect(formatNumberValue(field, -1000000.99)).toBe('-1.000.000,99')
field.number_separator = 'SPACE_PERIOD'
expect(formatNumberValue(field, 1234.56)).toBe('1 234.56')
expect(formatNumberValue(field, -1000000.99)).toBe('-1 000 000.99')
field.number_separator = 'COMMA_PERIOD'
expect(formatNumberValue(field, 1234.56)).toBe('1,234.56')
expect(formatNumberValue(field, -1000000.99)).toBe('-1,000,000.99')
})
test('test number formatting with prefix and suffix', () => {
const field = { ...baseField }
field.number_prefix = '$'
expect(formatNumberValue(field, 1234.56)).toBe('$1234.56')
expect(formatNumberValue(field, -1234.56)).toBe('-$1234.56')
field.number_suffix = ' USD'
expect(formatNumberValue(field, 1234.56)).toBe('$1234.56 USD')
expect(formatNumberValue(field, -1234.56)).toBe('-$1234.56 USD')
field.number_prefix = '$'
field.number_suffix = ' USD'
expect(formatNumberValue(field, 1234.56)).toBe('$1234.56 USD')
expect(formatNumberValue(field, -1234.56)).toBe('-$1234.56 USD')
field.number_separator = 'SPACE_COMMA'
field.number_prefix = '$'
field.number_suffix = ' USD'
expect(formatNumberValue(field, 1234.56)).toBe('$1 234,56 USD')
expect(formatNumberValue(field, -1234.56)).toBe('-$1 234,56 USD')
})
test('test number formatting with different decimal places', () => {
const field = { ...baseField }
field.number_decimal_places = 0
field.number_separator = 'SPACE_COMMA'
field.number_prefix = '$'
field.number_suffix = ' USD'
expect(formatNumberValue(field, 1234.56)).toBe('$1 235 USD')
field.number_decimal_places = 3
expect(formatNumberValue(field, 1234.56)).toBe('$1 234,560 USD')
})
test('test number parsing with different formats', () => {
const field = { ...baseField }
expect(parseNumberValue(field, null)).toBe(null)
expect(parseNumberValue(field, '')).toBe(null)
field.number_separator = 'SPACE_COMMA'
expect(parseNumberValue(field, '1 234,56').toNumber()).toBe(1234.56)
expect(parseNumberValue(field, '-1 234,56').toNumber()).toBe(-1234.56)
field.number_separator = 'PERIOD_COMMA'
expect(parseNumberValue(field, '1.234,56').toNumber()).toBe(1234.56)
expect(parseNumberValue(field, '-1.234,56').toNumber()).toBe(-1234.56)
field.number_separator = 'SPACE_PERIOD'
expect(parseNumberValue(field, '1 234.56').toNumber()).toBe(1234.56)
expect(parseNumberValue(field, '-1 234.56').toNumber()).toBe(-1234.56)
field.number_separator = 'COMMA_PERIOD'
expect(parseNumberValue(field, '1,234.56').toNumber()).toBe(1234.56)
expect(parseNumberValue(field, '-1,234.56').toNumber()).toBe(-1234.56)
})
test('test number parsing with prefix and suffix', () => {
const field = { ...baseField }
field.number_separator = 'PERIOD_COMMA'
field.number_prefix = '$'
expect(parseNumberValue(field, '$1.234,56').toNumber()).toBe(1234.56)
expect(parseNumberValue(field, '-$1.234,56').toNumber()).toBe(-1234.56)
field.number_suffix = ' USD'
expect(parseNumberValue(field, '$1.234,56 USD').toNumber()).toBe(1234.56)
expect(parseNumberValue(field, '-$1.234,56 USD').toNumber()).toBe(-1234.56)
field.number_prefix = ''
expect(parseNumberValue(field, '1.234,56 USD').toNumber()).toBe(1234.56)
expect(parseNumberValue(field, '-1.234,56 USD').toNumber()).toBe(-1234.56)
})
})