code:
globalThis.cumSlaves = function() {
	return V.slaves.filter(s => (s.assignment === Job.MILKED || s.assignment === Job.DAIRY) && s.balls > 0 && s.ballType === "human");
};
globalThis.fuckSlavesLength = function() {
	return V.slaves.filter(s => [Job.FUCKTOY, Job.MASTERSUITE, Job.CONCUBINE].includes(s.assignment)).length;
};

globalThis.getRieEligibleSlaves = function() {
	return V.slaves.filter(s => assignmentVisible(s) || ["serve in the master suite", "be your Concubine", "work as a servant"].includes(s.assignment) && s.fuckdoll === 0);
};

globalThis.SlaveSort = function() {
	const effectivePreg = (slave) => {
		// slave.preg is only *mostly* usable for sorting
		if (slave.preg > 0 && !slave.pregKnown) {
			// don't reveal unknown pregnancies
			return 0;
		}
		if (slave.pubertyXX === 0 && (slave.ovaries === 1 || slave.mpreg === 1)) {
			// not ovulating yet - sort between barren slaves and slaves on contraceptives
			return -1.2;
		} else if (slave.ovaryAge >= 47 && (slave.ovaries === 1 || slave.mpreg === 1)) {
			// menopausal - sort between barren slaves and slaves on contraceptives
			return -1.1;
		} else if (slave.pregWeek < 0) {
			// postpartum - sort between slaves on contraceptives and fertile slaves
			return -0.1;
		}
		return slave.preg;
	};

	const effectiveEnergy = (slave) => {
		return slave.attrKnown === 1 ? slave.energy : -101;
	};

	const comparators = {
		Aassignment: (a, b) => a.assignment < b.assignment ? -1 : 1,
		Dassignment: (a, b) => a.assignment > b.assignment ? -1 : 1,
		Aname: (a, b) => a.slaveName < b.slaveName ? -1 : 1,
		Dname: (a, b) => a.slaveName > b.slaveName ? -1 : 1,
		Aseniority: (a, b) => b.weekAcquired - a.weekAcquired,
		Dseniority: (a, b) => a.weekAcquired - b.weekAcquired,
		AactualAge: (a, b) => a.actualAge - b.actualAge,
		DactualAge: (a, b) => b.actualAge - a.actualAge,
		AvisualAge: (a, b) => a.visualAge - b.visualAge,
		DvisualAge: (a, b) => b.visualAge - a.visualAge,
		AphysicalAge: (a, b) => a.physicalAge - b.physicalAge,
		DphysicalAge: (a, b) => b.physicalAge - a.physicalAge,
		Adevotion: (a, b) => a.devotion - b.devotion,
		Ddevotion: (a, b) => b.devotion - a.devotion,
		Atrust: (a, b) => a.trust - b.trust,
		Dtrust: (a, b) => b.trust - a.trust,
		AID: (a, b) => a.ID - b.ID,
		DID: (a, b) => b.ID - a.ID,
		AweeklyIncome: (a, b) => a.lastWeeksCashIncome - b.lastWeeksCashIncome,
		DweeklyIncome: (a, b) => b.lastWeeksCashIncome - a.lastWeeksCashIncome,
		Ahealth: (a, b) => a.health.health - b.health.health,
		Dhealth: (a, b) => b.health.health - a.health.health,
		Aweight: (a, b) => a.weight - b.weight,
		Dweight: (a, b) => b.weight - a.weight,
		Amuscles: (a, b) => a.muscles - b.muscles,
		Dmuscles: (a, b) => b.muscles - a.muscles,
		Aintelligence: (a, b) => (a.intelligence + a.intelligenceImplant) - (b.intelligence + b.intelligenceImplant),
		Dintelligence: (a, b) => (b.intelligence + b.intelligenceImplant) - (a.intelligence + a.intelligenceImplant),
		AsexDrive: (a, b) => effectiveEnergy(a) - effectiveEnergy(b),
		DsexDrive: (a, b) => effectiveEnergy(b) - effectiveEnergy(a),
		Apregnancy: (a, b) => effectivePreg(a) - effectivePreg(b),
		Dpregnancy: (a, b) => effectivePreg(b) - effectivePreg(a),
	};

	return {
		slaves: sortSlaves,
		IDs: sortIDs,
		indices: sortIndices
	};

	/** @param {App.Entity.SlaveState[]} [slaves] */
	function sortSlaves(slaves) {
		slaves = slaves || V.slaves;
		slaves.sort(_comparator());
		if (slaves === V.slaves) {
			V.slaveIndices = slaves2indices();
		}
	}

	/** @param {number[]} [slaveIDs] */
	function sortIDs(slaveIDs) {
		const slaves = V.slaves;
		const slaveIndices = V.slaveIndices;
		const cmp = _comparator();
		slaveIDs = slaveIDs || slaves.map(s => s.ID);
		slaveIDs.sort((IDa, IDb) => cmp(slaves[slaveIndices[IDa]], slaves[slaveIndices[IDb]]));
	}

	/** @param {number[]} [slaveIdxs] */
	function sortIndices(slaveIdxs) {
		const slaves = V.slaves;
		const cmp = _comparator();
		slaveIdxs = slaveIdxs || [...slaves.keys()];
		slaveIdxs.sort((ia, ib) => cmp(slaves[ia], slaves[ib]));
	}

	/**
	 * @callback slaveComparator
	 * @param {App.Entity.SlaveState} a
	 * @param {App.Entity.SlaveState} b
	 * @returns {number}
	 */
	/** @returns {slaveComparator} */
	function _comparator() {
		return _makeStableComparator(comparators[(V.sortSlavesOrder === "ascending" ? 'A' : 'D') + V.sortSlavesBy]);
	}

	/** secondary-sort by ascending ID if the primary comparator would return 0 (equal), so we have a guaranteed stable order regardless of input
	 * @param {slaveComparator} comparator
	 * @returns {slaveComparator}
	 */
	function _makeStableComparator(comparator) {
		return function(a, b) {
			return comparator(a, b) || comparators.AID(a, b);
		};
	}
}();

