Commit 6bdc9fac by Torkel Ödegaard Committed by GitHub

Decimals: Big Improvements to auto decimals and fixes to auto decimals bug found…

Decimals: Big Improvements to auto decimals and fixes to auto decimals bug found in 7.4-beta1  (#30519)

* Decimals: Nukes scaledDecimals from the earth it was an abomination

* Moved move tests

* Fixed test

* Updated tests

* Updated test
parent be8ba8ef
......@@ -144,33 +144,6 @@ describe('Format value', () => {
expect(result.text).toEqual('10.0');
});
it('should set auto decimals, 1 significant', () => {
const value = 3.23;
const instance = getDisplayProcessorFromConfig({ decimals: null });
expect(instance(value).text).toEqual('3.23');
});
it('should set auto decimals, 2 significant', () => {
const value = 0.0245;
const instance = getDisplayProcessorFromConfig({ decimals: null });
expect(instance(value).text).toEqual('0.0245');
});
it('should set auto decimals correctly for value 0.333333333333', () => {
const value = 1 / 3;
const instance = getDisplayProcessorFromConfig({ decimals: null });
expect(instance(value).text).toEqual('0.333');
});
it('should use override decimals', () => {
const value = 100030303;
const instance = getDisplayProcessorFromConfig({ decimals: 2, unit: 'bytes' });
const disp = instance(value);
expect(disp.text).toEqual('95.40');
expect(disp.suffix).toEqual(' MiB');
});
it('should return mapped value if there are matching value mappings', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
......@@ -211,7 +184,7 @@ describe('Format value', () => {
const value = 1000;
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
const disp = instance(value);
expect(disp.text).toEqual('1.0');
expect(disp.text).toEqual('1');
expect(disp.suffix).toEqual(' K');
});
......@@ -219,7 +192,7 @@ describe('Format value', () => {
const value = 1200;
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
const disp = instance(value);
expect(disp.text).toEqual('1.2');
expect(disp.text).toEqual('1.20');
expect(disp.suffix).toEqual(' K');
});
......@@ -235,7 +208,7 @@ describe('Format value', () => {
const value = 1000000;
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
const disp = instance(value);
expect(disp.text).toEqual('1.0');
expect(disp.text).toEqual('1');
expect(disp.suffix).toEqual(' Mil');
});
......@@ -243,9 +216,17 @@ describe('Format value', () => {
const value = 1500000;
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
const disp = instance(value);
expect(disp.text).toEqual('1.5');
expect(disp.text).toEqual('1.50');
expect(disp.suffix).toEqual(' Mil');
});
it('with value 128000000 and unit bytes', () => {
const value = 1280000125;
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'bytes' });
const disp = instance(value);
expect(disp.text).toEqual('1.19');
expect(disp.suffix).toEqual(' GiB');
});
});
describe('Date display options', () => {
......
......@@ -4,7 +4,7 @@ import _ from 'lodash';
// Types
import { Field, FieldType } from '../types/dataFrame';
import { GrafanaTheme } from '../types/theme';
import { DecimalCount, DecimalInfo, DisplayProcessor, DisplayValue } from '../types/displayValue';
import { DisplayProcessor, DisplayValue } from '../types/displayValue';
import { getValueFormat } from '../valueFormats/valueFormats';
import { getMappedValue } from '../utils/valueMappings';
import { dateTime } from '../datetime';
......@@ -86,8 +86,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
if (!isNaN(numeric)) {
if (shouldFormat && !_.isBoolean(value)) {
const { decimals, scaledDecimals } = getDecimalsForValue(value, config.decimals);
const v = formatFunc(numeric, decimals, scaledDecimals, options.timeZone);
const v = formatFunc(numeric, config.decimals, null, options.timeZone);
text = v.text;
suffix = v.suffix;
prefix = v.prefix;
......@@ -137,53 +136,6 @@ function toStringProcessor(value: any): DisplayValue {
return { text: _.toString(value), numeric: toNumber(value) };
}
function getSignificantDigitCount(n: number) {
//remove decimal and make positive
n = Math.abs(+String(n).replace('.', ''));
if (n === 0) {
return 0;
}
// kill the 0s at the end of n
while (n !== 0 && n % 10 === 0) {
n /= 10;
}
// get number of digits
return Math.floor(Math.log(n) / Math.LN10) + 1;
}
export function getDecimalsForValue(value: number, decimalOverride?: DecimalCount): DecimalInfo {
if (_.isNumber(decimalOverride)) {
// It's important that scaledDecimals is null here
return { decimals: decimalOverride, scaledDecimals: null };
}
if (value === 0) {
return { decimals: 0, scaledDecimals: 0 };
}
const digits = getSignificantDigitCount(value);
const log10 = Math.floor(Math.log(Math.abs(value)) / Math.LN10);
let dec = -log10 + 1;
const magn = Math.pow(10, -dec);
const norm = value / magn; // norm is between 1.0 and 10.0
// special case for 2.5, requires an extra decimal
if (norm > 2.25) {
++dec;
}
if (value % 1 === 0) {
dec = 0;
}
const decimals = Math.max(0, dec);
const scaledDecimals = decimals - log10 + digits - 1;
return { decimals, scaledDecimals };
}
export function getRawDisplayProcessor(): DisplayProcessor {
return (value: any) => ({
text: `${value}`,
......
......@@ -31,7 +31,7 @@ const INTERVALS_IN_SECONDS: IntervalsInSeconds = {
[Interval.Millisecond]: 0.001,
};
export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
export function toNanoSeconds(size: number, decimals?: DecimalCount): FormattedValue {
if (size === null) {
return { text: '' };
}
......@@ -39,21 +39,21 @@ export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecim
if (Math.abs(size) < 1000) {
return { text: toFixed(size, decimals), suffix: ' ns' };
} else if (Math.abs(size) < 1000000) {
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' µs');
return toFixedScaled(size / 1000, decimals, ' µs');
} else if (Math.abs(size) < 1000000000) {
return toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' ms');
return toFixedScaled(size / 1000000, decimals, ' ms');
} else if (Math.abs(size) < 60000000000) {
return toFixedScaled(size / 1000000000, decimals, scaledDecimals, 9, ' s');
return toFixedScaled(size / 1000000000, decimals, ' s');
} else if (Math.abs(size) < 3600000000000) {
return toFixedScaled(size / 60000000000, decimals, scaledDecimals, 12, ' min');
return toFixedScaled(size / 60000000000, decimals, ' min');
} else if (Math.abs(size) < 86400000000000) {
return toFixedScaled(size / 3600000000000, decimals, scaledDecimals, 13, ' hour');
return toFixedScaled(size / 3600000000000, decimals, ' hour');
} else {
return toFixedScaled(size / 86400000000000, decimals, scaledDecimals, 14, ' day');
return toFixedScaled(size / 86400000000000, decimals, ' day');
}
}
export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
export function toMicroSeconds(size: number, decimals?: DecimalCount): FormattedValue {
if (size === null) {
return { text: '' };
}
......@@ -61,9 +61,9 @@ export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDeci
if (Math.abs(size) < 1000) {
return { text: toFixed(size, decimals), suffix: ' µs' };
} else if (Math.abs(size) < 1000000) {
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' ms');
return toFixedScaled(size / 1000, decimals, ' ms');
} else {
return toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' s');
return toFixedScaled(size / 1000000, decimals, ' s');
}
}
......@@ -76,19 +76,19 @@ export function toMilliSeconds(size: number, decimals?: DecimalCount, scaledDeci
return { text: toFixed(size, decimals), suffix: ' ms' };
} else if (Math.abs(size) < 60000) {
// Less than 1 min
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' s');
return toFixedScaled(size / 1000, decimals, ' s');
} else if (Math.abs(size) < 3600000) {
// Less than 1 hour, divide in minutes
return toFixedScaled(size / 60000, decimals, scaledDecimals, 5, ' min');
return toFixedScaled(size / 60000, decimals, ' min');
} else if (Math.abs(size) < 86400000) {
// Less than one day, divide in hours
return toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, ' hour');
return toFixedScaled(size / 3600000, decimals, ' hour');
} else if (Math.abs(size) < 31536000000) {
// Less than one year, divide in days
return toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, ' day');
return toFixedScaled(size / 86400000, decimals, ' day');
}
return toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year');
return toFixedScaled(size / 31536000000, decimals, ' year');
}
export function trySubstract(value1: DecimalCount, value2: DecimalCount): DecimalCount {
......@@ -98,7 +98,7 @@ export function trySubstract(value1: DecimalCount, value2: DecimalCount): Decima
return undefined;
}
export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
export function toSeconds(size: number, decimals?: DecimalCount): FormattedValue {
if (size === null) {
return { text: '' };
}
......@@ -110,37 +110,37 @@ export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?
// Less than 1 µs, divide in ns
if (Math.abs(size) < 0.000001) {
return toFixedScaled(size * 1e9, decimals, trySubstract(scaledDecimals, decimals), -9, ' ns');
return toFixedScaled(size * 1e9, decimals, ' ns');
}
// Less than 1 ms, divide in µs
if (Math.abs(size) < 0.001) {
return toFixedScaled(size * 1e6, decimals, trySubstract(scaledDecimals, decimals), -6, ' µs');
return toFixedScaled(size * 1e6, decimals, ' µs');
}
// Less than 1 second, divide in ms
if (Math.abs(size) < 1) {
return toFixedScaled(size * 1e3, decimals, trySubstract(scaledDecimals, decimals), -3, ' ms');
return toFixedScaled(size * 1e3, decimals, ' ms');
}
if (Math.abs(size) < 60) {
return { text: toFixed(size, decimals), suffix: ' s' };
} else if (Math.abs(size) < 3600) {
// Less than 1 hour, divide in minutes
return toFixedScaled(size / 60, decimals, scaledDecimals, 1, ' min');
return toFixedScaled(size / 60, decimals, ' min');
} else if (Math.abs(size) < 86400) {
// Less than one day, divide in hours
return toFixedScaled(size / 3600, decimals, scaledDecimals, 4, ' hour');
return toFixedScaled(size / 3600, decimals, ' hour');
} else if (Math.abs(size) < 604800) {
// Less than one week, divide in days
return toFixedScaled(size / 86400, decimals, scaledDecimals, 5, ' day');
return toFixedScaled(size / 86400, decimals, ' day');
} else if (Math.abs(size) < 31536000) {
// Less than one year, divide in week
return toFixedScaled(size / 604800, decimals, scaledDecimals, 6, ' week');
return toFixedScaled(size / 604800, decimals, ' week');
}
return toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year');
return toFixedScaled(size / 3.15569e7, decimals, ' year');
}
export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
export function toMinutes(size: number, decimals?: DecimalCount): FormattedValue {
if (size === null) {
return { text: '' };
}
......@@ -148,17 +148,17 @@ export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals?
if (Math.abs(size) < 60) {
return { text: toFixed(size, decimals), suffix: ' min' };
} else if (Math.abs(size) < 1440) {
return toFixedScaled(size / 60, decimals, scaledDecimals, 2, ' hour');
return toFixedScaled(size / 60, decimals, ' hour');
} else if (Math.abs(size) < 10080) {
return toFixedScaled(size / 1440, decimals, scaledDecimals, 3, ' day');
return toFixedScaled(size / 1440, decimals, ' day');
} else if (Math.abs(size) < 604800) {
return toFixedScaled(size / 10080, decimals, scaledDecimals, 4, ' week');
return toFixedScaled(size / 10080, decimals, ' week');
} else {
return toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, ' year');
return toFixedScaled(size / 5.25948e5, decimals, ' year');
}
}
export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
export function toHours(size: number, decimals?: DecimalCount): FormattedValue {
if (size === null) {
return { text: '' };
}
......@@ -166,15 +166,15 @@ export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?:
if (Math.abs(size) < 24) {
return { text: toFixed(size, decimals), suffix: ' hour' };
} else if (Math.abs(size) < 168) {
return toFixedScaled(size / 24, decimals, scaledDecimals, 2, ' day');
return toFixedScaled(size / 24, decimals, ' day');
} else if (Math.abs(size) < 8760) {
return toFixedScaled(size / 168, decimals, scaledDecimals, 3, ' week');
return toFixedScaled(size / 168, decimals, ' week');
} else {
return toFixedScaled(size / 8760, decimals, scaledDecimals, 4, ' year');
return toFixedScaled(size / 8760, decimals, ' year');
}
}
export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount): FormattedValue {
export function toDays(size: number, decimals?: DecimalCount): FormattedValue {
if (size === null) {
return { text: '' };
}
......@@ -182,9 +182,9 @@ export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: D
if (Math.abs(size) < 7) {
return { text: toFixed(size, decimals), suffix: ' day' };
} else if (Math.abs(size) < 365) {
return toFixedScaled(size / 7, decimals, scaledDecimals, 2, ' week');
return toFixedScaled(size / 7, decimals, ' week');
} else {
return toFixedScaled(size / 365, decimals, scaledDecimals, 3, ' year');
return toFixedScaled(size / 365, decimals, ' year');
}
}
......@@ -340,8 +340,8 @@ export function toDurationInDaysHoursMinutesSeconds(size: number): FormattedValu
return { text: dayString + hmsString.text };
}
export function toTimeTicks(size: number, decimals: DecimalCount, scaledDecimals: DecimalCount): FormattedValue {
return toSeconds(size / 100, decimals, scaledDecimals);
export function toTimeTicks(size: number, decimals: DecimalCount): FormattedValue {
return toSeconds(size / 100, decimals);
}
export function toClockMilliseconds(size: number, decimals: DecimalCount): FormattedValue {
......
......@@ -17,6 +17,10 @@ const formatTests: ValueFormatTest[] = [
{ id: 'currencyUSD', decimals: 2, value: 1532.82, result: '$1.53K' },
{ id: 'currencyKRW', decimals: 2, value: 1532.82, result: '₩1.53K' },
{ id: 'currencyIDR', decimals: 2, value: 1532.82, result: 'Rp1.53K' },
// no unit
{ id: 'none', decimals: null, value: 3.23, result: '3.23' },
{ id: 'none', decimals: null, value: 0.0245, result: '0.0245' },
{ id: 'none', decimals: null, value: 1 / 3, result: '0.333' },
// Standard
{ id: 'ms', decimals: 4, value: 0.0024, result: '0.0024 ms' },
......@@ -24,11 +28,17 @@ const formatTests: ValueFormatTest[] = [
{ id: 'ms', decimals: 2, value: 1250, result: '1.25 s' },
{ id: 'ms', decimals: 1, value: 10000086.123, result: '2.8 hour' },
{ id: 'ms', decimals: 0, value: 1200, result: '1 s' },
{ id: 'short', decimals: 0, scaledDecimals: -1, value: 98765, result: '98.77 K' },
{ id: 'short', decimals: 0, scaledDecimals: 0, value: 9876543, result: '9.876543 Mil' },
{ id: 'short', decimals: 2, scaledDecimals: null, value: 9876543, result: '9.88 Mil' },
{ id: 'kbytes', decimals: 3, value: 10000000, result: '9.537 GiB' },
{ id: 'deckbytes', decimals: 3, value: 10000000, result: '10.000 GB' },
{ id: 'short', decimals: null, value: 1000, result: '1 K' },
{ id: 'short', decimals: null, value: 1200, result: '1.20 K' },
{ id: 'short', decimals: null, value: 1250, result: '1.25 K' },
{ id: 'short', decimals: null, value: 1000000, result: '1 Mil' },
{ id: 'short', decimals: null, value: 1500000, result: '1.50 Mil' },
{ id: 'short', decimals: null, value: 1000120, result: '1.00 Mil' },
{ id: 'short', decimals: null, value: 98765, result: '98.8 K' },
{ id: 'short', decimals: null, value: 9876543, result: '9.88 Mil' },
{ id: 'short', decimals: null, value: 9876543, result: '9.88 Mil' },
{ id: 'kbytes', decimals: null, value: 10000000, result: '9.54 GiB' },
{ id: 'deckbytes', decimals: null, value: 10000000, result: '10 GB' },
{ id: 'megwatt', decimals: 3, value: 1000, result: '1.000 GW' },
{ id: 'kohm', decimals: 3, value: 1000, result: '1.000 MΩ' },
{ id: 'Mohm', decimals: 3, value: 1000, result: '1.000 GΩ' },
......
......@@ -45,10 +45,15 @@ export function toFixed(value: number, decimals?: DecimalCount): string {
if (value === null) {
return '';
}
if (value === Number.NEGATIVE_INFINITY || value === Number.POSITIVE_INFINITY) {
return value.toLocaleString();
}
if (decimals === null || decimals === undefined) {
decimals = getDecimalsForValue(value);
}
const factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1;
const formatted = String(Math.round(value * factor) / factor);
......@@ -57,31 +62,37 @@ export function toFixed(value: number, decimals?: DecimalCount): string {
return formatted;
}
// If tickDecimals was specified, ensure that we have exactly that
// much precision; otherwise default to the value's own precision.
if (decimals != null) {
const decimalPos = formatted.indexOf('.');
const precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
if (precision < decimals) {
return (precision ? formatted : formatted + '.') + String(factor).substr(1, decimals - precision);
}
}
return formatted;
}
export function toFixedScaled(
value: number,
decimals: DecimalCount,
scaledDecimals: DecimalCount,
additionalDecimals: number,
ext?: string
): FormattedValue {
if (scaledDecimals === null || scaledDecimals === undefined) {
return { text: toFixed(value, decimals), suffix: ext };
function getDecimalsForValue(value: number): number {
const log10 = Math.floor(Math.log(Math.abs(value)) / Math.LN10);
let dec = -log10 + 1;
const magn = Math.pow(10, -dec);
const norm = value / magn; // norm is between 1.0 and 10.0
// special case for 2.5, requires an extra decimal
if (norm > 2.25) {
++dec;
}
if (value % 1 === 0) {
dec = 0;
}
const decimals = Math.max(0, dec);
return decimals;
}
export function toFixedScaled(value: number, decimals: DecimalCount, ext?: string): FormattedValue {
return {
text: toFixed(value, scaledDecimals + additionalDecimals),
text: toFixed(value, decimals),
suffix: ext,
};
}
......@@ -126,10 +137,6 @@ export function scaledUnits(factor: number, extArray: string[]): ValueFormatter
}
}
if (steps > 0 && scaledDecimals !== null && scaledDecimals !== undefined) {
decimals = scaledDecimals + 3 * steps;
}
return { text: toFixed(size, decimals), suffix: extArray[steps] };
};
}
......
......@@ -599,7 +599,7 @@ describe('logSeriesToLogsModel', () => {
hasUniqueLabels: false,
meta: [
{ label: 'Limit', value: '1000 (0 returned)', kind: 1 },
{ label: 'Total bytes processed', value: '97 kB', kind: 1 },
{ label: 'Total bytes processed', value: '97.0 kB', kind: 1 },
],
rows: [],
};
......
......@@ -4,7 +4,6 @@ import { DecimalCount, TimeZone } from '@grafana/data';
interface ValueFormatTest {
id: string;
decimals?: DecimalCount;
scaledDecimals?: DecimalCount;
timeZone?: TimeZone;
value: number;
result: string;
......@@ -22,8 +21,8 @@ const formatTests: ValueFormatTest[] = [
{ id: 'ms', decimals: 2, value: 1250, result: '1.25 s' },
{ id: 'ms', decimals: 1, value: 10000086.123, result: '2.8 hour' },
{ id: 'ms', decimals: 0, value: 1200, result: '1 s' },
{ id: 'short', decimals: 0, scaledDecimals: -1, value: 98765, result: '98.77 K' },
{ id: 'short', decimals: 0, scaledDecimals: 0, value: 9876543, result: '9.876543 Mil' },
{ id: 'short', decimals: 0, value: 98765, result: '99 K' },
{ id: 'short', decimals: 0, value: 9876543, result: '10 Mil' },
{ id: 'kbytes', decimals: 3, value: 10000000, result: '9.537 GiB' },
{ id: 'deckbytes', decimals: 3, value: 10000000, result: '10.000 GB' },
{ id: 'megwatt', decimals: 3, value: 1000, result: '1.000 GW' },
......@@ -45,7 +44,7 @@ describe('Chcek KBN value formats', () => {
for (const test of formatTests) {
describe(`value format: ${test.id}`, () => {
it(`should translate ${test.value} as ${test.result}`, () => {
const result = kbn.valueFormats[test.id](test.value, test.decimals, test.scaledDecimals);
const result = kbn.valueFormats[test.id](test.value, test.decimals);
expect(result).toBe(test.result);
});
});
......
......@@ -743,7 +743,7 @@ class GraphElement {
};
// Use 'short' format for histogram values
this.configureAxisMode(options.xaxis, 'short');
this.configureAxisMode(options.xaxis, 'short', null);
}
addXTableAxis(options: any) {
......@@ -794,13 +794,15 @@ class GraphElement {
this.applyLogScale(options.yaxes[1], data);
this.configureAxisMode(
options.yaxes[1],
this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[1].format
this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[1].format,
this.panel.yaxes[1].decimals
);
}
this.applyLogScale(options.yaxes[0], data);
this.configureAxisMode(
options.yaxes[0],
this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[0].format
this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[0].format,
this.panel.yaxes[0].decimals
);
}
......@@ -915,14 +917,19 @@ class GraphElement {
return ticks;
}
configureAxisMode(axis: { tickFormatter: (val: any, axis: any) => string }, format: string) {
configureAxisMode(
axis: { tickFormatter: (val: any, axis: any) => string },
format: string,
decimals?: number | null
) {
axis.tickFormatter = (val, axis) => {
const formatter = getValueFormat(format);
if (!formatter) {
throw new Error(`Unit '${format}' is not supported`);
}
return formattedValueToString(formatter(val, axis.tickDecimals, axis.scaledDecimals));
return formattedValueToString(formatter(val, decimals));
};
}
}
......
......@@ -1783,11 +1783,6 @@ Licensed under the MIT license.
axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
axis.tickSize = opts.tickSize || size;
// grafana addition
if (opts.tickDecimals === null || opts.tickDecimals === undefined) {
axis.scaledDecimals = axis.tickDecimals + dec;
}
// Time mode was moved to a plug-in in 0.8, and since so many people use it
// we'll add an especially friendly reminder to make sure they included it.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment