// ==UserScript==
// @name MAM Ratio Prettify
// @namespace http://tampermonkey.net/
// @version 0.5
// @description 11/22/23 Reduce the precision of the main menu ratio to prettify it for larger ratios
// @author studioninja
// @match https://www.myanonamouse.net/*
// @icon https://cdn.myanonamouse.net/imagebucket/218812/square_root_icon_512x512_i31wd3km.png
// @grant none
// @license MIT
// ==/UserScript==
'use strict';
/**
* Configuration options for the formatNumber function.
*
* @property {number} maxFractionDigits - The number of decimal places to be used in the formatted number.
* Applies only to both abbreviated numbers.
* Example: With precision set to 2, a number like 1234.567 will be formatted as 1234.57.
*
* @property {number} maxSignificantDigits - Specifies how many significant digits to show in the formatted number.
* The larger the number gets, the fewer decimal places will be shown.
* The smaller the number gets, the more decimal places will be shown.
* Example: With significantFigures set to 7, a number like 12.345678 will be shown as 12.34568.
* Exmaple: With significantFigures set to 3, a number like 123,238,283 will be shown as 123,000,000.
*
* @property {number} minDigitsForAbbreviation - The minimum number of digits a number must have to be formatted in abbreviated notation.
* Controls when to convert the number into abbreviated format (like K, M, B, T).
* Example: If digitsForShorthand is 5, numbers like 9999.99999 will be formatted in abbreviated, e.g., 10.00K.
*
* @property {Object} units - An object defining abbreviated units and their corresponding numeric values.
* Maps abbreviated notations to their numeric thresholds.
* Example: In { K: 1E3, M: 1E6, B: 1E9, T: 1E12 }, K represents thousands (1E3), M millions (1E6), B billions (1E9), T trillions (1E12).
* @property {Object} location - Determines if the script should replace the ratio in specific UI locations.
* This allows for selective application of the formatting rules based on the UI context.
* Each property represents a different part of the UI.
* @property {boolean} mainMenu - Whether to apply formatting in the main menu.
* @property {boolean} userMenuList - Whether to apply formatting in the user menu list.
* @property {boolean} forum - Whether to apply formatting in forum sections.
* @property {boolean} user - Whether to apply formatting in user-related sections.
* Disabled by default to allow the user to see the full share ratio value in at least one location.
*/
const config = {
maxFractionDigits: 2,
maxSignificantDigits: 7,
minDigitsForAbbreviation: 5,
units: { K: 1E3, M: 1E6, B: 1E9, T: 1E12 },
location: {
mainMenu: true,
userMenuList: true,
forum: true,
user: true, // disabled by default so the user can have at least one location with the full share ratio value
}
};
const DEBUG = 0;
const LOG_PREFIX = '[MAM-Ratio-Prettify]';
(function () {
config.location.mainMenu && updateMenuRatio();
config.location.userMenuList && updateMenuListRatio();
config.location.forum && updateForumRatio();
config.location.user && updateUserRatio();
})();
function updateMenuRatio() {
const ratioEl = document.querySelector("#tmR");
updateRatio(ratioEl.firstChild);
// utilize an observer to handle the site or other scripts changing the value
const callback = (mutationList, observer) => {
debugLog(`Observer callback triggered`, mutationList, observer);
// Check if the relevant child node exists
if (!ratioEl.firstChild) return;
// Disconnect the observer to prevent infinite loops
observer.disconnect();
// Update the ratio
debugLog(`Observer updating ratio in element: `, ratioEl.firstChild);
updateRatio(ratioEl.firstChild);
// Reconnect the observer to listen again for changes
observer.observe(ratioEl, { childList: true, subtree: true });
debugLog(`Observer reconnected on element: `, ratioEl);
};
const observer = new MutationObserver(callback);
// Observe both child list changes and character data changes
observer.observe(ratioEl, { childList: true, subtree: true, characterData: true });
debugLog(`Observer initiated on element: `, ratioEl);
}
function updateMenuListRatio() {
const ratio = document.querySelector(".mmUserStats #RatioTD");
ratio.textContent = ratio.textContent.replace("Ratio: ", "");
updateRatio(ratio)
ratio.textContent = `Ratio: ${ratio.textContent}`;
}
function updateForumRatio() {
// the forum page path starts: /f/t/:topidId
if (!window.location.pathname.match(/^\/f\/t\/\d+/)) {
debugLog(`Not in a forum thread, skipping prettification`);
return;
}
// This is pretty fragile because the site doesn't have any unique identifiers for the ratio element and the html isn't semantic
const ratioEls = document.querySelectorAll(".forumAviBox b:nth-of-type(2)");
ratioEls.forEach(el => {
updateRatio(el.nextSibling);
el.nextSibling.textContent = `\u00A0${el.nextSibling.textContent}`; // \u00A0 is a non-breaking space
});
}
function updateUserRatio() {
if (!window.location.pathname.match(/^\/u\/\d+/)) {
debugLog(`Not in a user page, skipping prettification`);
return;
}
// This is pretty fragile because the site doesn't have any unique identifiers for the ratio element and the html isn't semantic
// the html here changes between own user vs other user, so we'll need to look for the "Share ratio" text to find the correct row
const ratioEl = [...document.querySelectorAll(".blockBody table > tbody > tr")].find(el => el.textContent.includes("Share ratio"))?.lastChild?.firstChild;
if (!ratioEl) {
debugLog(`Unable to find user ratio element, skipping prettification`);
return;
}
updateRatio(ratioEl);
}
function updateRatio(el) {
if (!el) {
console.error(`Attempted to update the ratio of a nonexistent element`);
return;
}
const ratioText = el.textContent;
const ratio = parseFloat(ratioText.replaceAll(',', ''));
if (isNaN(ratio)) {
debugLog(`Invalid ratio value '${ratio}', skipping prettification`);
return;
}
const prettyRatio = formatNumber(ratio);
el.textContent = prettyRatio;
debugLog(`Updated ratio from '${ratioText}' to ${prettyRatio}`);
}
function formatNumber(num) {
const formatConfig = {
style: 'decimal',
maximumSignificantDigits: config.maxSignificantDigits,
maximumFractionDigits: config.maxFractionDigits,
}
const roundedNum = parseFloat(num.toPrecision(config.maxSignificantDigits));
const leftSideDigits = Math.floor(roundedNum).toString().length;
// Early return for non-abbreviated numbers
if (leftSideDigits < config.minDigitsForAbbreviation) {
return new Intl.NumberFormat("en", {...formatConfig, roundingPriority: 'morePrecision'}).format(num);
}
// Process for abbreviated numbers
let formattedNumber = roundedNum;
let suffix = '';
for (const [unitSuffix, unitValue] of Object.entries(config.units)) {
if (formattedNumber < unitValue * 1000) {
formattedNumber /= unitValue;
suffix = unitSuffix;
break;
}
}
// Ensure the number doesn't exceed the range of the last unit
if (formattedNumber >= 1000 && suffix === '') {
const lastUnit = Object.keys(config.units).pop();
const lastUnitValue = config.units[lastUnit];
formattedNumber /= lastUnitValue;
suffix = lastUnit;
}
return new Intl.NumberFormat("en", {...formatConfig, roundingPriority: 'lessPrecision'}).format(formattedNumber) + suffix;
}
function debugLog(message, ...args) {
DEBUG && console.debug(`%c${LOG_PREFIX}%c ${message}`, `font-weight: bold`, `font-weight: normal`, ...args);
}