/**
 * @param {App.Entity.SlaveState[]} slaves
 */
globalThis.slaveSortMinor = function(slaves) {
	slaves.sort((a, b) => a.slaveName < b.slaveName ? -1 : 1);
};

/** @typedef {object} getBestSlavesParams
 * @property {string|function(App.Entity.SlaveState): number} part slave object property or custom function
 * @property {number} [count] number of slaves to return
 * @property {boolean} [largest] should it search for the biggest or smallest value
 * @property {function(App.Entity.SlaveState): boolean} [filter] filter out undesired slaves
 */

/**
 * Example:
 * getBestSlaves({part:"butt", count: 5});
 * getBestSlaves({part:"boobs"});//defaults to top 3
 * getBestSlaves({part:"dick", smallest:true, filter:(slave)=>slave.dick > 0});//defaults to top 3
 * getBestSlaves({part:slave=>slave.intelligence+slave.intelligenceImplant});
 * @param {getBestSlavesParams} params
 * @returns {App.Entity.SlaveState[]} sorted from best to worst
 */
globalThis.getBestSlaves = function({part, count = 3, largest = true, filter = (() => true)}) {
	const partCB = _.isFunction(part) ? part : (slave) => slave[part];

	const sortMethod = largest ? (left, right) => right.value - left.value : (left, right) => left.value - right.value;
	return V.slaves.filter(slave => filter(slave))
		.map(slave => ({slave, value: partCB(slave)}))
		.sort(sortMethod)
		.slice(0, count)
		.map(slaveInfo => slaveInfo.slave);
};

/**
 * @param {getBestSlavesParams} info
 * @returns {number[]}
 */
globalThis.getBestSlavesIDs = function(info) {
	return getBestSlaves(info).map(slave => slave.ID);
};

/**
 * @param {App.Entity.SlaveState[]} [slaves]
 * @returns {Object.<number, number>}
 */
globalThis.slaves2indices = function(slaves = V.slaves) {
	return slaves.reduce((acc, slave, i) => {
		acc[slave.ID] = i;
		return acc;
	}, {});
};

/** Calculate various averages for the master suite slaves
 * @returns {{energy: number, milk: number, cum: number, dom: number, sadism: number, dick: number, preg: number}}
 */
