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:
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
components
formula/array
row
view/grid/fields
mixins
utils
test/unit/database/mixins
|
@ -247,11 +247,3 @@ EXCLUDE_FIELDS_API_PARAM = OpenApiParameter(
|
|||
"response. "
|
||||
),
|
||||
)
|
||||
|
||||
NUMBER_SEPARATOR_MAPPING = {
|
||||
"": ("", "."),
|
||||
"SPACE_COMMA": (" ", ","),
|
||||
"SPACE_PERIOD": (" ", "."),
|
||||
"COMMA_PERIOD": (",", "."),
|
||||
"PERIOD_COMMA": (".", ","),
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()]
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "refactor",
|
||||
"message": "Code cleanup for advanced number formatting",
|
||||
"issue_number": 3293,
|
||||
"bullet_points": [],
|
||||
"created_at": "2025-01-08"
|
||||
}
|
|
@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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 ?? '')
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
119
web-frontend/test/unit/database/mixins/number.spec.js
Normal file
119
web-frontend/test/unit/database/mixins/number.spec.js
Normal 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)
|
||||
})
|
||||
})
|
Loading…
Add table
Reference in a new issue