WebUI: replace rounding function from MooTools

The `round()` returning floating point number is not a good idea. This is due to floating point
representation is imprecise and sometimes it cannot faithfully represent a number, for example
`0.09 + 0.01 !== 0.1 `. Therefore, it should be avoided and/or utilize other function
to achieve the goal.

Also, improve `window.qBittorrent.Misc.toFixedPointString()` and add test cases.

PR #22281.
This commit is contained in:
Chocobo1 2025-02-17 15:11:55 +08:00 committed by GitHub
parent 8da43a4054
commit 955688c125
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 134 additions and 47 deletions

View File

@ -34,7 +34,12 @@ jobs:
run: |
npm install
npm ls
echo "::group::npm ls --all"
npm ls --all
echo "::endgroup::"
- name: Run tests
run: npm test
- name: Lint code
run: npm run lint

View File

@ -6,8 +6,9 @@
"url": "https://github.com/qbittorrent/qBittorrent.git"
},
"scripts": {
"format": "js-beautify -r *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && prettier --write **.css",
"lint": "eslint --cache *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && stylelint --cache **/*.css && html-validate private public"
"format": "js-beautify -r *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js test/*/*.js && prettier --write **.css",
"lint": "eslint --cache *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js test/*/*.js && stylelint --cache **/*.css && html-validate private public",
"test": "vitest run --dom"
},
"devDependencies": {
"@stylistic/eslint-plugin": "*",
@ -15,11 +16,13 @@
"eslint-plugin-html": "*",
"eslint-plugin-prefer-arrow-functions": "*",
"eslint-plugin-regexp": "*",
"happy-dom": "*",
"html-validate": "*",
"js-beautify": "*",
"prettier": "*",
"stylelint": "*",
"stylelint-config-standard": "*",
"stylelint-order": "*"
"stylelint-order": "*",
"vitest": "*"
}
}

View File