App.Utils.masterSuiteAverages = (function() {
	const domMap = {dom: 1, submissive: -1};
	const sadismMap = {sadism: 1, masochism: -1};

	/** Returns either zero or the results of mapping the slave's fetish through an object containing fetish names and result values
	 * @param {App.Entity.SlaveState} s
	 * @param {Object.<string, number>} map
	 * @returns {number}
	 */
	const fetishMapOrZero = (s, map) => map.hasOwnProperty(s.fetish) ? map[s.fetish] : 0;

	return () => {
		const msSlaves = App.Entity.facilities.masterSuite.employees();
		return {
			energy: _.mean(msSlaves.map(s => s.energy)),
			milk: _.mean(msSlaves.map(s => s.lactation * (s.boobs - s.boobsImplant))),
			cum: _.mean(msSlaves.map(s => canAchieveErection(s) ? s.balls : 0)),
			dick: _.mean(msSlaves.map(s => canAchieveErection(s) ? s.dick : 0)),
			preg: _.mean(msSlaves.map(s => s.preg)),
			sadism: _.mean(msSlaves.map(s => (s.fetishStrength * fetishMapOrZero(s, sadismMap)))),
			dom: _.mean(msSlaves.map(s => (s.fetishStrength * fetishMapOrZero(s, domMap))))
		};
	};
})();

/**
 * Updates the globals roomsPopulation and dormitoryPopulation
 * @returns {void}
 */
globalThis.penthouseCensus = function() {
	function occupiesRoom(slave) {
		if (slave.rules.living !== "luxurious") {
			return false; // assigned to dormitory
		} else if (slave.assignment === Job.HEADGIRL && V.HGSuite > 0) {
			return false; // lives in HG suite
		} else if (slave.assignment === Job.BODYGUARD && V.dojo > 0) {
			return false; // lives in dojo
		} else if (slave.relationship >= 4) {
			const partner = getSlave(slave.relationshipTarget);
			if (assignmentVisible(partner) && partner.ID < slave.ID && partner.rules.living === "luxurious") {
				return false; // living with partner, who is already assigned a room (always allocate a room to the partner with the lower ID)
			}
		}
		return true; // takes her own room
	}

	const penthouseSlaves = V.slaves.filter(s => assignmentVisible(s));
	V.roomsPopulation = penthouseSlaves.filter(occupiesRoom).length;
	V.dormitoryPopulation = penthouseSlaves.filter(s => s.rules.living !== "luxurious").length;
};

/**
 * @param {App.Entity.Facilities.Job|App.Entity.Facilities.Facility} jobOrFacility job or facility object
 * @returns {App.Entity.SlaveState[]} array of slaves employed at the job or facility, sorted in accordance to user choice
 */
App.Utils.sortedEmployees = function(jobOrFacility) {
	const employees = jobOrFacility.employees();
	SlaveSort.slaves(employees);
	return employees;
};

/**
 * @param {Array<string|App.Entity.Facilities.Facility>} [facilities]
 * @param {Object.<string, string>} [mapping] Optional mapping for the property names in the result object. Keys
 * are the standard facility names, values are the desired names.
 * @returns {Object.<string, number>}
 */
App.Utils.countFacilityWorkers = function(facilities = null, mapping = {}) {
	facilities = facilities || Object.values(App.Entity.facilities);
	/** @type {App.Entity.Facilities.Facility[]} */
	const fObjects = facilities.map(f => typeof f === "string" ? App.Entity.facilities[f] : f);
	return fObjects.reduce((acc, cur) => {
		acc[mapping[cur.desc.baseName] || cur.desc.baseName] = cur.employeesIDs().size;
		return acc;
	}, {});
};

/**
 * @param {string[]} [assignments] array of assignment strings. The default is to count for all assignments
 * @returns {Object.<string, number>}
 */
App.Utils.countAssignmentWorkers = function(assignments) {
	assignments = assignments || Object.values(Job);
	return assignments.reduce((acc, cur) => {
		acc[cur] = V.JobIDMap[cur].size;
		return acc;
	}, {});
};

repo_name: Elohiem/fc

license: mit


code:
/**
 * Converts numbers to text according to the player's settings.
 * @param {number} x
 * @param {boolean} [printText=false] Whether to return the text form regardless of the user's settings. Mainly used for events.
 * @returns {string} Returns the given value in string form as either an integer (e.g. "10") or word form (e.g."ten").
 */
