mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2025-02-17 19:20:43 +08:00
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:
parent
8da43a4054
commit
955688c125
5
.github/workflows/ci_webui.yaml
vendored
5
.github/workflows/ci_webui.yaml
vendored
@ -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
|
||||
|
@ -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": "*"
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
|
||||
|
@ -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";
|
||||
}
|
||||
});
|
||||
|
78
src/webui/www/test/private/misc.test.js
Normal file
78
src/webui/www/test/private/misc.test.js
Normal 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");
|
||||
});
|
Loading…
Reference in New Issue
Block a user