/** @module */
import Item from "./models/item.js";
import { random } from "./random_utils.js";
const CONS_Y = /[BCDFGHIJKLMNPQRSTVWXZbcdfghijklmnpqrstvwxz]y$/;
const FORMAT_PARSER = /\{[^\|}]+\}/g;
const PARENS = /(\{[^\}]*\})/g;
const SMALL_WORDS = /^(a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|the|to|vs?\.?|via)$/i;
const STARTS_WITH_THE = /^[tT]he\s/;
const STARTS_WITH_VOWEL = /^[aeiouAEIOU]/;
const SLICE = Array.prototype.slice;
function basicPluralize(string) {
if (/[x|s]$/.test(string)) {
return string + "es";
} else if (CONS_Y.test(string)) {
return string.substring(0, string.length - 1) + "ies";
}
return string + "s";
}
/**
* Format a string with parameters. There are many ways to supply values to this method:
*
* @example
* format('This {0} a {1}.', ['is', 'test']);
* => "This is a test."
* format('This {0} a {1}.', 'is', 'test');
* => "This is a test."
* format('This {verb} a {noun}.', {verb: 'is', noun: 'test'})
* => "This is a test."
*
* @param template {String} template string
* @param values+ {Object} An array, a set of values, or an object with key/value pairs that
* will be substituted into the template.
* @return {String} the formatted string
*/
export function format(string, obj) {
if (typeof obj == "undefined") {
return string;
}
if (arguments.length > 2 || typeof obj !== "object") {
obj = Array.from(arguments);
string = obj.shift();
}
// Selects {a} sequences with no pipe (these are multiple selection strings, not substitutions)
return string.replace(FORMAT_PARSER, function (token) {
var prop = token.substring(1, token.length - 1);
return typeof obj[prop] == "function" ? obj[prop]() : obj[prop];
});
}
/**
* Convert string to title case. There's a long list of rules for this
* kind of capitalization, see:
*
* *To Title Case 2.1 - http://individed.com/code/to-title-case/<br>
* Copyright 2008-2013 David Gouch. Licensed under the MIT License.*
*
* [0]: http://daringfireball.net/2008/05/title_case
*
* @example
* titleCase('antwerp benedict');
* => "Antwerp Benedict"
* titleCase('antwerp-Benedict');
* => "Antwerp-Benedict"
* titleCase('bead to a small mouth');
* => "Bead to a Small Mouth"
*
* @param string {String} string to title case
* @return {String} in title case
*/
export function titleCase(string) {
return string.replace(/[A-Za-z0-9\u00C0-\u00FF]+[^\s-]*/g, function (match, index, title) {
if (
index > 0 &&
index + match.length !== title.length &&
match.search(SMALL_WORDS) > -1 &&
title.charAt(index - 2) !== ":" &&
(title.charAt(index + match.length) !== "-" || title.charAt(index - 1) === "-") &&
title.charAt(index - 1).search(/[^\s-]/) < 0
) {
return match.toLowerCase();
}
if (match.substr(1).search(/[A-Z]|\../) > -1) {
return match;
}
return match.charAt(0).toUpperCase() + match.substr(1);
});
}
/**
* Format the elements of an array into a list phrase.
*
* @example
* toList(['Apples', 'Bananas', 'Oranges'], (value) => '*'+value);
* => "*Apples, *Bananas, and *Oranges"
*
* @param array {Array} The array to format
* @param [func=identity] {Function} An optional function to format the elements of the array in the returned string.
* @param [join=and] {String} the word to join the last word in the list.
* @param [separator=,] {String} the delimiter to separate items in the list.
* @return {String} the array formatted as a list.
*/
export function toList(array, func = (s) => s.toString(), join = "and", separator = ", ") {
let len = array.length;
if (len === 0) {
return "";
} else if (len === 1) {
return func(array[0]);
} else if (len === 2) {
return `${func(array[0])} ${join} ${func(array[1])}`;
} else {
var arr = array.map(func);
arr[arr.length - 1] = join + " " + arr[arr.length - 1];
return arr.join(separator);
}
}
/**
* Convert a string to sentence case (only the first letter capitalized).
*
* @example
* sentenceCase('antwerp benedict');
* => "Antwerp benedict"
* sentenceCase('antwerp-Benedict');
* => "Antwerp-benedict"
* sentenceCase('bead to a small mouth');
* => "Bead to a small mouth"
*
* @param string {String}
* @return {String} in sentence case
*/
export function sentenceCase(string) {
if (typeof string === "string") {
return string.substring(0, 1).toUpperCase() + string.substring(1);
}
return string;
}
/**
* Pluralizes a string (usually a noun), if the count is greater than one. If
* it's a single item, an indefinite article will be added (see example below
* for cases where it should not be added, "uncountables"). The string should
* note the method of pluralizing the string in curly braces if it is not a
* simple noun that is pluralized using "s", "es" or "aries". For example:
*
* @example
* pluralize('shoe', 3)
* => "3 shoes"
* pluralize('status', 2)
* => "2 statuses"
* pluralize('bag{s} of flour', 1)
* => "a bag of flour"
* pluralize('bag{s} of flour', 2)
* => "2 bags of flour"
* // Note suppression of the indefinite article!
* pluralize('{|suits of }makeshift metal armor')
* => "makeshift metal armor"
* pluralize('{|suits of }makeshift metal armor', 4)
* => "4 suits of makeshift metal armor"
* let item = new Item('quarry');
* pluralize(item, 3)
* => "3 quarries"
*
* @param name {String|Item} A string name following the rules described above, or an Item
* with a name property
* @param [count=1] {Number} The number of these items
* @return {String} the correct singular or plural name
*/
export function pluralize(string, count = 1) {
string = string.name ? string.name : string;
let obj = { singular: "", plural: "" };
let addArticle = string.substring(0, 2) !== "{|";
if (count > 1) {
obj.plural += count + " ";
}
if (string.indexOf("{") === -1) {
obj.singular = string;
obj.plural += basicPluralize(string);
} else {
string.split(PARENS).forEach(function (element, index) {
if (element.indexOf("{") === -1) {
obj.singular += element;
obj.plural += element;
} else if (element.indexOf("|") === -1) {
obj.plural += element.substring(1, element.length - 1);
} else {
let parts = element.substring(1, element.length - 1).split("|");
obj.singular += parts[0];
obj.plural += parts[1];
}
});
}
if (addArticle) {
obj.singular = article(obj.singular);
}
return count === 1 ? obj.singular : obj.plural;
}
export function uncountable(string) {
string = string.name ? string.name : string;
let plural = "";
if (string.indexOf("{") === -1) {
plural += basicPluralize(string);
} else {
string.split(PARENS).forEach((element) => {
if (element.indexOf("{") === -1) {
plural += element;
} else if (element.indexOf("|") === -1) {
plural += element.substring(1, element.length - 1);
} else {
let parts = element.substring(1, element.length - 1).split("|");
plural += parts[1];
}
});
}
return plural;
}
/**
* Put an indefinite article in front of the word based on whether or not it
* starts with a vowel.
*
* @example
* article('walkie talkie')
* => "a walkie talkie"
* article('album')
* => "an album"
*
* @param string {String} String to prefix with an indefinite article
* @return {String} The string with "a" or "an" in front of it.
*/
export function article(string) {
return STARTS_WITH_THE.test(string)
? string
: STARTS_WITH_VOWEL.test(string)
? "an " + string
: "a " + string;
}
/**
* Combines randomization with formatting. First, randomizes the first argument using
* the `random()` function. Then formats the resulting string using the rest of the
* arguments passed in to the method, as described by the `format()` function.
*
* resolve(["Mr. {name}", "Mrs. {name}"], {name: "Smith"});
* => "Mrs. Smith"
*
* @method resolve
*
* @param value {String|Array|Function} A string with optional variants, or an array from
* which to select an element, or a function that returns a value.
* @param [...args] zero or more objects to use in formatting the final string
* @return {String} the randomly selected, formatted string
*/
export function resolve() {
if (arguments.length === 1) {
return random(arguments[0]);
}
var array = SLICE.call(arguments, 0);
array[0] = random(array[0]);
return format.apply(format, array);
}
// Weirdly, you need all this to serialize correctly...
export function toJSON(obj) {
return JSON.stringify(obj, (key, value) => {
if (value instanceof Set) {
return Array.from(value);
} else if (value instanceof Map) {
return Object.fromEntries(value);
}
return value;
});
}
export function parseJSON(json) {
return JSON.parse(json, (key, value) => {
if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/.test(value)) {
return new Date(value);
}
return value;
});
}
export class Builder {
constructor(joiner = "") {
this.buffer = [];
this.joiner = joiner;
}
if(expr, trueCase, falseCase) {
this.#addToBuffer(expr, trueCase, falseCase, "push");
}
preIf(expr, trueCase, falseCase) {
this.#addToBuffer(expr, trueCase, falseCase, "unshift");
}
#addToBuffer(expr, trueCase, falseCase, operation) {
if (expr) {
if (typeof trueCase === "function") {
this.buffer[operation](trueCase());
} else {
this.buffer[operation](trueCase);
}
} else if (falseCase) {
if (typeof falseCase === "function") {
this.buffer[operation](falseCase());
} else {
this.buffer[operation](falseCase);
}
}
}
prepend(output) {
this.buffer.unshift(output);
}
append(output) {
this.buffer.push(output);
}
get length() {
return this.buffer.length;
}
toString() {
return this.buffer.join(this.joiner);
}
}
export function toTag(name) {
return (
pluralize(name.split(";")[0].trim(), 1)
.toLowerCase()
.replaceAll(/&/g, "and")
.replaceAll(/\s/g, "-")
.replaceAll(/^an-/g, "")
.replaceAll(/^a-/g, "")
.replaceAll(/^\w+-of-/g, "")
/*
.replaceAll(/^box-of-/g, "")
.replaceAll(/^jar-of-/g, "")
.replaceAll(/^pair-of-/g, "")
.replaceAll(/^can-of-/g, "")
.replaceAll(/^set-of-/g, "")
.replaceAll(/^roll-of-/g, "")
.replaceAll(/^bottle-of-/g, "")
*/
.replaceAll(/[^a-zA-Z0-9-]/g, "")
);
}
export function traitsToString(traits) {
return Object.keys(traits)
.sort()
.map((key) => `${key} ${traits[key]}`)
.join(", ");
}
/**
* Parses a spec string in this format: "Bag($N tag1 tag2)" into the parameters
* to generate a bag through the newCreateBag method. The components include a total
* value for the bag, and a tag expresion.
*
* @param {String} spec
* @returns {Object} the parameters, incuding “value” and “tags”
*/
export function bagSpecParser(spec) {
if (!spec.startsWith("Bag(") && !spec.startsWith("Stockpile(")) {
return {};
}
spec = spec.match(/\((.*)\)/)[1];
let tags, value;
if (spec.startsWith("$")) {
[value, ...tags] = spec.split(" ");
return {
value: value.substring(1),
totalValue: value.substring(1),
tags: tags.join(" "),
cluster: "high",
};
}
return { tags: spec, cluster: "high" };
}
export function startLowercase(string) {
return string.substring(0, 1).toLowerCase() + string.substring(1);
}