globalThis.num = function(x, printText = false) {
	const max = V.showNumbersMax;

	const ONE_TO_NINETEEN = [
		"one", "two", "three", "four", "five",
		"six", "seven", "eight", "nine", "ten",
		"eleven", "twelve", "thirteen", "fourteen", "fifteen",
		"sixteen", "seventeen", "eighteen", "nineteen",
	];

	const TENS = [
		"ten", "twenty", "thirty", "forty", "fifty",
		"sixty", "seventy", "eighty", "ninety",
	];

	const SCALES = ["thousand", "million", "billion", "trillion", "quadrillion", "quintillion", "sextillion", "septillion", "octillion", "nonillion", "decillion"];

	/**
	 * helper function for use with Array.filter
	 * @param {any} item
	 * @returns {boolean}
	 */
	function isTruthy(item) {
		return !!item;
	}

	/**
	 * convert a number into "chunks" of 0-999
	 * @param {number} number
	 * @returns {number[]}
	 */
	function chunk(number) {
		const thousands = [];

		while (number > 0) {
			thousands.push(number % 1000);
			number = Math.floor(number / 1000);
		}

		return thousands;
	}

	/**
	 * translate a number from 1-999 into English
	 * @param {number} number
	 * @returns {string}
	 */
	function inEnglish(number) {
		let hundreds;
		let tens;
		let ones;
		const words = [];

		if (number === 0) {
			return "zero";
		}

		if (number < 20) {
			return ONE_TO_NINETEEN[number - 1]; // may be undefined
		}

		if (number < 100) {
			ones = number % 10;
			tens = number / 10 | 0; // equivalent to Math.floor(number / 10)

			words.push(TENS[tens - 1]);

			if (number % 10 !== 0) {
				words.push(inEnglish(ones));
			}

			return words.filter(isTruthy).join("-");
		}

		hundreds = number / 100 | 0;
		words.push(inEnglish(hundreds));
		words.push("hundred");
		words.push(inEnglish(number % 100));

		return words.filter(isTruthy).join(" ");
	}

	if (printText) {
		return inEnglish(x);
	}

	/**
	 * append the word for a scale. Made for use with Array.map
	 * @param {string} chunk
	 * @param {number} exp
	 * @returns {string}
	 */
	function appendScale(chunk, exp) {
		let scale;
		if (!chunk) {
			return null;
		}
		scale = SCALES[exp - 1];
		return [chunk, scale].filter(isTruthy).join(" ");
	}

	if (V.showNumbers === 2) {
		return commaNum(x);
	} else {
		if (x === 0) {
			return "zero";
		}

		if (V.showNumbers === 1 && Math.abs(x) > max) {
			return commaNum(x);
		}

		let numberAsString = chunk(Math.abs(x))
			.map(inEnglish)
			.map(appendScale)
			.filter(isTruthy)
			.reverse()
			.join(" ");

		if (x > 0) {
			return numberAsString;
		} else {
			return `negative ${numberAsString}`;
		}
	}
};

/**
 * Converts the given string to the plural form of the given string. Mainly used as a helper function.
 * @param {string|{single: string, plural: string}} single The singular form.
 * @param {string} [plural] The plural form (e.g. "oxen" for "ox"). Returns the singular form with an "s" if one is not given.
 * @returns {string} Returns the plural form of a given number of items.
 */
globalThis.asPlural = function(single, plural = null) {
	if (typeof single !== 'string') {
		let asObj = single;
		single = asObj.single;
		plural = asObj.plural;
	}
	if (!plural) {
		plural = single + "s";
	}
	return plural;
};

/**
 * Converts the given string to the singular form of the given string. Mainly used as a helper function.
 * @param {string|{single: string}} single The singular form.
 * @returns {string} Returns the singular form of a given number of items.
 */
globalThis.asSingular = function(single) {
	if (typeof single !== 'string') {
		let asObj = single;
		single = asObj.single;
	}
	return single;
};

/**
 * Converts the given number to a string in either singular or plural form.
 * @param {number} number The number to format.
 * @param {string} single The singular form.
 * @param {string} [plural] The plural form (e.g. "oxen" for "ox"). Returns the singular form with an "s" if one is not given.
 * @returns {string} Returns "a _", "less than one _", or the number formatted as either words or numbers, depending on the users' settings.
 */
globalThis.numberWithPlural = function(number, single, plural) {
	if (number === 0) {
		return "no " + asPlural(single, plural);
	} else if (number === 1) {
		return addA(asSingular(single));
	} else if (number.isBetween(0, 1)) {
		return "less than one " + asSingular(single);
	} else {
		return num(number) + " " + asPlural(single, plural);
	}
};