@ -1231,9 +1231,7 @@ window.qBittorrent.DynamicTable ??= (() => {
// progress
this.columns["progress"].updateTd = function(td, row) {
const progress = this.getRowValue(row);
let progressFormatted = (progress * 100).round(1);
if ((progressFormatted === 100.0) && (progress !== 1.0))
progressFormatted = 99.9;
const progressFormatted = window.qBittorrent.Misc.toFixedPointString((progress * 100), 1);
const div = td.firstElementChild;
if (div !== null) {
@ -1782,10 +1780,7 @@ window.qBittorrent.DynamicTable ??= (() => {
// progress
this.columns["progress"].updateTd = function(td, row) {
const progress = this.getRowValue(row);
let progressFormatted = (progress * 100).round(1);
if ((progressFormatted === 100.0) && (progress !== 1.0))
progressFormatted = 99.9;
progressFormatted += "%";
const progressFormatted = `${window.qBittorrent.Misc.toFixedPointString((progress * 100), 1)}%`;
td.textContent = progressFormatted;
td.title = progressFormatted;
};

View File

@ -92,6 +92,9 @@ window.qBittorrent.Misc ??= (() => {
* JS counterpart of the function in src/misc.cpp
*/
const friendlyUnit = (value, isSpeed) => {
if ((value === undefined) || (value === null) || Number.isNaN(value) || (value < 0))
return "QBT_TR(Unknown)QBT_TR[CONTEXT=misc]";
const units = [
"QBT_TR(B)QBT_TR[CONTEXT=misc]",
"QBT_TR(KiB)QBT_TR[CONTEXT=misc]",
@ -102,15 +105,6 @@ window.qBittorrent.Misc ??= (() => {
"QBT_TR(EiB)QBT_TR[CONTEXT=misc]"
];
if ((value === undefined) || (value === null) || (value < 0))
return "QBT_TR(Unknown)QBT_TR[CONTEXT=misc]";
let i = 0;
while ((value >= 1024.0) && (i < 6)) {
value /= 1024.0;
++i;
}
const friendlyUnitPrecision = (sizeUnit) => {
if (sizeUnit <= 2) // KiB, MiB
return 1;
@ -120,15 +114,20 @@ window.qBittorrent.Misc ??= (() => {
return 3;
};
let i = 0;
while ((value >= 1024) && (i < 6)) {
value /= 1024;
++i;
}
let ret;
if (i === 0) {
ret = `${value} ${units[i]}`;
}
else {
const precision = friendlyUnitPrecision(i);
const offset = Math.pow(10, precision);
// Don't round up
ret = `${(Math.floor(offset * value) / offset).toFixed(precision)} ${units[i]}`;
ret = `${toFixedPointString(value, precision)} ${units[i]}`;
}
if (isSpeed)
@ -163,12 +162,12 @@ window.qBittorrent.Misc ??= (() => {
};
const friendlyPercentage = (value) => {
let percentage = (value * 100).round(1);
let percentage = value * 100;
if (Number.isNaN(percentage) || (percentage < 0))
percentage = 0;
if (percentage > 100)
percentage = 100;
return `${percentage.toFixed(1)}%`;
return `${toFixedPointString(percentage, 1)}%`;
};
/*
@ -225,13 +224,27 @@ window.qBittorrent.Misc ??= (() => {
};
const toFixedPointString = (number, digits) => {
// Do not round up number
const power = Math.pow(10, digits);
return (Math.floor(power * number) / power).toFixed(digits);
if (Number.isNaN(number))
return number.toString();
const sign = (number < 0) ? "-" : "";
// Do not round up `number`
// Small floating point numbers are imprecise, thus process as a String
const tmp = Math.trunc(`${Math.abs(number)}e${digits}`).toString();
if (digits <= 0) {
return (tmp === "0") ? tmp : `${sign}${tmp}`;
}
else if (digits < tmp.length) {
const idx = tmp.length - digits;
return `${sign}${tmp.slice(0, idx)}.${tmp.slice(idx)}`;
}
else {
const zeros = "0".repeat(digits - tmp.length);
return `${sign}0.${zeros}${tmp}`;
}
};
/**
*
* @param {String} text the text to search
* @param {Array<String>} terms terms to search for within the text
* @returns {Boolean} true if all terms match the text, false otherwise

View File

@ -116,13 +116,13 @@ window.qBittorrent.ProgressBar ??= (() => {
}
function ProgressBar_setValue(value) {
value = parseFloat(value);
value = Number(value);
if (Number.isNaN(value))
value = 0;
value = Math.min(Math.max(value, 0), 100);
this.vals.value = value;
const displayedValue = `${value.round(1).toFixed(1)}%`;
const displayedValue = `${window.qBittorrent.Misc.toFixedPointString(value, 1)}%`;
this.vals.dark.textContent = displayedValue;
this.vals.light.textContent = displayedValue;

View File

@ -383,25 +383,18 @@ window.qBittorrent.PropFiles ??= (() => {
is_seed = (files.length > 0) ? files[0].is_seed : true;
const rows = files.map((file, index) => {
let progress = (file.progress * 100).round(1);
if ((progress === 100) && (file.progress < 1))
progress = 99.9;
const ignore = (file.priority === FilePriority.Ignored);
const checked = (ignore ? TriState.Unchecked : TriState.Checked);
const remaining = (ignore ? 0 : (file.size * (1.0 - file.progress)));
const row = {
fileId: index,
checked: checked,
checked: (ignore ? TriState.Unchecked : TriState.Checked),
fileName: file.name,
name: window.qBittorrent.Filesystem.fileName(file.name),
size: file.size,
progress: progress,
progress: window.qBittorrent.Misc.toFixedPointString((file.progress * 100), 1),
priority: normalizePriority(file.priority),
remaining: remaining,
remaining: (ignore ? 0 : (file.size * (1 - file.progress))),
availability: file.availability
};
return row;
});

View File

@ -64,7 +64,7 @@ MochaUI.extend({
new Slider($("uplimitSliderarea"), $("uplimitSliderknob"), {
steps: maximum,
offset: 0,
initialStep: up_limit.round(),
initialStep: Math.round(up_limit),
onChange: (pos) => {
if (pos > 0) {
$("uplimitUpdatevalue").value = pos;
@ -82,7 +82,7 @@ MochaUI.extend({
$("upLimitUnit").style.visibility = "hidden";
}
else {
$("uplimitUpdatevalue").value = up_limit.round();
$("uplimitUpdatevalue").value = Math.round(up_limit);
$("upLimitUnit").style.visibility = "visible";
}
}
@ -111,7 +111,7 @@ MochaUI.extend({
new Slider($("uplimitSliderarea"), $("uplimitSliderknob"), {
steps: maximum,
offset: 0,
initialStep: (up_limit / 1024.0).round(),
initialStep: Math.round(up_limit / 1024),
onChange: (pos) => {
if (pos > 0) {
$("uplimitUpdatevalue").value = pos;
@ -129,7 +129,7 @@ MochaUI.extend({
$("upLimitUnit").style.visibility = "hidden";
}
else {
$("uplimitUpdatevalue").value = (up_limit / 1024.0).round();
$("uplimitUpdatevalue").value = Math.round(up_limit / 1024);
$("upLimitUnit").style.visibility = "visible";
}
});
@ -173,7 +173,7 @@ MochaUI.extend({
new Slider($("dllimitSliderarea"), $("dllimitSliderknob"), {
steps: maximum,
offset: 0,
initialStep: dl_limit.round(),
initialStep: Math.round(dl_limit),
onChange: (pos) => {
if (pos > 0) {
$("dllimitUpdatevalue").value = pos;
@ -191,7 +191,7 @@ MochaUI.extend({
$("dlLimitUnit").style.visibility = "hidden";
}
else {
$("dllimitUpdatevalue").value = dl_limit.round();
$("dllimitUpdatevalue").value = Math.round(dl_limit);
$("dlLimitUnit").style.visibility = "visible";
}
}
@ -220,7 +220,7 @@ MochaUI.extend({
new Slider($("dllimitSliderarea"), $("dllimitSliderknob"), {
steps: maximum,
offset: 0,
initialStep: (dl_limit / 1024.0).round(),
initialStep: Math.round(dl_limit / 1024),
onChange: (pos) => {
if (pos > 0) {
$("dllimitUpdatevalue").value = pos;
@ -238,7 +238,7 @@ MochaUI.extend({
$("dlLimitUnit").style.visibility = "hidden";
}
else {
$("dllimitUpdatevalue").value = (dl_limit / 1024.0).round();
$("dllimitUpdatevalue").value = Math.round(dl_limit / 1024);
$("dlLimitUnit").style.visibility = "visible";
}
});

View File

@ -0,0 +1,78 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Mike Tzou (Chocobo1)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
import { expect, test } from "vitest";
import "../../private/scripts/misc.js";
test("Test toFixedPointString()", () => {
const toFixedPointString = window.qBittorrent.Misc.toFixedPointString;
expect(toFixedPointString(0, 0)).toBe("0");
expect(toFixedPointString(0, 1)).toBe("0.0");
expect(toFixedPointString(0, 2)).toBe("0.00");
expect(toFixedPointString(0.1, 0)).toBe("0");
expect(toFixedPointString(0.1, 1)).toBe("0.1");
expect(toFixedPointString(0.1, 2)).toBe("0.10");
expect(toFixedPointString(-0.1, 0)).toBe("0");
expect(toFixedPointString(-0.1, 1)).toBe("-0.1");
expect(toFixedPointString(-0.1, 2)).toBe("-0.10");
expect(toFixedPointString(1.005, 0)).toBe("1");
expect(toFixedPointString(1.005, 1)).toBe("1.0");
expect(toFixedPointString(1.005, 2)).toBe("1.00");
expect(toFixedPointString(1.005, 3)).toBe("1.005");
expect(toFixedPointString(1.005, 4)).toBe("1.0050");
expect(toFixedPointString(-1.005, 0)).toBe("-1");
expect(toFixedPointString(-1.005, 1)).toBe("-1.0");
expect(toFixedPointString(-1.005, 2)).toBe("-1.00");
expect(toFixedPointString(-1.005, 3)).toBe("-1.005");
expect(toFixedPointString(-1.005, 4)).toBe("-1.0050");
expect(toFixedPointString(35.855, 0)).toBe("35");
expect(toFixedPointString(35.855, 1)).toBe("35.8");
expect(toFixedPointString(35.855, 2)).toBe("35.85");
expect(toFixedPointString(35.855, 3)).toBe("35.855");
expect(toFixedPointString(35.855, 4)).toBe("35.8550");
expect(toFixedPointString(-35.855, 0)).toBe("-35");
expect(toFixedPointString(-35.855, 1)).toBe("-35.8");
expect(toFixedPointString(-35.855, 2)).toBe("-35.85");
expect(toFixedPointString(-35.855, 3)).toBe("-35.855");
expect(toFixedPointString(-35.855, 4)).toBe("-35.8550");
expect(toFixedPointString(100.00, 0)).toBe("100");
expect(toFixedPointString(100.00, 1)).toBe("100.0");
expect(toFixedPointString(100.00, 2)).toBe("100.00");
expect(toFixedPointString(-100.00, 0)).toBe("-100");
expect(toFixedPointString(-100.00, 1)).toBe("-100.0");
expect(toFixedPointString(-100.00, 2)).toBe("-100.00");
});