/**
 * Converts the given number to a string in either singular or plural form.
 * @param {number} number The number to format.
 * @param {string} single The singular form.
 * @param {string} [plural] The plural form (e.g. "oxen" for "ox"). Returns the singular form with an "s" if one is not given.
 * @returns {string} Returns "no _", "one _", "less than one _", or the number formatted as either words or numbers, depending on the user's settings.
 */
globalThis.numberWithPluralOne = function(number, single, plural) {
	if (number === 1) {
		return "one " + asSingular(single);
	} else {
		return numberWithPlural(number, single, plural);
	}
};

/**
 * Converts the given number to a string in either singular or plural form.
 * @param {number} number The number to format.
 * @param {string} single The singular form.
 * @param {string} [plural] The plural form (e.g. "oxen" for "ox").
 * @returns {string} Returns "less than one _" instead of "no _" when `number` is 0.
 */
globalThis.numberWithPluralNonZero = function(number, single, plural) {
	if (number === 0) {
		number = 0.1;
	}
	return numberWithPlural(number, single, plural);
};

/**
 * Converts the given number to a string in either singular or plural form.
 * @param {number} number The number to format.
 * @param {string} single The singular form.
 * @param {string} plural The plural form (e.g. "oxen" for "ox").
 * @returns {string} Returns the singular form if `number` is greater than 0 and less than or equal to 1. Otherwise returns the plural form of the number.
 */
globalThis.onlyPlural = function(number, single, plural) {
	if (number > 0 && number <= 1) {
		return asSingular(single);
	}
	return asPlural(single, plural);
};

/**
 * Converts the given number to a string with or without commas, based on the user's settings.
 * @param {number} s The number to format.
 * @returns {string} Returns the number either with or without comma dividers, depending on the user's settings.
 */
globalThis.commaNum = function(s) {
	// Separated from num because some places in code (like long lists, tables) should never have numbers spelled out, but still benefit from commas
	if (!s) {
		return "0";
	}
	if (V.formatNumbers !== 1) {
		return s.toString();
	} else {
		return s.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
	}
};

/**
 * Converts the given number in weeks to a string in a years / months / weeks format.
 * @param {number} weeks The number of weeks to format.
 * @returns {string} Returns "_ years _ months _ weeks".
 */
globalThis.years = function(weeks) {
	let years = 0;
	let quarters = 0; // needed for calc, not user facing
	let months = 0;
	let array = [];

	// A year is always 52 weeks
	// that could be 13 months, but lets say 4 quarters each getting an extra week (13 weeks)

	// Find years
	years = Math.trunc(weeks / 52);

	if (years >= 1) { // Is there at least 1 year
		weeks = weeks - (years * 52); // Find leftover weeks
	}
	if (weeks && weeks / 13 >= 1) { // Is there at least 1 quarter
		quarters = Math.trunc(weeks / 13); // How many quarters?
		weeks = weeks - (quarters * 13); // A quarter contains 13 weeks, how many extra weeks do we have?
	}
	if (weeks && weeks / 4 >= 1) { // Is there at least 1 month
		months = Math.trunc(weeks / 4); // How many months?
		if (months === 3) { // Almost a quarter of a year
			months--; // Quarters have 13 weeks though, so let's be sure the extra is in weeks. Otherwise 51 will return "12 months" instead of "11 months and 4 weeks."
		}
		weeks = weeks - (months * 4); // A month contains 4 weeks, how many extra weeks do we have?
	}

	// So we have years, quarters, months, and weeks.

	// Quarters are useless so:

	months += quarters * 3; // Each quarter has three months.

	if (years) {
		array.push(numberWithPluralOne(years, 'year'));
	}

	if (months) {
		array.push(numberWithPluralOne(months, 'month'));
	}

	if (weeks) {
		array.push(numberWithPluralOne(weeks, 'week'));
	}

	return toSentence(array);
};

/**
 * Converts the given number in weeks to a new Date object.
 * @param {number} [weeks] The week to use. Uses the current week if one is not given.
 * @param {number} [bonusDay] A specific day of the week to represent. 0-indexed.
 * @returns {Date} Returns a new Date object.
 */
globalThis.asDate = function(weeks = null, bonusDay = 0) {
	if (weeks === null) {
		weeks = V.week;
	}
	let d = new Date(2037, 0, 12);
	d.setDate(d.getDate() + weeks * 7 + bonusDay);
	return d;
};

/**
 * Formats the given number in weeks as a string.
 * @param {number} [weeks] The week to use. Uses the current week if one is not given.
 * @param {number} [bonusDay] A specific day of the week to represent. 0-indexed.
 * @returns {string} Returns a string of the number formatted as a date based on the user's locale settings.
 */
globalThis.asDateString = function(weeks = null, bonusDay = 0) {
	return asDate(weeks, bonusDay).toLocaleString(undefined, {year: 'numeric', month: 'long', day: 'numeric'});
};

/**
 * Formats the given number as currency.
 * @param {number} s The number to format.
 * @returns {string} Returns a string of the number formatted as a currency.
 */
globalThis.cashFormat = function(s = 0) {
	if (s < 0) {
		return `-¤${commaNum(Math.abs(s))}`;
	}
	return `¤${commaNum(s)}`;
};

/**
 * Formats the given number as currency.
 *
 * Positive values returns in green, negative values return in red, unless the invert parameter is set.
 * @param {number} s The number to format.
 * @param {boolean} invert Whether or not to invert the numbers (i.e. display positive numbers in red, and negative numbers in green).
 * @returns {string}
 */
globalThis.cashFormatColor = function(s = 0, invert = false) {
	if ((invert && s > 0) || s < 0) {
		// Display in red (WITHOUT a negative sign) if the value is negative, unless invert is true
		return `<span class='cash dec'>${cashFormat(s)}</span>`;
	} else if (s === 0) {
		// White for exactly zero
		return `<span>${cashFormat(s)}</span>`;
	} else {
		// Yellow for positive
		return `<span class='cash inc'>${cashFormat(s)}</span>`;
	}
};

/**
 * Formats the given number as reputation.
 *
 * Positive values returns in green, negative values return in red.
 * @param {number} s The number to format.
 * @returns {string} Returns a given number of reputation as a string.
 */
globalThis.repFormat = function(s = 0) {
	if (V.cheatMode === 1 || V.debugMode === 1) {
		if (s > 0) {
			return `<span class="green">${commaNum(Math.round(s * 100) / 100)} rep</span>`;
		} else if (s < 0) {
			return `<span class="red">${commaNum(Math.round(s * 100) / 100)} rep</span>`;
		} else {
			return `${commaNum(Math.round(s * 100) / 100)} rep`;
		}
	} else {
		/* In order to calculate just how much any one category matters so we can show a "fuzzy" symbolic value to the player, we need to know how "busy" reputation was this week. To calculate this, I ADD income to expenses. Why? 100 - 100 and 10000 - 10000 BOTH are 0, but a +50 event matters a lot more in the first case than the second. I exclude overflow from the calculation because it's not a "real" expense for our purposes, and divide by half just to make percentages a bit easier. */
		let weight = s / (((V.lastWeeksRepIncome.Total - V.lastWeeksRepExpenses.Total) + V.lastWeeksRepExpenses.overflow) / 2);
		if (weight > 0.60) {
			return `<span class="green">+++++ rep</span>`;
		} else if (weight > 0.45) {
			return `<span class="green">++++ rep</span>`;
		} else if (weight > 0.30) {
			return `<span class="green">+++ rep</span>`;
		} else if (weight > 0.15) {
			return `<span class="green">++ rep</span>`;
		} else if (weight > 0.0) {
			return `<span class="green">+ rep</span>`;
		} else if (weight === 0) {
			return "0 rep";
		} else if (weight < -0.60) {
			return `<span class="red">−−−−− rep</span>`;
		} else if (weight < -0.45) {
			return `<span class="red">−−−− rep</span>`;
		} else if (weight < -0.30) {
			return `<span class="red">−−− rep</span>`;
		} else if (weight < -0.15) {
			return `<span class="red">−− rep</span>`;
		} else if (weight < 0) {
			return `<span class="red">− rep</span>`;
		}
	}
};

/**
 * Converts the given number in kg to a string in tons or kg.
 * @param {number} s The number to format. Returns "tons" if greater than 1000, otherwise returns "kg".
 * @returns {string} Returns "_ tons", "1 ton" (or "one ton"), or "_ kg".
 */
globalThis.massFormat = function(s = 0) {
	if (Math.abs(s) >= 1000) {
		s = Math.trunc(s / 1000);
		return `${num(s)} ${s !== 1 ? 'tons' : 'ton'}`;
	} else {
		return `${num(s)} kg`;
	}
};

/**
 * Converts the given number in cm to a string in inches.
 * @param {number} cm The number to format, in cm.
 * @returns {string} Returns "less than an inch", "1 inch" (or "one inch"), or "_ inches".
 */
globalThis.cmToInchString = function(cm) {
	let inches = cm / 2.54;
	if (inches.isBetween(0, 1)) {
		return "less than an inch";
	}
	inches = Math.round(inches);
	if (inches === 1) {
		return `${num(1)} inch`;
	}
	return `${inches} inches`;
};

/**
 * Converts the given number in cm to a string in feet and inches.
 * @param {number} cm The value to convert.
 * @returns {string} Returns "less than an inch", "1 inch" (or "one inch"), "_ inches", or "_'_"".
 */
globalThis.cmToFootInchString = function(cm) {
	if (Math.round(cm / 2.54) < 12) {
		return cmToInchString(cm);
	}
	return `${Math.trunc(Math.round(cm / 2.54) / 12)}'${Math.round(cm / 2.54) % 12}"`;
};

/**
 * Converts the given `.dick` value to a string in inches.
 * @param {number} dick The value to convert.
 * @returns {string} Returns "less than an inch", "1 inch" (or "one inch"), or "_ inches".
 */
globalThis.dickToInchString = function(dick) {
	return cmToInchString(dickToCM(dick));
};

/**
 * Converts the given `.dick` value to a number in cm.
 * @param {number} dick The value to convert.
 * @returns {number} Returns the value in cm.
 */
globalThis.dickToCM = function(dick) {
	if (dick < 9) {
		return dick * 5;
	} else if (dick === 9) {
		return 50;
	}
	return dick * 6;
};

/**
 * Converts the given `.balls` value to a string in inches.
 * @param {number} balls The value to convert.
 * @returns {string} Returns "less than an inch", "1 inch" (or "one inch"), or "_ inches".
 */
globalThis.ballsToInchString = function(balls) {
	return cmToInchString(ballsToCM(balls));
};

/**
 * Converts the given `.balls` value to a number in cm.
 * @param {number} balls The value to convert.
 * @returns {number} Returns the value in cm.
 */
globalThis.ballsToCM = function(balls) {
	if (balls < 2) {
		return 0;
	}
	return (balls < 10 ? (balls - 1) * 2 : balls * 2);
};

/**
 * Converts the given `.dick` value to a string in either cm or inches, depending on the user's settings.
 * @param {number} dick The value to convert.
 * @returns {string} Returns either "_cm (_ inches)", "_ inches", or "_cm".
 */
globalThis.dickToEitherUnit = function(dick) {
	if (V.showInches === 1) {
		return `${dickToCM(dick)}cm (${dickToInchString(dick)})`;
	}
	if (V.showInches === 2) {
		return dickToInchString(dick);
	}
	return `${dickToCM(dick)}cm`;
};

/**
 * Converts the given `.balls` value to a string in either cm or inches, depending on the user's settings.
 * @param {number} balls The value to convert.
 * @returns {string} Returns either "_cm (_ inches)", "_ inches", or "_cm".
 */
globalThis.ballsToEitherUnit = function(balls) {
	if (V.showInches === 1) {
		return `${ballsToCM(balls)}cm (${ballsToInchString(balls)})`;
	}
	if (V.showInches === 2) {
		return ballsToInchString(balls);
	}
	return `${ballsToCM(balls)}cm`;
};

/**
 * Converts the given `.height` value to a string in either cm or inches, depending on the user's settings.
 * @param {number} height The value to convert.
 * @returns {string} Returns either "_cm (_'_")", "_'_"", or "_cm".
 */
globalThis.heightToEitherUnit = function(height) {
	if (V.showInches === 1) {
		return `${height}cm (${cmToFootInchString(height)})`;
	}
	if (V.showInches === 2) {
		return cmToFootInchString(height);
	}
	return `${height}cm`;
};

/**
 * Converts the given value in cm to a string in either cm or inches, depending on the user's settings.
 * @param {number} length The value to convert.
 * @returns {string} Returns either "_cm (_ inches)", "_ inches", or "_cm".
 */
globalThis.lengthToEitherUnit = function(length) {
	if (V.showInches === 1) {
		return `${length}cm (${cmToInchString(length)})`;
	}
	if (V.showInches === 2) {
		return cmToInchString(length);
	}
	return `${length}cm`;
};