From 8cd69d21312464d1869b58852f3de07173d670eb Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Thu, 10 Jul 2025 15:54:35 +0200 Subject: [PATCH 01/54] Initial implementation of optimistic report name computation --- src/CONST/index.ts | 1 + src/libs/API/index.ts | 17 +- src/libs/CustomFormula.ts | 389 ++++++++++++++++++++++++++++++ src/libs/OptimisticReportNames.ts | 289 ++++++++++++++++++++++ src/libs/Permissions.ts | 6 + 5 files changed, 701 insertions(+), 1 deletion(-) create mode 100644 src/libs/CustomFormula.ts create mode 100644 src/libs/OptimisticReportNames.ts diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 1b1f9c43e9fdd..e7d974bb8bd7e 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -628,6 +628,7 @@ const CONST = { BETAS: { ALL: 'all', ASAP_SUBMIT: 'asapSubmit', + AUTH_AUTO_REPORT_TITLES: 'authAutoReportTitles', DEFAULT_ROOMS: 'defaultRooms', P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', SPOTNANA_TRAVEL: 'spotnanaTravel', diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 089c1452ccb46..0f8530fc467f3 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -7,6 +7,7 @@ import Log from '@libs/Log'; import {handleDeletedAccount, HandleUnusedOptimisticID, Logging, Pagination, Reauthentication, RecheckConnection, SaveResponseInOnyx} from '@libs/Middleware'; import {isOffline} from '@libs/Network/NetworkStore'; import {push as pushToSequentialQueue, waitForIdle as waitForSequentialQueueIdle} from '@libs/Network/SequentialQueue'; +import * as OptimisticReportNames from '@libs/OptimisticReportNames'; import Pusher from '@libs/Pusher'; import {processWithMiddleware, use} from '@libs/Request'; import {getAll, getLength as getPersistedRequestsLength} from '@userActions/PersistedRequests'; @@ -74,7 +75,21 @@ function prepareRequest( const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData; if (optimisticData && shouldApplyOptimisticData) { Log.info('[API] Applying optimistic data', false, {command, type}); - Onyx.update(optimisticData); + + // Process optimistic data through report name middleware + OptimisticReportNames.createUpdateContext() + .then((context) => { + const processedOptimisticData = OptimisticReportNames.updateOptimisticReportNamesFromUpdates( + optimisticData, + context, + ); + Onyx.update(processedOptimisticData); + }) + .catch((error) => { + Log.warn('[API] Failed to process optimistic report names', {error}); + // Fallback to original optimistic data if processing fails + Onyx.update(optimisticData); + }); } const isWriteRequest = type === CONST.API_REQUEST_TYPE.WRITE; diff --git a/src/libs/CustomFormula.ts b/src/libs/CustomFormula.ts new file mode 100644 index 0000000000000..7b584b5c041ee --- /dev/null +++ b/src/libs/CustomFormula.ts @@ -0,0 +1,389 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import CONST from '@src/CONST'; +import type Policy from '@src/types/onyx/Policy'; +import type Report from '@src/types/onyx/Report'; + +type FormulaPart = { + /** The original definition from the formula */ + definition: string; + /** The type of formula part (report, field, user, etc.) */ + type: string; + /** The field path for accessing data (e.g., ['type'], ['startdate'], ['total']) */ + fieldPath: string[]; + /** Functions to apply to the computed value (e.g., ['frontPart']) */ + functions: string[]; +}; + +type FormulaContext = { + report: Report; + policy: OnyxEntry; +}; + +const FORMULA_PART_TYPES = { + REPORT: 'report', + FIELD: 'field', + USER: 'user', + FREETEXT: 'freetext', +} as const; + +/** + * Extract formula parts from a formula string, handling nested braces and escapes + * Based on OldDot Formula.extract method + */ +function extract(formula: string, opener = '{', closer = '}'): string[] { + if (!formula || typeof formula !== 'string') { + return []; + } + + const letters = formula.split(''); + const sections: string[] = []; + let nesting = 0; + let start = 0; + + for (let i = 0; i < letters.length; i++) { + // Found an escape character, skip the next character + if (letters[i] === '\\') { + i++; + continue; + } + + // Found an opener, save the spot + if (letters[i] === opener) { + if (nesting === 0) { + start = i; + } + nesting++; + } + + // Found a closer, decrement the nesting and possibly extract it + if (letters[i] === closer && nesting > 0) { + nesting--; + if (nesting === 0) { + sections.push(formula.substring(start, i + 1)); + } + } + } + + return sections; +} + +/** + * Parse a formula string into an array of formula parts + * Based on OldDot Formula.parse method + */ +function parse(formula: string): FormulaPart[] { + if (!formula || typeof formula !== 'string') { + return []; + } + + const parts: FormulaPart[] = []; + const formulaParts = extract(formula); + + // If no formula parts found, treat the entire string as free text + if (formulaParts.length === 0) { + if (formula.trim()) { + parts.push({ + definition: formula, + type: FORMULA_PART_TYPES.FREETEXT, + fieldPath: [], + functions: [], + }); + } + return parts; + } + + // Process the formula by splitting on formula parts to preserve free text + let remainingFormula = formula; + let lastIndex = 0; + + formulaParts.forEach((part) => { + const partIndex = remainingFormula.indexOf(part, lastIndex); + + // Add any free text before this formula part + if (partIndex > lastIndex) { + const freeText = remainingFormula.substring(lastIndex, partIndex); + if (freeText.trim()) { + parts.push({ + definition: freeText, + type: FORMULA_PART_TYPES.FREETEXT, + fieldPath: [], + functions: [], + }); + } + } + + // Add the formula part + parts.push(parsePart(part)); + lastIndex = partIndex + part.length; + }); + + // Add any remaining free text after the last formula part + if (lastIndex < remainingFormula.length) { + const freeText = remainingFormula.substring(lastIndex); + if (freeText.trim()) { + parts.push({ + definition: freeText, + type: FORMULA_PART_TYPES.FREETEXT, + fieldPath: [], + functions: [], + }); + } + } + + return parts; +} + +/** + * Parse a single formula part definition into a FormulaPart object + * Based on OldDot Formula.parsePart method + */ +function parsePart(definition: string): FormulaPart { + const part: FormulaPart = { + definition, + type: FORMULA_PART_TYPES.FREETEXT, + fieldPath: [], + functions: [], + }; + + // If it doesn't start and end with braces, it's free text + if (!definition.startsWith('{') || !definition.endsWith('}')) { + return part; + } + + // Remove the braces and trim + const cleanDefinition = definition.slice(1, -1).trim(); + if (!cleanDefinition) { + return part; + } + + // Split on | to separate functions + const segments = cleanDefinition.split('|'); + const fieldSegment = segments[0]; + const functions = segments.slice(1); + + // Split the field segment on : to get the field path + const fieldPath = fieldSegment.split(':'); + const type = fieldPath[0]?.toLowerCase(); + + // Determine the formula part type + if (type === 'report') { + part.type = FORMULA_PART_TYPES.REPORT; + } else if (type === 'field') { + part.type = FORMULA_PART_TYPES.FIELD; + } else if (type === 'user') { + part.type = FORMULA_PART_TYPES.USER; + } + + // Set field path (excluding the type) + part.fieldPath = fieldPath.slice(1); + part.functions = functions; + + return part; +} + +/** + * Compute the value of a formula given a context + */ +function compute(formula: string, context: FormulaContext): string { + if (!formula || typeof formula !== 'string') { + return ''; + } + + const parts = parse(formula); + let result = ''; + + for (const part of parts) { + let value = ''; + + switch (part.type) { + case FORMULA_PART_TYPES.REPORT: + value = computeReportPart(part, context); + break; + case FORMULA_PART_TYPES.FIELD: + value = computeFieldPart(part, context); + break; + case FORMULA_PART_TYPES.USER: + value = computeUserPart(part, context); + break; + case FORMULA_PART_TYPES.FREETEXT: + value = part.definition; + break; + default: + // If we don't recognize the part type, use the original definition + value = part.definition; + } + + // Apply any functions to the computed value + value = applyFunctions(value, part.functions); + result += value; + } + + return result; +} + +/** + * Compute the value of a report formula part + */ +function computeReportPart(part: FormulaPart, context: FormulaContext): string { + const {report, policy} = context; + const [field] = part.fieldPath; + + if (!field) { + return part.definition; + } + + switch (field.toLowerCase()) { + case 'type': + return 'Expense Report'; // Default report type for now + case 'startdate': + return formatDate(report.lastVisibleActionCreated); + case 'total': + return formatAmount(report.total, report.currency); + case 'currency': + return report.currency ?? ''; + case 'policyname': + case 'workspacename': + return policy?.name ?? ''; + case 'created': + return formatDate(report.lastVisibleActionCreated, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING); + default: + return part.definition; + } +} + +/** + * Compute the value of a field formula part + */ +function computeFieldPart(part: FormulaPart, context: FormulaContext): string { + // Field computation will be implemented later + return part.definition; +} + +/** + * Compute the value of a user formula part + */ +function computeUserPart(part: FormulaPart, context: FormulaContext): string { + // User computation will be implemented later + return part.definition; +} + +/** + * Apply functions to a computed value + */ +function applyFunctions(value: string, functions: string[]): string { + let result = value; + + for (const func of functions) { + const [functionName, ...args] = func.split(':'); + + switch (functionName.toLowerCase()) { + case 'frontpart': + result = getFrontPart(result); + break; + case 'substr': + result = getSubstring(result, args); + break; + default: + // Unknown function, leave value as is + break; + } + } + + return result; +} + +/** + * Get the front part of an email or first word of a string + */ +function getFrontPart(value: string): string { + const trimmed = value.trim(); + + // If it's an email, return the part before @ + if (trimmed.includes('@')) { + return trimmed.split('@')[0]; + } + + // Otherwise, return the first word + return trimmed.split(' ')[0]; +} + +/** + * Get substring of a value + */ +function getSubstring(value: string, args: string[]): string { + const start = parseInt(args[0], 10) || 0; + const length = args[1] ? parseInt(args[1], 10) : undefined; + + if (length !== undefined) { + return value.substr(start, length); + } + + return value.substr(start); +} + +/** + * Format a date value + */ +function formatDate(dateString: string | undefined, format = CONST.DATE.FNS_FORMAT_STRING): string { + if (!dateString) { + return ''; + } + + try { + const date = new Date(dateString); + if (isNaN(date.getTime())) { + return ''; + } + + // Simple date formatting - this could be enhanced with a proper date library + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + if (format === CONST.DATE.FNS_FORMAT_STRING) { + return `${month}/${day}/${year}`; + } + + return `${year}-${month}-${day}`; + } catch { + return ''; + } +} + +/** + * Format an amount value + */ +function formatAmount(amount: number | undefined, currency: string | undefined): string { + if (amount === undefined) { + return ''; + } + + const absoluteAmount = Math.abs(amount); + const formattedAmount = (absoluteAmount / 100).toFixed(2); + + if (currency) { + return `${currency} ${formattedAmount}`; + } + + return formattedAmount; +} + +/** + * Check if a string contains formula parts + */ +function isFormula(str: string): boolean { + return extract(str).length > 0; +} + +export { + extract, + parse, + compute, + isFormula, + FORMULA_PART_TYPES, +}; + +export type { + FormulaPart, + FormulaContext, +}; \ No newline at end of file diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts new file mode 100644 index 0000000000000..44f8e68dbe6ad --- /dev/null +++ b/src/libs/OptimisticReportNames.ts @@ -0,0 +1,289 @@ +import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import * as CustomFormula from '@libs/CustomFormula'; +import Permissions from '@libs/Permissions'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type Beta from '@src/types/onyx/Beta'; +import type Policy from '@src/types/onyx/Policy'; +import type Report from '@src/types/onyx/Report'; + +type UpdateContext = { + betas: OnyxEntry; + allReports: Record; + allPolicies: Record; +}; + +/** + * Get the object type from an Onyx key + */ +function determineObjectTypeByKey(key: string): 'report' | 'policy' | 'transaction' | 'unknown' { + if (key.startsWith(ONYXKEYS.COLLECTION.REPORT)) { + return 'report'; + } + if (key.startsWith(ONYXKEYS.COLLECTION.POLICY)) { + return 'policy'; + } + if (key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) { + return 'transaction'; + } + return 'unknown'; +} + +/** + * Extract report ID from an Onyx key + */ +function getReportIDFromKey(key: string): string { + return key.replace(ONYXKEYS.COLLECTION.REPORT, ''); +} + +/** + * Extract policy ID from an Onyx key + */ +function getPolicyIDFromKey(key: string): string { + return key.replace(ONYXKEYS.COLLECTION.POLICY, ''); +} + +/** + * Extract transaction ID from an Onyx key + */ +function getTransactionIDFromKey(key: string): string { + return key.replace(ONYXKEYS.COLLECTION.TRANSACTION, ''); +} + +/** + * Get report by ID from the reports collection + */ +function getReportByID(reportID: string, allReports: Record): Report | undefined { + return allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; +} + +/** + * Get policy by ID from the policies collection + */ +function getPolicyByID(policyID: string, allPolicies: Record): Policy | undefined { + return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; +} + +/** + * Get all reports associated with a policy ID + */ +function getReportsByPolicyID(policyID: string, allReports: Record): Report[] { + return Object.values(allReports).filter((report) => report?.policyID === policyID); +} + +/** + * Get the report associated with a transaction ID + */ +function getReportByTransactionID(transactionID: string, allReports: Record): Report | undefined { + // This is a simplified version - in reality, we'd need to look up the transaction + // and get its reportID, but for now we'll return undefined + // TODO: Implement proper transaction -> report lookup + return undefined; +} + +/** + * Generate the Onyx key for a report + */ +function getReportKey(reportID: string): string { + return `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; +} + +/** + * Check if a report should have its name automatically computed + */ +function shouldComputeReportName(report: Report, policy: Policy | undefined): boolean { + // Only compute names for expense reports with policies that have title fields + if (!report || !policy) { + return false; + } + + // Check if the report is an expense report + if (!ReportUtils.isExpenseReport(report)) { + return false; + } + + // Check if the policy has a title field with a formula + const titleField = ReportUtils.getTitleReportField(policy.fieldList ?? {}); + if (!titleField?.defaultValue) { + return false; + } + + // Check if the formula contains formula parts + return CustomFormula.isFormula(titleField.defaultValue); +} + +/** + * Compute a new report name if needed based on an optimistic update + */ +function computeReportNameIfNeeded(report: Report, incomingUpdate: OnyxUpdate, context: UpdateContext): {reportName: string} | null { + console.log('morwa: computeReportNameIfNeeded called', {reportID: report.reportID, updateKey: incomingUpdate.key}); + + const {allPolicies} = context; + + const policy = getPolicyByID(report.policyID ?? '', allPolicies); + if (!shouldComputeReportName(report, policy)) { + return null; + } + + const titleField = ReportUtils.getTitleReportField(policy?.fieldList ?? {}); + if (!titleField?.defaultValue) { + return null; + } + + // Quick check: see if the update might affect the report name + const updateType = determineObjectTypeByKey(incomingUpdate.key); + const formula = titleField.defaultValue; + const formulaParts = CustomFormula.parse(formula); + + // Check if any formula part might be affected by this update + const isAffected = formulaParts.some((part) => { + if (part.type === CustomFormula.FORMULA_PART_TYPES.REPORT) { + return updateType === 'report' || updateType === 'transaction'; + } + if (part.type === CustomFormula.FORMULA_PART_TYPES.FIELD) { + return updateType === 'report'; + } + return false; + }); + + if (!isAffected) { + return null; + } + + // Build context with the updated data + const updatedReport = updateType === 'report' && report.reportID === getReportIDFromKey(incomingUpdate.key) ? {...report, ...incomingUpdate.value} : report; + + const updatedPolicy = updateType === 'policy' && report.policyID === getPolicyIDFromKey(incomingUpdate.key) ? {...policy, ...incomingUpdate.value} : policy; + + // Compute the new name + const formulaContext: CustomFormula.FormulaContext = { + report: updatedReport, + policy: updatedPolicy, + }; + + const newName = CustomFormula.compute(formula, formulaContext); + + // Only return an update if the name actually changed + if (newName && newName !== report.reportName) { + console.log('morwa: Report name computed', { + reportID: report.reportID, + oldName: report.reportName, + newName, + formula, + }); + return {reportName: newName}; + } + + return null; +} + +/** + * Update optimistic report names based on incoming updates + * This is the main middleware function that processes optimistic data + */ +function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: UpdateContext): OnyxUpdate[] { + console.log('morwa: updateOptimisticReportNamesFromUpdates called with', updates.length, 'updates', updates, context); + const {betas, allReports} = context; + + // Check if the feature is enabled + if (false && !Permissions.canUseAuthAutoReportTitles(betas)) { + return updates; + } + + const additionalUpdates: OnyxUpdate[] = []; + + for (const update of updates) { + const objectType = determineObjectTypeByKey(update.key); + let affectedReports: Report[] = []; + + switch (objectType) { + case 'report': { + const reportID = getReportIDFromKey(update.key); + const report = getReportByID(reportID, allReports); + if (report) { + affectedReports = [report]; + } + break; + } + + case 'policy': { + const policyID = getPolicyIDFromKey(update.key); + affectedReports = getReportsByPolicyID(policyID, allReports); + break; + } + + case 'transaction': { + const transactionID = getTransactionIDFromKey(update.key); + const report = getReportByTransactionID(transactionID, allReports); + if (report) { + affectedReports = [report]; + } + break; + } + + default: + continue; + } + + for (const report of affectedReports) { + const reportNameUpdate = computeReportNameIfNeeded(report, update, context); + + if (reportNameUpdate) { + additionalUpdates.push({ + key: getReportKey(report.reportID), + onyxMethod: Onyx.METHOD.MERGE, + value: reportNameUpdate, + }); + } + } + } + + console.log('morwa: Generated', additionalUpdates.length, 'additional report name updates', additionalUpdates); + return updates.concat(additionalUpdates); +} + +/** + * Initialize the context needed for report name computation + * This should be called before processing optimistic updates + */ +function createUpdateContext(): Promise { + return new Promise((resolve) => { + // Get all the data we need from Onyx + const connectionID = Onyx.connect({ + key: ONYXKEYS.BETAS, + callback: (betas) => { + Onyx.disconnect(connectionID); + + // Also get reports and policies + const reportsConnectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(reportsConnectionID); + + const policiesConnectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY, + waitForCollectionCallback: true, + callback: (allPolicies) => { + Onyx.disconnect(policiesConnectionID); + + resolve({ + betas, + allReports: allReports ?? {}, + allPolicies: allPolicies ?? {}, + }); + }, + }); + }, + }); + }, + }); + }); +} + +export {updateOptimisticReportNamesFromUpdates, computeReportNameIfNeeded, createUpdateContext, shouldComputeReportName}; + +export type {UpdateContext}; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 408df3326275e..0e970f3c0a2be 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -7,6 +7,11 @@ function canUseAllBetas(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.ALL); } +// eslint-disable-next-line rulesdir/no-beta-handler +function canUseAuthAutoReportTitles(betas: OnyxEntry): boolean { + return isBetaEnabled(CONST.BETAS.AUTH_AUTO_REPORT_TITLES, betas); +} + // eslint-disable-next-line rulesdir/no-beta-handler function isBlockedFromSpotnanaTravel(betas: OnyxEntry): boolean { // Don't check for all betas or nobody can use test travel on dev @@ -30,6 +35,7 @@ function isBetaEnabled(beta: Beta, betas: OnyxEntry): boolean { } export default { + canUseAuthAutoReportTitles, canUseLinkPreviews, isBlockedFromSpotnanaTravel, isBetaEnabled, From 08b84d385b7e1ae62641f256d1c3f757b7582b8f Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 11 Jul 2025 12:50:46 +0200 Subject: [PATCH 02/54] Introduce tests for Optimistic Report Names. Add Logging and Perfrormance tracking --- .../javascript/checkAndroidStatus/index.js | 8488 ++++++++--------- src/CONST/index.ts | 3 + src/libs/OptimisticReportNames.ts | 125 +- 3 files changed, 4367 insertions(+), 4249 deletions(-) diff --git a/.github/actions/javascript/checkAndroidStatus/index.js b/.github/actions/javascript/checkAndroidStatus/index.js index 143802767ebdc..0ac35cf3df529 100644 --- a/.github/actions/javascript/checkAndroidStatus/index.js +++ b/.github/actions/javascript/checkAndroidStatus/index.js @@ -559,8 +559,8 @@ class OidcClient { const res = yield httpclient .getJson(id_token_url) .catch(error => { - throw new Error(`Failed to get ID Token. \n - Error Code : ${error.statusCode}\n + throw new Error(`Failed to get ID Token. \n + Error Code : ${error.statusCode}\n Error Message: ${error.result.message}`); }); const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value; @@ -5143,2928 +5143,2928 @@ function removeHook(state, name, method) { /***/ 87558: /***/ (function(module) { -;(function (globalObject) { - 'use strict'; - -/* - * bignumber.js v9.1.2 - * A JavaScript library for arbitrary-precision arithmetic. - * https://github.com/MikeMcl/bignumber.js - * Copyright (c) 2022 Michael Mclaughlin - * MIT Licensed. - * - * BigNumber.prototype methods | BigNumber methods - * | - * absoluteValue abs | clone - * comparedTo | config set - * decimalPlaces dp | DECIMAL_PLACES - * dividedBy div | ROUNDING_MODE - * dividedToIntegerBy idiv | EXPONENTIAL_AT - * exponentiatedBy pow | RANGE - * integerValue | CRYPTO - * isEqualTo eq | MODULO_MODE - * isFinite | POW_PRECISION - * isGreaterThan gt | FORMAT - * isGreaterThanOrEqualTo gte | ALPHABET - * isInteger | isBigNumber - * isLessThan lt | maximum max - * isLessThanOrEqualTo lte | minimum min - * isNaN | random - * isNegative | sum - * isPositive | - * isZero | - * minus | - * modulo mod | - * multipliedBy times | - * negated | - * plus | - * precision sd | - * shiftedBy | - * squareRoot sqrt | - * toExponential | - * toFixed | - * toFormat | - * toFraction | - * toJSON | - * toNumber | - * toPrecision | - * toString | - * valueOf | - * - */ - - - var BigNumber, - isNumeric = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i, - mathceil = Math.ceil, - mathfloor = Math.floor, - - bignumberError = '[BigNumber Error] ', - tooManyDigits = bignumberError + 'Number primitive has more than 15 significant digits: ', - - BASE = 1e14, - LOG_BASE = 14, - MAX_SAFE_INTEGER = 0x1fffffffffffff, // 2^53 - 1 - // MAX_INT32 = 0x7fffffff, // 2^31 - 1 - POWS_TEN = [1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13], - SQRT_BASE = 1e7, - - // EDITABLE - // The limit on the value of DECIMAL_PLACES, TO_EXP_NEG, TO_EXP_POS, MIN_EXP, MAX_EXP, and - // the arguments to toExponential, toFixed, toFormat, and toPrecision. - MAX = 1E9; // 0 to MAX_INT32 - - - /* - * Create and return a BigNumber constructor. - */ - function clone(configObject) { - var div, convertBase, parseNumeric, - P = BigNumber.prototype = { constructor: BigNumber, toString: null, valueOf: null }, - ONE = new BigNumber(1), - - - //----------------------------- EDITABLE CONFIG DEFAULTS ------------------------------- - - - // The default values below must be integers within the inclusive ranges stated. - // The values can also be changed at run-time using BigNumber.set. - - // The maximum number of decimal places for operations involving division. - DECIMAL_PLACES = 20, // 0 to MAX - - // The rounding mode used when rounding to the above decimal places, and when using - // toExponential, toFixed, toFormat and toPrecision, and round (default value). - // UP 0 Away from zero. - // DOWN 1 Towards zero. - // CEIL 2 Towards +Infinity. - // FLOOR 3 Towards -Infinity. - // HALF_UP 4 Towards nearest neighbour. If equidistant, up. - // HALF_DOWN 5 Towards nearest neighbour. If equidistant, down. - // HALF_EVEN 6 Towards nearest neighbour. If equidistant, towards even neighbour. - // HALF_CEIL 7 Towards nearest neighbour. If equidistant, towards +Infinity. - // HALF_FLOOR 8 Towards nearest neighbour. If equidistant, towards -Infinity. - ROUNDING_MODE = 4, // 0 to 8 - - // EXPONENTIAL_AT : [TO_EXP_NEG , TO_EXP_POS] - - // The exponent value at and beneath which toString returns exponential notation. - // Number type: -7 - TO_EXP_NEG = -7, // 0 to -MAX - - // The exponent value at and above which toString returns exponential notation. - // Number type: 21 - TO_EXP_POS = 21, // 0 to MAX - - // RANGE : [MIN_EXP, MAX_EXP] - - // The minimum exponent value, beneath which underflow to zero occurs. - // Number type: -324 (5e-324) - MIN_EXP = -1e7, // -1 to -MAX - - // The maximum exponent value, above which overflow to Infinity occurs. - // Number type: 308 (1.7976931348623157e+308) - // For MAX_EXP > 1e7, e.g. new BigNumber('1e100000000').plus(1) may be slow. - MAX_EXP = 1e7, // 1 to MAX - - // Whether to use cryptographically-secure random number generation, if available. - CRYPTO = false, // true or false - - // The modulo mode used when calculating the modulus: a mod n. - // The quotient (q = a / n) is calculated according to the corresponding rounding mode. - // The remainder (r) is calculated as: r = a - n * q. - // - // UP 0 The remainder is positive if the dividend is negative, else is negative. - // DOWN 1 The remainder has the same sign as the dividend. - // This modulo mode is commonly known as 'truncated division' and is - // equivalent to (a % n) in JavaScript. - // FLOOR 3 The remainder has the same sign as the divisor (Python %). - // HALF_EVEN 6 This modulo mode implements the IEEE 754 remainder function. - // EUCLID 9 Euclidian division. q = sign(n) * floor(a / abs(n)). - // The remainder is always positive. - // - // The truncated division, floored division, Euclidian division and IEEE 754 remainder - // modes are commonly used for the modulus operation. - // Although the other rounding modes can also be used, they may not give useful results. - MODULO_MODE = 1, // 0 to 9 - - // The maximum number of significant digits of the result of the exponentiatedBy operation. - // If POW_PRECISION is 0, there will be unlimited significant digits. - POW_PRECISION = 0, // 0 to MAX - - // The format specification used by the BigNumber.prototype.toFormat method. - FORMAT = { - prefix: '', - groupSize: 3, - secondaryGroupSize: 0, - groupSeparator: ',', - decimalSeparator: '.', - fractionGroupSize: 0, - fractionGroupSeparator: '\xA0', // non-breaking space - suffix: '' - }, - - // The alphabet used for base conversion. It must be at least 2 characters long, with no '+', - // '-', '.', whitespace, or repeated character. - // '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_' - ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz', - alphabetHasNormalDecimalDigits = true; - - - //------------------------------------------------------------------------------------------ - - - // CONSTRUCTOR - - - /* - * The BigNumber constructor and exported function. - * Create and return a new instance of a BigNumber object. - * - * v {number|string|BigNumber} A numeric value. - * [b] {number} The base of v. Integer, 2 to ALPHABET.length inclusive. - */ - function BigNumber(v, b) { - var alphabet, c, caseChanged, e, i, isNum, len, str, - x = this; - - // Enable constructor call without `new`. - if (!(x instanceof BigNumber)) return new BigNumber(v, b); - - if (b == null) { - - if (v && v._isBigNumber === true) { - x.s = v.s; - - if (!v.c || v.e > MAX_EXP) { - x.c = x.e = null; - } else if (v.e < MIN_EXP) { - x.c = [x.e = 0]; - } else { - x.e = v.e; - x.c = v.c.slice(); - } - - return; - } - - if ((isNum = typeof v == 'number') && v * 0 == 0) { - - // Use `1 / n` to handle minus zero also. - x.s = 1 / v < 0 ? (v = -v, -1) : 1; - - // Fast path for integers, where n < 2147483648 (2**31). - if (v === ~~v) { - for (e = 0, i = v; i >= 10; i /= 10, e++); - - if (e > MAX_EXP) { - x.c = x.e = null; - } else { - x.e = e; - x.c = [v]; - } - - return; - } - - str = String(v); - } else { - - if (!isNumeric.test(str = String(v))) return parseNumeric(x, str, isNum); - - x.s = str.charCodeAt(0) == 45 ? (str = str.slice(1), -1) : 1; - } - - // Decimal point? - if ((e = str.indexOf('.')) > -1) str = str.replace('.', ''); - - // Exponential form? - if ((i = str.search(/e/i)) > 0) { - - // Determine exponent. - if (e < 0) e = i; - e += +str.slice(i + 1); - str = str.substring(0, i); - } else if (e < 0) { - - // Integer. - e = str.length; - } - - } else { - - // '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}' - intCheck(b, 2, ALPHABET.length, 'Base'); - - // Allow exponential notation to be used with base 10 argument, while - // also rounding to DECIMAL_PLACES as with other bases. - if (b == 10 && alphabetHasNormalDecimalDigits) { - x = new BigNumber(v); - return round(x, DECIMAL_PLACES + x.e + 1, ROUNDING_MODE); - } - - str = String(v); - - if (isNum = typeof v == 'number') { - - // Avoid potential interpretation of Infinity and NaN as base 44+ values. - if (v * 0 != 0) return parseNumeric(x, str, isNum, b); - - x.s = 1 / v < 0 ? (str = str.slice(1), -1) : 1; - - // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}' - if (BigNumber.DEBUG && str.replace(/^0\.0*|\./, '').length > 15) { - throw Error - (tooManyDigits + v); - } - } else { - x.s = str.charCodeAt(0) === 45 ? (str = str.slice(1), -1) : 1; - } - - alphabet = ALPHABET.slice(0, b); - e = i = 0; - - // Check that str is a valid base b number. - // Don't use RegExp, so alphabet can contain special characters. - for (len = str.length; i < len; i++) { - if (alphabet.indexOf(c = str.charAt(i)) < 0) { - if (c == '.') { - - // If '.' is not the first character and it has not be found before. - if (i > e) { - e = len; - continue; - } - } else if (!caseChanged) { - - // Allow e.g. hexadecimal 'FF' as well as 'ff'. - if (str == str.toUpperCase() && (str = str.toLowerCase()) || - str == str.toLowerCase() && (str = str.toUpperCase())) { - caseChanged = true; - i = -1; - e = 0; - continue; - } - } - - return parseNumeric(x, String(v), isNum, b); - } - } - - // Prevent later check for length on converted number. - isNum = false; - str = convertBase(str, b, 10, x.s); - - // Decimal point? - if ((e = str.indexOf('.')) > -1) str = str.replace('.', ''); - else e = str.length; - } - - // Determine leading zeros. - for (i = 0; str.charCodeAt(i) === 48; i++); - - // Determine trailing zeros. - for (len = str.length; str.charCodeAt(--len) === 48;); - - if (str = str.slice(i, ++len)) { - len -= i; - - // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}' - if (isNum && BigNumber.DEBUG && - len > 15 && (v > MAX_SAFE_INTEGER || v !== mathfloor(v))) { - throw Error - (tooManyDigits + (x.s * v)); - } - - // Overflow? - if ((e = e - i - 1) > MAX_EXP) { - - // Infinity. - x.c = x.e = null; - - // Underflow? - } else if (e < MIN_EXP) { - - // Zero. - x.c = [x.e = 0]; - } else { - x.e = e; - x.c = []; - - // Transform base - - // e is the base 10 exponent. - // i is where to slice str to get the first element of the coefficient array. - i = (e + 1) % LOG_BASE; - if (e < 0) i += LOG_BASE; // i < 1 - - if (i < len) { - if (i) x.c.push(+str.slice(0, i)); - - for (len -= LOG_BASE; i < len;) { - x.c.push(+str.slice(i, i += LOG_BASE)); - } - - i = LOG_BASE - (str = str.slice(i)).length; - } else { - i -= len; - } - - for (; i--; str += '0'); - x.c.push(+str); - } - } else { - - // Zero. - x.c = [x.e = 0]; - } - } - - - // CONSTRUCTOR PROPERTIES - - - BigNumber.clone = clone; - - BigNumber.ROUND_UP = 0; - BigNumber.ROUND_DOWN = 1; - BigNumber.ROUND_CEIL = 2; - BigNumber.ROUND_FLOOR = 3; - BigNumber.ROUND_HALF_UP = 4; - BigNumber.ROUND_HALF_DOWN = 5; - BigNumber.ROUND_HALF_EVEN = 6; - BigNumber.ROUND_HALF_CEIL = 7; - BigNumber.ROUND_HALF_FLOOR = 8; - BigNumber.EUCLID = 9; - - - /* - * Configure infrequently-changing library-wide settings. - * - * Accept an object with the following optional properties (if the value of a property is - * a number, it must be an integer within the inclusive range stated): - * - * DECIMAL_PLACES {number} 0 to MAX - * ROUNDING_MODE {number} 0 to 8 - * EXPONENTIAL_AT {number|number[]} -MAX to MAX or [-MAX to 0, 0 to MAX] - * RANGE {number|number[]} -MAX to MAX (not zero) or [-MAX to -1, 1 to MAX] - * CRYPTO {boolean} true or false - * MODULO_MODE {number} 0 to 9 - * POW_PRECISION {number} 0 to MAX - * ALPHABET {string} A string of two or more unique characters which does - * not contain '.'. - * FORMAT {object} An object with some of the following properties: - * prefix {string} - * groupSize {number} - * secondaryGroupSize {number} - * groupSeparator {string} - * decimalSeparator {string} - * fractionGroupSize {number} - * fractionGroupSeparator {string} - * suffix {string} - * - * (The values assigned to the above FORMAT object properties are not checked for validity.) - * - * E.g. - * BigNumber.config({ DECIMAL_PLACES : 20, ROUNDING_MODE : 4 }) - * - * Ignore properties/parameters set to null or undefined, except for ALPHABET. - * - * Return an object with the properties current values. - */ - BigNumber.config = BigNumber.set = function (obj) { - var p, v; - - if (obj != null) { - - if (typeof obj == 'object') { - - // DECIMAL_PLACES {number} Integer, 0 to MAX inclusive. - // '[BigNumber Error] DECIMAL_PLACES {not a primitive number|not an integer|out of range}: {v}' - if (obj.hasOwnProperty(p = 'DECIMAL_PLACES')) { - v = obj[p]; - intCheck(v, 0, MAX, p); - DECIMAL_PLACES = v; - } - - // ROUNDING_MODE {number} Integer, 0 to 8 inclusive. - // '[BigNumber Error] ROUNDING_MODE {not a primitive number|not an integer|out of range}: {v}' - if (obj.hasOwnProperty(p = 'ROUNDING_MODE')) { - v = obj[p]; - intCheck(v, 0, 8, p); - ROUNDING_MODE = v; - } - - // EXPONENTIAL_AT {number|number[]} - // Integer, -MAX to MAX inclusive or - // [integer -MAX to 0 inclusive, 0 to MAX inclusive]. - // '[BigNumber Error] EXPONENTIAL_AT {not a primitive number|not an integer|out of range}: {v}' - if (obj.hasOwnProperty(p = 'EXPONENTIAL_AT')) { - v = obj[p]; - if (v && v.pop) { - intCheck(v[0], -MAX, 0, p); - intCheck(v[1], 0, MAX, p); - TO_EXP_NEG = v[0]; - TO_EXP_POS = v[1]; - } else { - intCheck(v, -MAX, MAX, p); - TO_EXP_NEG = -(TO_EXP_POS = v < 0 ? -v : v); - } - } - - // RANGE {number|number[]} Non-zero integer, -MAX to MAX inclusive or - // [integer -MAX to -1 inclusive, integer 1 to MAX inclusive]. - // '[BigNumber Error] RANGE {not a primitive number|not an integer|out of range|cannot be zero}: {v}' - if (obj.hasOwnProperty(p = 'RANGE')) { - v = obj[p]; - if (v && v.pop) { - intCheck(v[0], -MAX, -1, p); - intCheck(v[1], 1, MAX, p); - MIN_EXP = v[0]; - MAX_EXP = v[1]; - } else { - intCheck(v, -MAX, MAX, p); - if (v) { - MIN_EXP = -(MAX_EXP = v < 0 ? -v : v); - } else { - throw Error - (bignumberError + p + ' cannot be zero: ' + v); - } - } - } - - // CRYPTO {boolean} true or false. - // '[BigNumber Error] CRYPTO not true or false: {v}' - // '[BigNumber Error] crypto unavailable' - if (obj.hasOwnProperty(p = 'CRYPTO')) { - v = obj[p]; - if (v === !!v) { - if (v) { - if (typeof crypto != 'undefined' && crypto && - (crypto.getRandomValues || crypto.randomBytes)) { - CRYPTO = v; - } else { - CRYPTO = !v; - throw Error - (bignumberError + 'crypto unavailable'); - } - } else { - CRYPTO = v; - } - } else { - throw Error - (bignumberError + p + ' not true or false: ' + v); - } - } - - // MODULO_MODE {number} Integer, 0 to 9 inclusive. - // '[BigNumber Error] MODULO_MODE {not a primitive number|not an integer|out of range}: {v}' - if (obj.hasOwnProperty(p = 'MODULO_MODE')) { - v = obj[p]; - intCheck(v, 0, 9, p); - MODULO_MODE = v; - } - - // POW_PRECISION {number} Integer, 0 to MAX inclusive. - // '[BigNumber Error] POW_PRECISION {not a primitive number|not an integer|out of range}: {v}' - if (obj.hasOwnProperty(p = 'POW_PRECISION')) { - v = obj[p]; - intCheck(v, 0, MAX, p); - POW_PRECISION = v; - } - - // FORMAT {object} - // '[BigNumber Error] FORMAT not an object: {v}' - if (obj.hasOwnProperty(p = 'FORMAT')) { - v = obj[p]; - if (typeof v == 'object') FORMAT = v; - else throw Error - (bignumberError + p + ' not an object: ' + v); - } - - // ALPHABET {string} - // '[BigNumber Error] ALPHABET invalid: {v}' - if (obj.hasOwnProperty(p = 'ALPHABET')) { - v = obj[p]; - - // Disallow if less than two characters, - // or if it contains '+', '-', '.', whitespace, or a repeated character. - if (typeof v == 'string' && !/^.?$|[+\-.\s]|(.).*\1/.test(v)) { - alphabetHasNormalDecimalDigits = v.slice(0, 10) == '0123456789'; - ALPHABET = v; - } else { - throw Error - (bignumberError + p + ' invalid: ' + v); - } - } - - } else { - - // '[BigNumber Error] Object expected: {v}' - throw Error - (bignumberError + 'Object expected: ' + obj); - } - } - - return { - DECIMAL_PLACES: DECIMAL_PLACES, - ROUNDING_MODE: ROUNDING_MODE, - EXPONENTIAL_AT: [TO_EXP_NEG, TO_EXP_POS], - RANGE: [MIN_EXP, MAX_EXP], - CRYPTO: CRYPTO, - MODULO_MODE: MODULO_MODE, - POW_PRECISION: POW_PRECISION, - FORMAT: FORMAT, - ALPHABET: ALPHABET - }; - }; - - - /* - * Return true if v is a BigNumber instance, otherwise return false. - * - * If BigNumber.DEBUG is true, throw if a BigNumber instance is not well-formed. - * - * v {any} - * - * '[BigNumber Error] Invalid BigNumber: {v}' - */ - BigNumber.isBigNumber = function (v) { - if (!v || v._isBigNumber !== true) return false; - if (!BigNumber.DEBUG) return true; - - var i, n, - c = v.c, - e = v.e, - s = v.s; - - out: if ({}.toString.call(c) == '[object Array]') { - - if ((s === 1 || s === -1) && e >= -MAX && e <= MAX && e === mathfloor(e)) { - - // If the first element is zero, the BigNumber value must be zero. - if (c[0] === 0) { - if (e === 0 && c.length === 1) return true; - break out; - } - - // Calculate number of digits that c[0] should have, based on the exponent. - i = (e + 1) % LOG_BASE; - if (i < 1) i += LOG_BASE; - - // Calculate number of digits of c[0]. - //if (Math.ceil(Math.log(c[0] + 1) / Math.LN10) == i) { - if (String(c[0]).length == i) { - - for (i = 0; i < c.length; i++) { - n = c[i]; - if (n < 0 || n >= BASE || n !== mathfloor(n)) break out; - } - - // Last element cannot be zero, unless it is the only element. - if (n !== 0) return true; - } - } - - // Infinity/NaN - } else if (c === null && e === null && (s === null || s === 1 || s === -1)) { - return true; - } - - throw Error - (bignumberError + 'Invalid BigNumber: ' + v); - }; - - - /* - * Return a new BigNumber whose value is the maximum of the arguments. - * - * arguments {number|string|BigNumber} - */ - BigNumber.maximum = BigNumber.max = function () { - return maxOrMin(arguments, -1); - }; - - - /* - * Return a new BigNumber whose value is the minimum of the arguments. - * - * arguments {number|string|BigNumber} - */ - BigNumber.minimum = BigNumber.min = function () { - return maxOrMin(arguments, 1); - }; - - - /* - * Return a new BigNumber with a random value equal to or greater than 0 and less than 1, - * and with dp, or DECIMAL_PLACES if dp is omitted, decimal places (or less if trailing - * zeros are produced). - * - * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp}' - * '[BigNumber Error] crypto unavailable' - */ - BigNumber.random = (function () { - var pow2_53 = 0x20000000000000; - - // Return a 53 bit integer n, where 0 <= n < 9007199254740992. - // Check if Math.random() produces more than 32 bits of randomness. - // If it does, assume at least 53 bits are produced, otherwise assume at least 30 bits. - // 0x40000000 is 2^30, 0x800000 is 2^23, 0x1fffff is 2^21 - 1. - var random53bitInt = (Math.random() * pow2_53) & 0x1fffff - ? function () { return mathfloor(Math.random() * pow2_53); } - : function () { return ((Math.random() * 0x40000000 | 0) * 0x800000) + - (Math.random() * 0x800000 | 0); }; - - return function (dp) { - var a, b, e, k, v, - i = 0, - c = [], - rand = new BigNumber(ONE); - - if (dp == null) dp = DECIMAL_PLACES; - else intCheck(dp, 0, MAX); - - k = mathceil(dp / LOG_BASE); - - if (CRYPTO) { - - // Browsers supporting crypto.getRandomValues. - if (crypto.getRandomValues) { - - a = crypto.getRandomValues(new Uint32Array(k *= 2)); - - for (; i < k;) { - - // 53 bits: - // ((Math.pow(2, 32) - 1) * Math.pow(2, 21)).toString(2) - // 11111 11111111 11111111 11111111 11100000 00000000 00000000 - // ((Math.pow(2, 32) - 1) >>> 11).toString(2) - // 11111 11111111 11111111 - // 0x20000 is 2^21. - v = a[i] * 0x20000 + (a[i + 1] >>> 11); - - // Rejection sampling: - // 0 <= v < 9007199254740992 - // Probability that v >= 9e15, is - // 7199254740992 / 9007199254740992 ~= 0.0008, i.e. 1 in 1251 - if (v >= 9e15) { - b = crypto.getRandomValues(new Uint32Array(2)); - a[i] = b[0]; - a[i + 1] = b[1]; - } else { - - // 0 <= v <= 8999999999999999 - // 0 <= (v % 1e14) <= 99999999999999 - c.push(v % 1e14); - i += 2; - } - } - i = k / 2; - - // Node.js supporting crypto.randomBytes. - } else if (crypto.randomBytes) { - - // buffer - a = crypto.randomBytes(k *= 7); - - for (; i < k;) { - - // 0x1000000000000 is 2^48, 0x10000000000 is 2^40 - // 0x100000000 is 2^32, 0x1000000 is 2^24 - // 11111 11111111 11111111 11111111 11111111 11111111 11111111 - // 0 <= v < 9007199254740992 - v = ((a[i] & 31) * 0x1000000000000) + (a[i + 1] * 0x10000000000) + - (a[i + 2] * 0x100000000) + (a[i + 3] * 0x1000000) + - (a[i + 4] << 16) + (a[i + 5] << 8) + a[i + 6]; - - if (v >= 9e15) { - crypto.randomBytes(7).copy(a, i); - } else { - - // 0 <= (v % 1e14) <= 99999999999999 - c.push(v % 1e14); - i += 7; - } - } - i = k / 7; - } else { - CRYPTO = false; - throw Error - (bignumberError + 'crypto unavailable'); - } - } - - // Use Math.random. - if (!CRYPTO) { - - for (; i < k;) { - v = random53bitInt(); - if (v < 9e15) c[i++] = v % 1e14; - } - } - - k = c[--i]; - dp %= LOG_BASE; - - // Convert trailing digits to zeros according to dp. - if (k && dp) { - v = POWS_TEN[LOG_BASE - dp]; - c[i] = mathfloor(k / v) * v; - } - - // Remove trailing elements which are zero. - for (; c[i] === 0; c.pop(), i--); - - // Zero? - if (i < 0) { - c = [e = 0]; - } else { - - // Remove leading elements which are zero and adjust exponent accordingly. - for (e = -1 ; c[0] === 0; c.splice(0, 1), e -= LOG_BASE); - - // Count the digits of the first element of c to determine leading zeros, and... - for (i = 1, v = c[0]; v >= 10; v /= 10, i++); - - // adjust the exponent accordingly. - if (i < LOG_BASE) e -= LOG_BASE - i; - } - - rand.e = e; - rand.c = c; - return rand; - }; - })(); - - - /* - * Return a BigNumber whose value is the sum of the arguments. - * - * arguments {number|string|BigNumber} - */ - BigNumber.sum = function () { - var i = 1, - args = arguments, - sum = new BigNumber(args[0]); - for (; i < args.length;) sum = sum.plus(args[i++]); - return sum; - }; - - - // PRIVATE FUNCTIONS - - - // Called by BigNumber and BigNumber.prototype.toString. - convertBase = (function () { - var decimal = '0123456789'; - - /* - * Convert string of baseIn to an array of numbers of baseOut. - * Eg. toBaseOut('255', 10, 16) returns [15, 15]. - * Eg. toBaseOut('ff', 16, 10) returns [2, 5, 5]. - */ - function toBaseOut(str, baseIn, baseOut, alphabet) { - var j, - arr = [0], - arrL, - i = 0, - len = str.length; - - for (; i < len;) { - for (arrL = arr.length; arrL--; arr[arrL] *= baseIn); - - arr[0] += alphabet.indexOf(str.charAt(i++)); - - for (j = 0; j < arr.length; j++) { - - if (arr[j] > baseOut - 1) { - if (arr[j + 1] == null) arr[j + 1] = 0; - arr[j + 1] += arr[j] / baseOut | 0; - arr[j] %= baseOut; - } - } - } - - return arr.reverse(); - } - - // Convert a numeric string of baseIn to a numeric string of baseOut. - // If the caller is toString, we are converting from base 10 to baseOut. - // If the caller is BigNumber, we are converting from baseIn to base 10. - return function (str, baseIn, baseOut, sign, callerIsToString) { - var alphabet, d, e, k, r, x, xc, y, - i = str.indexOf('.'), - dp = DECIMAL_PLACES, - rm = ROUNDING_MODE; - - // Non-integer. - if (i >= 0) { - k = POW_PRECISION; - - // Unlimited precision. - POW_PRECISION = 0; - str = str.replace('.', ''); - y = new BigNumber(baseIn); - x = y.pow(str.length - i); - POW_PRECISION = k; - - // Convert str as if an integer, then restore the fraction part by dividing the - // result by its base raised to a power. - - y.c = toBaseOut(toFixedPoint(coeffToString(x.c), x.e, '0'), - 10, baseOut, decimal); - y.e = y.c.length; - } - - // Convert the number as integer. - - xc = toBaseOut(str, baseIn, baseOut, callerIsToString - ? (alphabet = ALPHABET, decimal) - : (alphabet = decimal, ALPHABET)); - - // xc now represents str as an integer and converted to baseOut. e is the exponent. - e = k = xc.length; - - // Remove trailing zeros. - for (; xc[--k] == 0; xc.pop()); - - // Zero? - if (!xc[0]) return alphabet.charAt(0); - - // Does str represent an integer? If so, no need for the division. - if (i < 0) { - --e; - } else { - x.c = xc; - x.e = e; - - // The sign is needed for correct rounding. - x.s = sign; - x = div(x, y, dp, rm, baseOut); - xc = x.c; - r = x.r; - e = x.e; - } - - // xc now represents str converted to baseOut. - - // THe index of the rounding digit. - d = e + dp + 1; - - // The rounding digit: the digit to the right of the digit that may be rounded up. - i = xc[d]; - - // Look at the rounding digits and mode to determine whether to round up. - - k = baseOut / 2; - r = r || d < 0 || xc[d + 1] != null; - - r = rm < 4 ? (i != null || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2)) - : i > k || i == k &&(rm == 4 || r || rm == 6 && xc[d - 1] & 1 || - rm == (x.s < 0 ? 8 : 7)); - - // If the index of the rounding digit is not greater than zero, or xc represents - // zero, then the result of the base conversion is zero or, if rounding up, a value - // such as 0.00001. - if (d < 1 || !xc[0]) { - - // 1^-dp or 0 - str = r ? toFixedPoint(alphabet.charAt(1), -dp, alphabet.charAt(0)) : alphabet.charAt(0); - } else { - - // Truncate xc to the required number of decimal places. - xc.length = d; - - // Round up? - if (r) { - - // Rounding up may mean the previous digit has to be rounded up and so on. - for (--baseOut; ++xc[--d] > baseOut;) { - xc[d] = 0; - - if (!d) { - ++e; - xc = [1].concat(xc); - } - } - } - - // Determine trailing zeros. - for (k = xc.length; !xc[--k];); - - // E.g. [4, 11, 15] becomes 4bf. - for (i = 0, str = ''; i <= k; str += alphabet.charAt(xc[i++])); - - // Add leading zeros, decimal point and trailing zeros as required. - str = toFixedPoint(str, e, alphabet.charAt(0)); - } - - // The caller will add the sign. - return str; - }; - })(); - - - // Perform division in the specified base. Called by div and convertBase. - div = (function () { - - // Assume non-zero x and k. - function multiply(x, k, base) { - var m, temp, xlo, xhi, - carry = 0, - i = x.length, - klo = k % SQRT_BASE, - khi = k / SQRT_BASE | 0; - - for (x = x.slice(); i--;) { - xlo = x[i] % SQRT_BASE; - xhi = x[i] / SQRT_BASE | 0; - m = khi * xlo + xhi * klo; - temp = klo * xlo + ((m % SQRT_BASE) * SQRT_BASE) + carry; - carry = (temp / base | 0) + (m / SQRT_BASE | 0) + khi * xhi; - x[i] = temp % base; - } - - if (carry) x = [carry].concat(x); - - return x; - } - - function compare(a, b, aL, bL) { - var i, cmp; - - if (aL != bL) { - cmp = aL > bL ? 1 : -1; - } else { - - for (i = cmp = 0; i < aL; i++) { - - if (a[i] != b[i]) { - cmp = a[i] > b[i] ? 1 : -1; - break; - } - } - } - - return cmp; - } - - function subtract(a, b, aL, base) { - var i = 0; - - // Subtract b from a. - for (; aL--;) { - a[aL] -= i; - i = a[aL] < b[aL] ? 1 : 0; - a[aL] = i * base + a[aL] - b[aL]; - } - - // Remove leading zeros. - for (; !a[0] && a.length > 1; a.splice(0, 1)); - } - - // x: dividend, y: divisor. - return function (x, y, dp, rm, base) { - var cmp, e, i, more, n, prod, prodL, q, qc, rem, remL, rem0, xi, xL, yc0, - yL, yz, - s = x.s == y.s ? 1 : -1, - xc = x.c, - yc = y.c; - - // Either NaN, Infinity or 0? - if (!xc || !xc[0] || !yc || !yc[0]) { - - return new BigNumber( - - // Return NaN if either NaN, or both Infinity or 0. - !x.s || !y.s || (xc ? yc && xc[0] == yc[0] : !yc) ? NaN : - - // Return ±0 if x is ±0 or y is ±Infinity, or return ±Infinity as y is ±0. - xc && xc[0] == 0 || !yc ? s * 0 : s / 0 - ); - } - - q = new BigNumber(s); - qc = q.c = []; - e = x.e - y.e; - s = dp + e + 1; - - if (!base) { - base = BASE; - e = bitFloor(x.e / LOG_BASE) - bitFloor(y.e / LOG_BASE); - s = s / LOG_BASE | 0; - } - - // Result exponent may be one less then the current value of e. - // The coefficients of the BigNumbers from convertBase may have trailing zeros. - for (i = 0; yc[i] == (xc[i] || 0); i++); - - if (yc[i] > (xc[i] || 0)) e--; - - if (s < 0) { - qc.push(1); - more = true; - } else { - xL = xc.length; - yL = yc.length; - i = 0; - s += 2; - - // Normalise xc and yc so highest order digit of yc is >= base / 2. - - n = mathfloor(base / (yc[0] + 1)); - - // Not necessary, but to handle odd bases where yc[0] == (base / 2) - 1. - // if (n > 1 || n++ == 1 && yc[0] < base / 2) { - if (n > 1) { - yc = multiply(yc, n, base); - xc = multiply(xc, n, base); - yL = yc.length; - xL = xc.length; - } - - xi = yL; - rem = xc.slice(0, yL); - remL = rem.length; - - // Add zeros to make remainder as long as divisor. - for (; remL < yL; rem[remL++] = 0); - yz = yc.slice(); - yz = [0].concat(yz); - yc0 = yc[0]; - if (yc[1] >= base / 2) yc0++; - // Not necessary, but to prevent trial digit n > base, when using base 3. - // else if (base == 3 && yc0 == 1) yc0 = 1 + 1e-15; - - do { - n = 0; - - // Compare divisor and remainder. - cmp = compare(yc, rem, yL, remL); - - // If divisor < remainder. - if (cmp < 0) { - - // Calculate trial digit, n. - - rem0 = rem[0]; - if (yL != remL) rem0 = rem0 * base + (rem[1] || 0); - - // n is how many times the divisor goes into the current remainder. - n = mathfloor(rem0 / yc0); - - // Algorithm: - // product = divisor multiplied by trial digit (n). - // Compare product and remainder. - // If product is greater than remainder: - // Subtract divisor from product, decrement trial digit. - // Subtract product from remainder. - // If product was less than remainder at the last compare: - // Compare new remainder and divisor. - // If remainder is greater than divisor: - // Subtract divisor from remainder, increment trial digit. - - if (n > 1) { - - // n may be > base only when base is 3. - if (n >= base) n = base - 1; - - // product = divisor * trial digit. - prod = multiply(yc, n, base); - prodL = prod.length; - remL = rem.length; - - // Compare product and remainder. - // If product > remainder then trial digit n too high. - // n is 1 too high about 5% of the time, and is not known to have - // ever been more than 1 too high. - while (compare(prod, rem, prodL, remL) == 1) { - n--; - - // Subtract divisor from product. - subtract(prod, yL < prodL ? yz : yc, prodL, base); - prodL = prod.length; - cmp = 1; - } - } else { - - // n is 0 or 1, cmp is -1. - // If n is 0, there is no need to compare yc and rem again below, - // so change cmp to 1 to avoid it. - // If n is 1, leave cmp as -1, so yc and rem are compared again. - if (n == 0) { - - // divisor < remainder, so n must be at least 1. - cmp = n = 1; - } - - // product = divisor - prod = yc.slice(); - prodL = prod.length; - } - - if (prodL < remL) prod = [0].concat(prod); - - // Subtract product from remainder. - subtract(rem, prod, remL, base); - remL = rem.length; - - // If product was < remainder. - if (cmp == -1) { - - // Compare divisor and new remainder. - // If divisor < new remainder, subtract divisor from remainder. - // Trial digit n too low. - // n is 1 too low about 5% of the time, and very rarely 2 too low. - while (compare(yc, rem, yL, remL) < 1) { - n++; - - // Subtract divisor from remainder. - subtract(rem, yL < remL ? yz : yc, remL, base); - remL = rem.length; - } - } - } else if (cmp === 0) { - n++; - rem = [0]; - } // else cmp === 1 and n will be 0 - - // Add the next digit, n, to the result array. - qc[i++] = n; - - // Update the remainder. - if (rem[0]) { - rem[remL++] = xc[xi] || 0; - } else { - rem = [xc[xi]]; - remL = 1; - } - } while ((xi++ < xL || rem[0] != null) && s--); - - more = rem[0] != null; - - // Leading zero? - if (!qc[0]) qc.splice(0, 1); - } - - if (base == BASE) { - - // To calculate q.e, first get the number of digits of qc[0]. - for (i = 1, s = qc[0]; s >= 10; s /= 10, i++); - - round(q, dp + (q.e = i + e * LOG_BASE - 1) + 1, rm, more); - - // Caller is convertBase. - } else { - q.e = e; - q.r = +more; - } - - return q; - }; - })(); - - - /* - * Return a string representing the value of BigNumber n in fixed-point or exponential - * notation rounded to the specified decimal places or significant digits. - * - * n: a BigNumber. - * i: the index of the last digit required (i.e. the digit that may be rounded up). - * rm: the rounding mode. - * id: 1 (toExponential) or 2 (toPrecision). - */ - function format(n, i, rm, id) { - var c0, e, ne, len, str; - - if (rm == null) rm = ROUNDING_MODE; - else intCheck(rm, 0, 8); - - if (!n.c) return n.toString(); - - c0 = n.c[0]; - ne = n.e; - - if (i == null) { - str = coeffToString(n.c); - str = id == 1 || id == 2 && (ne <= TO_EXP_NEG || ne >= TO_EXP_POS) - ? toExponential(str, ne) - : toFixedPoint(str, ne, '0'); - } else { - n = round(new BigNumber(n), i, rm); - - // n.e may have changed if the value was rounded up. - e = n.e; - - str = coeffToString(n.c); - len = str.length; - - // toPrecision returns exponential notation if the number of significant digits - // specified is less than the number of digits necessary to represent the integer - // part of the value in fixed-point notation. - - // Exponential notation. - if (id == 1 || id == 2 && (i <= e || e <= TO_EXP_NEG)) { - - // Append zeros? - for (; len < i; str += '0', len++); - str = toExponential(str, e); - - // Fixed-point notation. - } else { - i -= ne; - str = toFixedPoint(str, e, '0'); - - // Append zeros? - if (e + 1 > len) { - if (--i > 0) for (str += '.'; i--; str += '0'); - } else { - i += e - len; - if (i > 0) { - if (e + 1 == len) str += '.'; - for (; i--; str += '0'); - } - } - } - } - - return n.s < 0 && c0 ? '-' + str : str; - } - - - // Handle BigNumber.max and BigNumber.min. - // If any number is NaN, return NaN. - function maxOrMin(args, n) { - var k, y, - i = 1, - x = new BigNumber(args[0]); - - for (; i < args.length; i++) { - y = new BigNumber(args[i]); - if (!y.s || (k = compare(x, y)) === n || k === 0 && x.s === n) { - x = y; - } - } - - return x; - } - - - /* - * Strip trailing zeros, calculate base 10 exponent and check against MIN_EXP and MAX_EXP. - * Called by minus, plus and times. - */ - function normalise(n, c, e) { - var i = 1, - j = c.length; - - // Remove trailing zeros. - for (; !c[--j]; c.pop()); - - // Calculate the base 10 exponent. First get the number of digits of c[0]. - for (j = c[0]; j >= 10; j /= 10, i++); - - // Overflow? - if ((e = i + e * LOG_BASE - 1) > MAX_EXP) { - - // Infinity. - n.c = n.e = null; - - // Underflow? - } else if (e < MIN_EXP) { - - // Zero. - n.c = [n.e = 0]; - } else { - n.e = e; - n.c = c; - } - - return n; - } - - - // Handle values that fail the validity test in BigNumber. - parseNumeric = (function () { - var basePrefix = /^(-?)0([xbo])(?=\w[\w.]*$)/i, - dotAfter = /^([^.]+)\.$/, - dotBefore = /^\.([^.]+)$/, - isInfinityOrNaN = /^-?(Infinity|NaN)$/, - whitespaceOrPlus = /^\s*\+(?=[\w.])|^\s+|\s+$/g; - - return function (x, str, isNum, b) { - var base, - s = isNum ? str : str.replace(whitespaceOrPlus, ''); - - // No exception on ±Infinity or NaN. - if (isInfinityOrNaN.test(s)) { - x.s = isNaN(s) ? null : s < 0 ? -1 : 1; - } else { - if (!isNum) { - - // basePrefix = /^(-?)0([xbo])(?=\w[\w.]*$)/i - s = s.replace(basePrefix, function (m, p1, p2) { - base = (p2 = p2.toLowerCase()) == 'x' ? 16 : p2 == 'b' ? 2 : 8; - return !b || b == base ? p1 : m; - }); - - if (b) { - base = b; - - // E.g. '1.' to '1', '.1' to '0.1' - s = s.replace(dotAfter, '$1').replace(dotBefore, '0.$1'); - } - - if (str != s) return new BigNumber(s, base); - } - - // '[BigNumber Error] Not a number: {n}' - // '[BigNumber Error] Not a base {b} number: {n}' - if (BigNumber.DEBUG) { - throw Error - (bignumberError + 'Not a' + (b ? ' base ' + b : '') + ' number: ' + str); - } - - // NaN - x.s = null; - } - - x.c = x.e = null; - } - })(); - - - /* - * Round x to sd significant digits using rounding mode rm. Check for over/under-flow. - * If r is truthy, it is known that there are more digits after the rounding digit. - */ - function round(x, sd, rm, r) { - var d, i, j, k, n, ni, rd, - xc = x.c, - pows10 = POWS_TEN; - - // if x is not Infinity or NaN... - if (xc) { - - // rd is the rounding digit, i.e. the digit after the digit that may be rounded up. - // n is a base 1e14 number, the value of the element of array x.c containing rd. - // ni is the index of n within x.c. - // d is the number of digits of n. - // i is the index of rd within n including leading zeros. - // j is the actual index of rd within n (if < 0, rd is a leading zero). - out: { - - // Get the number of digits of the first element of xc. - for (d = 1, k = xc[0]; k >= 10; k /= 10, d++); - i = sd - d; - - // If the rounding digit is in the first element of xc... - if (i < 0) { - i += LOG_BASE; - j = sd; - n = xc[ni = 0]; - - // Get the rounding digit at index j of n. - rd = mathfloor(n / pows10[d - j - 1] % 10); - } else { - ni = mathceil((i + 1) / LOG_BASE); - - if (ni >= xc.length) { - - if (r) { - - // Needed by sqrt. - for (; xc.length <= ni; xc.push(0)); - n = rd = 0; - d = 1; - i %= LOG_BASE; - j = i - LOG_BASE + 1; - } else { - break out; - } - } else { - n = k = xc[ni]; - - // Get the number of digits of n. - for (d = 1; k >= 10; k /= 10, d++); - - // Get the index of rd within n. - i %= LOG_BASE; - - // Get the index of rd within n, adjusted for leading zeros. - // The number of leading zeros of n is given by LOG_BASE - d. - j = i - LOG_BASE + d; - - // Get the rounding digit at index j of n. - rd = j < 0 ? 0 : mathfloor(n / pows10[d - j - 1] % 10); - } - } - - r = r || sd < 0 || - - // Are there any non-zero digits after the rounding digit? - // The expression n % pows10[d - j - 1] returns all digits of n to the right - // of the digit at j, e.g. if n is 908714 and j is 2, the expression gives 714. - xc[ni + 1] != null || (j < 0 ? n : n % pows10[d - j - 1]); - - r = rm < 4 - ? (rd || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2)) - : rd > 5 || rd == 5 && (rm == 4 || r || rm == 6 && - - // Check whether the digit to the left of the rounding digit is odd. - ((i > 0 ? j > 0 ? n / pows10[d - j] : 0 : xc[ni - 1]) % 10) & 1 || - rm == (x.s < 0 ? 8 : 7)); - - if (sd < 1 || !xc[0]) { - xc.length = 0; - - if (r) { - - // Convert sd to decimal places. - sd -= x.e + 1; - - // 1, 0.1, 0.01, 0.001, 0.0001 etc. - xc[0] = pows10[(LOG_BASE - sd % LOG_BASE) % LOG_BASE]; - x.e = -sd || 0; - } else { - - // Zero. - xc[0] = x.e = 0; - } - - return x; - } - - // Remove excess digits. - if (i == 0) { - xc.length = ni; - k = 1; - ni--; - } else { - xc.length = ni + 1; - k = pows10[LOG_BASE - i]; - - // E.g. 56700 becomes 56000 if 7 is the rounding digit. - // j > 0 means i > number of leading zeros of n. - xc[ni] = j > 0 ? mathfloor(n / pows10[d - j] % pows10[j]) * k : 0; - } - - // Round up? - if (r) { - - for (; ;) { - - // If the digit to be rounded up is in the first element of xc... - if (ni == 0) { - - // i will be the length of xc[0] before k is added. - for (i = 1, j = xc[0]; j >= 10; j /= 10, i++); - j = xc[0] += k; - for (k = 1; j >= 10; j /= 10, k++); - - // if i != k the length has increased. - if (i != k) { - x.e++; - if (xc[0] == BASE) xc[0] = 1; - } - - break; - } else { - xc[ni] += k; - if (xc[ni] != BASE) break; - xc[ni--] = 0; - k = 1; - } - } - } - - // Remove trailing zeros. - for (i = xc.length; xc[--i] === 0; xc.pop()); - } - - // Overflow? Infinity. - if (x.e > MAX_EXP) { - x.c = x.e = null; - - // Underflow? Zero. - } else if (x.e < MIN_EXP) { - x.c = [x.e = 0]; - } - } - - return x; - } - - - function valueOf(n) { - var str, - e = n.e; - - if (e === null) return n.toString(); - - str = coeffToString(n.c); - - str = e <= TO_EXP_NEG || e >= TO_EXP_POS - ? toExponential(str, e) - : toFixedPoint(str, e, '0'); - - return n.s < 0 ? '-' + str : str; - } - - - // PROTOTYPE/INSTANCE METHODS - - - /* - * Return a new BigNumber whose value is the absolute value of this BigNumber. - */ - P.absoluteValue = P.abs = function () { - var x = new BigNumber(this); - if (x.s < 0) x.s = 1; - return x; - }; - - - /* - * Return - * 1 if the value of this BigNumber is greater than the value of BigNumber(y, b), - * -1 if the value of this BigNumber is less than the value of BigNumber(y, b), - * 0 if they have the same value, - * or null if the value of either is NaN. - */ - P.comparedTo = function (y, b) { - return compare(this, new BigNumber(y, b)); - }; - - - /* - * If dp is undefined or null or true or false, return the number of decimal places of the - * value of this BigNumber, or null if the value of this BigNumber is ±Infinity or NaN. - * - * Otherwise, if dp is a number, return a new BigNumber whose value is the value of this - * BigNumber rounded to a maximum of dp decimal places using rounding mode rm, or - * ROUNDING_MODE if rm is omitted. - * - * [dp] {number} Decimal places: integer, 0 to MAX inclusive. - * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' - */ - P.decimalPlaces = P.dp = function (dp, rm) { - var c, n, v, - x = this; - - if (dp != null) { - intCheck(dp, 0, MAX); - if (rm == null) rm = ROUNDING_MODE; - else intCheck(rm, 0, 8); - - return round(new BigNumber(x), dp + x.e + 1, rm); - } - - if (!(c = x.c)) return null; - n = ((v = c.length - 1) - bitFloor(this.e / LOG_BASE)) * LOG_BASE; - - // Subtract the number of trailing zeros of the last number. - if (v = c[v]) for (; v % 10 == 0; v /= 10, n--); - if (n < 0) n = 0; - - return n; - }; - - - /* - * n / 0 = I - * n / N = N - * n / I = 0 - * 0 / n = 0 - * 0 / 0 = N - * 0 / N = N - * 0 / I = 0 - * N / n = N - * N / 0 = N - * N / N = N - * N / I = N - * I / n = I - * I / 0 = I - * I / N = N - * I / I = N - * - * Return a new BigNumber whose value is the value of this BigNumber divided by the value of - * BigNumber(y, b), rounded according to DECIMAL_PLACES and ROUNDING_MODE. - */ - P.dividedBy = P.div = function (y, b) { - return div(this, new BigNumber(y, b), DECIMAL_PLACES, ROUNDING_MODE); - }; - - - /* - * Return a new BigNumber whose value is the integer part of dividing the value of this - * BigNumber by the value of BigNumber(y, b). - */ - P.dividedToIntegerBy = P.idiv = function (y, b) { - return div(this, new BigNumber(y, b), 0, 1); - }; - - - /* - * Return a BigNumber whose value is the value of this BigNumber exponentiated by n. - * - * If m is present, return the result modulo m. - * If n is negative round according to DECIMAL_PLACES and ROUNDING_MODE. - * If POW_PRECISION is non-zero and m is not present, round to POW_PRECISION using ROUNDING_MODE. - * - * The modular power operation works efficiently when x, n, and m are integers, otherwise it - * is equivalent to calculating x.exponentiatedBy(n).modulo(m) with a POW_PRECISION of 0. - * - * n {number|string|BigNumber} The exponent. An integer. - * [m] {number|string|BigNumber} The modulus. - * - * '[BigNumber Error] Exponent not an integer: {n}' - */ - P.exponentiatedBy = P.pow = function (n, m) { - var half, isModExp, i, k, more, nIsBig, nIsNeg, nIsOdd, y, - x = this; - - n = new BigNumber(n); - - // Allow NaN and ±Infinity, but not other non-integers. - if (n.c && !n.isInteger()) { - throw Error - (bignumberError + 'Exponent not an integer: ' + valueOf(n)); - } - - if (m != null) m = new BigNumber(m); - - // Exponent of MAX_SAFE_INTEGER is 15. - nIsBig = n.e > 14; - - // If x is NaN, ±Infinity, ±0 or ±1, or n is ±Infinity, NaN or ±0. - if (!x.c || !x.c[0] || x.c[0] == 1 && !x.e && x.c.length == 1 || !n.c || !n.c[0]) { - - // The sign of the result of pow when x is negative depends on the evenness of n. - // If +n overflows to ±Infinity, the evenness of n would be not be known. - y = new BigNumber(Math.pow(+valueOf(x), nIsBig ? n.s * (2 - isOdd(n)) : +valueOf(n))); - return m ? y.mod(m) : y; - } - - nIsNeg = n.s < 0; - - if (m) { - - // x % m returns NaN if abs(m) is zero, or m is NaN. - if (m.c ? !m.c[0] : !m.s) return new BigNumber(NaN); - - isModExp = !nIsNeg && x.isInteger() && m.isInteger(); - - if (isModExp) x = x.mod(m); - - // Overflow to ±Infinity: >=2**1e10 or >=1.0000024**1e15. - // Underflow to ±0: <=0.79**1e10 or <=0.9999975**1e15. - } else if (n.e > 9 && (x.e > 0 || x.e < -1 || (x.e == 0 - // [1, 240000000] - ? x.c[0] > 1 || nIsBig && x.c[1] >= 24e7 - // [80000000000000] [99999750000000] - : x.c[0] < 8e13 || nIsBig && x.c[0] <= 9999975e7))) { - - // If x is negative and n is odd, k = -0, else k = 0. - k = x.s < 0 && isOdd(n) ? -0 : 0; - - // If x >= 1, k = ±Infinity. - if (x.e > -1) k = 1 / k; - - // If n is negative return ±0, else return ±Infinity. - return new BigNumber(nIsNeg ? 1 / k : k); - - } else if (POW_PRECISION) { - - // Truncating each coefficient array to a length of k after each multiplication - // equates to truncating significant digits to POW_PRECISION + [28, 41], - // i.e. there will be a minimum of 28 guard digits retained. - k = mathceil(POW_PRECISION / LOG_BASE + 2); - } - - if (nIsBig) { - half = new BigNumber(0.5); - if (nIsNeg) n.s = 1; - nIsOdd = isOdd(n); - } else { - i = Math.abs(+valueOf(n)); - nIsOdd = i % 2; - } - - y = new BigNumber(ONE); - - // Performs 54 loop iterations for n of 9007199254740991. - for (; ;) { - - if (nIsOdd) { - y = y.times(x); - if (!y.c) break; - - if (k) { - if (y.c.length > k) y.c.length = k; - } else if (isModExp) { - y = y.mod(m); //y = y.minus(div(y, m, 0, MODULO_MODE).times(m)); - } - } - - if (i) { - i = mathfloor(i / 2); - if (i === 0) break; - nIsOdd = i % 2; - } else { - n = n.times(half); - round(n, n.e + 1, 1); - - if (n.e > 14) { - nIsOdd = isOdd(n); - } else { - i = +valueOf(n); - if (i === 0) break; - nIsOdd = i % 2; - } - } - - x = x.times(x); - - if (k) { - if (x.c && x.c.length > k) x.c.length = k; - } else if (isModExp) { - x = x.mod(m); //x = x.minus(div(x, m, 0, MODULO_MODE).times(m)); - } - } - - if (isModExp) return y; - if (nIsNeg) y = ONE.div(y); - - return m ? y.mod(m) : k ? round(y, POW_PRECISION, ROUNDING_MODE, more) : y; - }; - - - /* - * Return a new BigNumber whose value is the value of this BigNumber rounded to an integer - * using rounding mode rm, or ROUNDING_MODE if rm is omitted. - * - * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {rm}' - */ - P.integerValue = function (rm) { - var n = new BigNumber(this); - if (rm == null) rm = ROUNDING_MODE; - else intCheck(rm, 0, 8); - return round(n, n.e + 1, rm); - }; - - - /* - * Return true if the value of this BigNumber is equal to the value of BigNumber(y, b), - * otherwise return false. - */ - P.isEqualTo = P.eq = function (y, b) { - return compare(this, new BigNumber(y, b)) === 0; - }; - - - /* - * Return true if the value of this BigNumber is a finite number, otherwise return false. - */ - P.isFinite = function () { - return !!this.c; - }; - - - /* - * Return true if the value of this BigNumber is greater than the value of BigNumber(y, b), - * otherwise return false. - */ - P.isGreaterThan = P.gt = function (y, b) { - return compare(this, new BigNumber(y, b)) > 0; - }; - - - /* - * Return true if the value of this BigNumber is greater than or equal to the value of - * BigNumber(y, b), otherwise return false. - */ - P.isGreaterThanOrEqualTo = P.gte = function (y, b) { - return (b = compare(this, new BigNumber(y, b))) === 1 || b === 0; - - }; - - - /* - * Return true if the value of this BigNumber is an integer, otherwise return false. - */ - P.isInteger = function () { - return !!this.c && bitFloor(this.e / LOG_BASE) > this.c.length - 2; - }; - - - /* - * Return true if the value of this BigNumber is less than the value of BigNumber(y, b), - * otherwise return false. - */ - P.isLessThan = P.lt = function (y, b) { - return compare(this, new BigNumber(y, b)) < 0; - }; - - - /* - * Return true if the value of this BigNumber is less than or equal to the value of - * BigNumber(y, b), otherwise return false. - */ - P.isLessThanOrEqualTo = P.lte = function (y, b) { - return (b = compare(this, new BigNumber(y, b))) === -1 || b === 0; - }; - - - /* - * Return true if the value of this BigNumber is NaN, otherwise return false. - */ - P.isNaN = function () { - return !this.s; - }; - - - /* - * Return true if the value of this BigNumber is negative, otherwise return false. - */ - P.isNegative = function () { - return this.s < 0; - }; - - - /* - * Return true if the value of this BigNumber is positive, otherwise return false. - */ - P.isPositive = function () { - return this.s > 0; - }; - - - /* - * Return true if the value of this BigNumber is 0 or -0, otherwise return false. - */ - P.isZero = function () { - return !!this.c && this.c[0] == 0; - }; - - - /* - * n - 0 = n - * n - N = N - * n - I = -I - * 0 - n = -n - * 0 - 0 = 0 - * 0 - N = N - * 0 - I = -I - * N - n = N - * N - 0 = N - * N - N = N - * N - I = N - * I - n = I - * I - 0 = I - * I - N = N - * I - I = N - * - * Return a new BigNumber whose value is the value of this BigNumber minus the value of - * BigNumber(y, b). - */ - P.minus = function (y, b) { - var i, j, t, xLTy, - x = this, - a = x.s; - - y = new BigNumber(y, b); - b = y.s; - - // Either NaN? - if (!a || !b) return new BigNumber(NaN); - - // Signs differ? - if (a != b) { - y.s = -b; - return x.plus(y); - } - - var xe = x.e / LOG_BASE, - ye = y.e / LOG_BASE, - xc = x.c, - yc = y.c; - - if (!xe || !ye) { - - // Either Infinity? - if (!xc || !yc) return xc ? (y.s = -b, y) : new BigNumber(yc ? x : NaN); - - // Either zero? - if (!xc[0] || !yc[0]) { - - // Return y if y is non-zero, x if x is non-zero, or zero if both are zero. - return yc[0] ? (y.s = -b, y) : new BigNumber(xc[0] ? x : - - // IEEE 754 (2008) 6.3: n - n = -0 when rounding to -Infinity - ROUNDING_MODE == 3 ? -0 : 0); - } - } - - xe = bitFloor(xe); - ye = bitFloor(ye); - xc = xc.slice(); - - // Determine which is the bigger number. - if (a = xe - ye) { - - if (xLTy = a < 0) { - a = -a; - t = xc; - } else { - ye = xe; - t = yc; - } - - t.reverse(); - - // Prepend zeros to equalise exponents. - for (b = a; b--; t.push(0)); - t.reverse(); - } else { - - // Exponents equal. Check digit by digit. - j = (xLTy = (a = xc.length) < (b = yc.length)) ? a : b; - - for (a = b = 0; b < j; b++) { - - if (xc[b] != yc[b]) { - xLTy = xc[b] < yc[b]; - break; - } - } - } - - // x < y? Point xc to the array of the bigger number. - if (xLTy) { - t = xc; - xc = yc; - yc = t; - y.s = -y.s; - } - - b = (j = yc.length) - (i = xc.length); - - // Append zeros to xc if shorter. - // No need to add zeros to yc if shorter as subtract only needs to start at yc.length. - if (b > 0) for (; b--; xc[i++] = 0); - b = BASE - 1; - - // Subtract yc from xc. - for (; j > a;) { - - if (xc[--j] < yc[j]) { - for (i = j; i && !xc[--i]; xc[i] = b); - --xc[i]; - xc[j] += BASE; - } - - xc[j] -= yc[j]; - } - - // Remove leading zeros and adjust exponent accordingly. - for (; xc[0] == 0; xc.splice(0, 1), --ye); - - // Zero? - if (!xc[0]) { - - // Following IEEE 754 (2008) 6.3, - // n - n = +0 but n - n = -0 when rounding towards -Infinity. - y.s = ROUNDING_MODE == 3 ? -1 : 1; - y.c = [y.e = 0]; - return y; - } - - // No need to check for Infinity as +x - +y != Infinity && -x - -y != Infinity - // for finite x and y. - return normalise(y, xc, ye); - }; - - - /* - * n % 0 = N - * n % N = N - * n % I = n - * 0 % n = 0 - * -0 % n = -0 - * 0 % 0 = N - * 0 % N = N - * 0 % I = 0 - * N % n = N - * N % 0 = N - * N % N = N - * N % I = N - * I % n = N - * I % 0 = N - * I % N = N - * I % I = N - * - * Return a new BigNumber whose value is the value of this BigNumber modulo the value of - * BigNumber(y, b). The result depends on the value of MODULO_MODE. - */ - P.modulo = P.mod = function (y, b) { - var q, s, - x = this; - - y = new BigNumber(y, b); - - // Return NaN if x is Infinity or NaN, or y is NaN or zero. - if (!x.c || !y.s || y.c && !y.c[0]) { - return new BigNumber(NaN); - - // Return x if y is Infinity or x is zero. - } else if (!y.c || x.c && !x.c[0]) { - return new BigNumber(x); - } - - if (MODULO_MODE == 9) { - - // Euclidian division: q = sign(y) * floor(x / abs(y)) - // r = x - qy where 0 <= r < abs(y) - s = y.s; - y.s = 1; - q = div(x, y, 0, 3); - y.s = s; - q.s *= s; - } else { - q = div(x, y, 0, MODULO_MODE); - } - - y = x.minus(q.times(y)); - - // To match JavaScript %, ensure sign of zero is sign of dividend. - if (!y.c[0] && MODULO_MODE == 1) y.s = x.s; - - return y; - }; - - - /* - * n * 0 = 0 - * n * N = N - * n * I = I - * 0 * n = 0 - * 0 * 0 = 0 - * 0 * N = N - * 0 * I = N - * N * n = N - * N * 0 = N - * N * N = N - * N * I = N - * I * n = I - * I * 0 = N - * I * N = N - * I * I = I - * - * Return a new BigNumber whose value is the value of this BigNumber multiplied by the value - * of BigNumber(y, b). - */ - P.multipliedBy = P.times = function (y, b) { - var c, e, i, j, k, m, xcL, xlo, xhi, ycL, ylo, yhi, zc, - base, sqrtBase, - x = this, - xc = x.c, - yc = (y = new BigNumber(y, b)).c; - - // Either NaN, ±Infinity or ±0? - if (!xc || !yc || !xc[0] || !yc[0]) { - - // Return NaN if either is NaN, or one is 0 and the other is Infinity. - if (!x.s || !y.s || xc && !xc[0] && !yc || yc && !yc[0] && !xc) { - y.c = y.e = y.s = null; - } else { - y.s *= x.s; - - // Return ±Infinity if either is ±Infinity. - if (!xc || !yc) { - y.c = y.e = null; - - // Return ±0 if either is ±0. - } else { - y.c = [0]; - y.e = 0; - } - } - - return y; - } - - e = bitFloor(x.e / LOG_BASE) + bitFloor(y.e / LOG_BASE); - y.s *= x.s; - xcL = xc.length; - ycL = yc.length; - - // Ensure xc points to longer array and xcL to its length. - if (xcL < ycL) { - zc = xc; - xc = yc; - yc = zc; - i = xcL; - xcL = ycL; - ycL = i; - } - - // Initialise the result array with zeros. - for (i = xcL + ycL, zc = []; i--; zc.push(0)); - - base = BASE; - sqrtBase = SQRT_BASE; - - for (i = ycL; --i >= 0;) { - c = 0; - ylo = yc[i] % sqrtBase; - yhi = yc[i] / sqrtBase | 0; - - for (k = xcL, j = i + k; j > i;) { - xlo = xc[--k] % sqrtBase; - xhi = xc[k] / sqrtBase | 0; - m = yhi * xlo + xhi * ylo; - xlo = ylo * xlo + ((m % sqrtBase) * sqrtBase) + zc[j] + c; - c = (xlo / base | 0) + (m / sqrtBase | 0) + yhi * xhi; - zc[j--] = xlo % base; - } - - zc[j] = c; - } - - if (c) { - ++e; - } else { - zc.splice(0, 1); - } - - return normalise(y, zc, e); - }; - - - /* - * Return a new BigNumber whose value is the value of this BigNumber negated, - * i.e. multiplied by -1. - */ - P.negated = function () { - var x = new BigNumber(this); - x.s = -x.s || null; - return x; - }; - - - /* - * n + 0 = n - * n + N = N - * n + I = I - * 0 + n = n - * 0 + 0 = 0 - * 0 + N = N - * 0 + I = I - * N + n = N - * N + 0 = N - * N + N = N - * N + I = N - * I + n = I - * I + 0 = I - * I + N = N - * I + I = I - * - * Return a new BigNumber whose value is the value of this BigNumber plus the value of - * BigNumber(y, b). - */ - P.plus = function (y, b) { - var t, - x = this, - a = x.s; - - y = new BigNumber(y, b); - b = y.s; - - // Either NaN? - if (!a || !b) return new BigNumber(NaN); - - // Signs differ? - if (a != b) { - y.s = -b; - return x.minus(y); - } - - var xe = x.e / LOG_BASE, - ye = y.e / LOG_BASE, - xc = x.c, - yc = y.c; - - if (!xe || !ye) { - - // Return ±Infinity if either ±Infinity. - if (!xc || !yc) return new BigNumber(a / 0); - - // Either zero? - // Return y if y is non-zero, x if x is non-zero, or zero if both are zero. - if (!xc[0] || !yc[0]) return yc[0] ? y : new BigNumber(xc[0] ? x : a * 0); - } - - xe = bitFloor(xe); - ye = bitFloor(ye); - xc = xc.slice(); - - // Prepend zeros to equalise exponents. Faster to use reverse then do unshifts. - if (a = xe - ye) { - if (a > 0) { - ye = xe; - t = yc; - } else { - a = -a; - t = xc; - } - - t.reverse(); - for (; a--; t.push(0)); - t.reverse(); - } - - a = xc.length; - b = yc.length; - - // Point xc to the longer array, and b to the shorter length. - if (a - b < 0) { - t = yc; - yc = xc; - xc = t; - b = a; - } - - // Only start adding at yc.length - 1 as the further digits of xc can be ignored. - for (a = 0; b;) { - a = (xc[--b] = xc[b] + yc[b] + a) / BASE | 0; - xc[b] = BASE === xc[b] ? 0 : xc[b] % BASE; - } - - if (a) { - xc = [a].concat(xc); - ++ye; - } - - // No need to check for zero, as +x + +y != 0 && -x + -y != 0 - // ye = MAX_EXP + 1 possible - return normalise(y, xc, ye); - }; - - - /* - * If sd is undefined or null or true or false, return the number of significant digits of - * the value of this BigNumber, or null if the value of this BigNumber is ±Infinity or NaN. - * If sd is true include integer-part trailing zeros in the count. - * - * Otherwise, if sd is a number, return a new BigNumber whose value is the value of this - * BigNumber rounded to a maximum of sd significant digits using rounding mode rm, or - * ROUNDING_MODE if rm is omitted. - * - * sd {number|boolean} number: significant digits: integer, 1 to MAX inclusive. - * boolean: whether to count integer-part trailing zeros: true or false. - * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}' - */ - P.precision = P.sd = function (sd, rm) { - var c, n, v, - x = this; - - if (sd != null && sd !== !!sd) { - intCheck(sd, 1, MAX); - if (rm == null) rm = ROUNDING_MODE; - else intCheck(rm, 0, 8); - - return round(new BigNumber(x), sd, rm); - } - - if (!(c = x.c)) return null; - v = c.length - 1; - n = v * LOG_BASE + 1; - - if (v = c[v]) { - - // Subtract the number of trailing zeros of the last element. - for (; v % 10 == 0; v /= 10, n--); - - // Add the number of digits of the first element. - for (v = c[0]; v >= 10; v /= 10, n++); - } - - if (sd && x.e + 1 > n) n = x.e + 1; - - return n; - }; - - - /* - * Return a new BigNumber whose value is the value of this BigNumber shifted by k places - * (powers of 10). Shift to the right if n > 0, and to the left if n < 0. - * - * k {number} Integer, -MAX_SAFE_INTEGER to MAX_SAFE_INTEGER inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {k}' - */ - P.shiftedBy = function (k) { - intCheck(k, -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER); - return this.times('1e' + k); - }; - - - /* - * sqrt(-n) = N - * sqrt(N) = N - * sqrt(-I) = N - * sqrt(I) = I - * sqrt(0) = 0 - * sqrt(-0) = -0 - * - * Return a new BigNumber whose value is the square root of the value of this BigNumber, - * rounded according to DECIMAL_PLACES and ROUNDING_MODE. - */ - P.squareRoot = P.sqrt = function () { - var m, n, r, rep, t, - x = this, - c = x.c, - s = x.s, - e = x.e, - dp = DECIMAL_PLACES + 4, - half = new BigNumber('0.5'); - - // Negative/NaN/Infinity/zero? - if (s !== 1 || !c || !c[0]) { - return new BigNumber(!s || s < 0 && (!c || c[0]) ? NaN : c ? x : 1 / 0); - } - - // Initial estimate. - s = Math.sqrt(+valueOf(x)); - - // Math.sqrt underflow/overflow? - // Pass x to Math.sqrt as integer, then adjust the exponent of the result. - if (s == 0 || s == 1 / 0) { - n = coeffToString(c); - if ((n.length + e) % 2 == 0) n += '0'; - s = Math.sqrt(+n); - e = bitFloor((e + 1) / 2) - (e < 0 || e % 2); - - if (s == 1 / 0) { - n = '5e' + e; - } else { - n = s.toExponential(); - n = n.slice(0, n.indexOf('e') + 1) + e; - } - - r = new BigNumber(n); - } else { - r = new BigNumber(s + ''); - } - - // Check for zero. - // r could be zero if MIN_EXP is changed after the this value was created. - // This would cause a division by zero (x/t) and hence Infinity below, which would cause - // coeffToString to throw. - if (r.c[0]) { - e = r.e; - s = e + dp; - if (s < 3) s = 0; - - // Newton-Raphson iteration. - for (; ;) { - t = r; - r = half.times(t.plus(div(x, t, dp, 1))); - - if (coeffToString(t.c).slice(0, s) === (n = coeffToString(r.c)).slice(0, s)) { - - // The exponent of r may here be one less than the final result exponent, - // e.g 0.0009999 (e-4) --> 0.001 (e-3), so adjust s so the rounding digits - // are indexed correctly. - if (r.e < e) --s; - n = n.slice(s - 3, s + 1); - - // The 4th rounding digit may be in error by -1 so if the 4 rounding digits - // are 9999 or 4999 (i.e. approaching a rounding boundary) continue the - // iteration. - if (n == '9999' || !rep && n == '4999') { - - // On the first iteration only, check to see if rounding up gives the - // exact result as the nines may infinitely repeat. - if (!rep) { - round(t, t.e + DECIMAL_PLACES + 2, 0); - - if (t.times(t).eq(x)) { - r = t; - break; - } - } - - dp += 4; - s += 4; - rep = 1; - } else { - - // If rounding digits are null, 0{0,4} or 50{0,3}, check for exact - // result. If not, then there are further digits and m will be truthy. - if (!+n || !+n.slice(1) && n.charAt(0) == '5') { - - // Truncate to the first rounding digit. - round(r, r.e + DECIMAL_PLACES + 2, 1); - m = !r.times(r).eq(x); - } - - break; - } - } - } - } - - return round(r, r.e + DECIMAL_PLACES + 1, ROUNDING_MODE, m); - }; - - - /* - * Return a string representing the value of this BigNumber in exponential notation and - * rounded using ROUNDING_MODE to dp fixed decimal places. - * - * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. - * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' - */ - P.toExponential = function (dp, rm) { - if (dp != null) { - intCheck(dp, 0, MAX); - dp++; - } - return format(this, dp, rm, 1); - }; - - - /* - * Return a string representing the value of this BigNumber in fixed-point notation rounding - * to dp fixed decimal places using rounding mode rm, or ROUNDING_MODE if rm is omitted. - * - * Note: as with JavaScript's number type, (-0).toFixed(0) is '0', - * but e.g. (-0.00001).toFixed(0) is '-0'. - * - * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. - * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' - */ - P.toFixed = function (dp, rm) { - if (dp != null) { - intCheck(dp, 0, MAX); - dp = dp + this.e + 1; - } - return format(this, dp, rm); - }; - - - /* - * Return a string representing the value of this BigNumber in fixed-point notation rounded - * using rm or ROUNDING_MODE to dp decimal places, and formatted according to the properties - * of the format or FORMAT object (see BigNumber.set). - * - * The formatting object may contain some or all of the properties shown below. - * - * FORMAT = { - * prefix: '', - * groupSize: 3, - * secondaryGroupSize: 0, - * groupSeparator: ',', - * decimalSeparator: '.', - * fractionGroupSize: 0, - * fractionGroupSeparator: '\xA0', // non-breaking space - * suffix: '' - * }; - * - * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. - * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. - * [format] {object} Formatting options. See FORMAT pbject above. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' - * '[BigNumber Error] Argument not an object: {format}' - */ - P.toFormat = function (dp, rm, format) { - var str, - x = this; - - if (format == null) { - if (dp != null && rm && typeof rm == 'object') { - format = rm; - rm = null; - } else if (dp && typeof dp == 'object') { - format = dp; - dp = rm = null; - } else { - format = FORMAT; - } - } else if (typeof format != 'object') { - throw Error - (bignumberError + 'Argument not an object: ' + format); - } - - str = x.toFixed(dp, rm); - - if (x.c) { - var i, - arr = str.split('.'), - g1 = +format.groupSize, - g2 = +format.secondaryGroupSize, - groupSeparator = format.groupSeparator || '', - intPart = arr[0], - fractionPart = arr[1], - isNeg = x.s < 0, - intDigits = isNeg ? intPart.slice(1) : intPart, - len = intDigits.length; - - if (g2) { - i = g1; - g1 = g2; - g2 = i; - len -= i; - } - - if (g1 > 0 && len > 0) { - i = len % g1 || g1; - intPart = intDigits.substr(0, i); - for (; i < len; i += g1) intPart += groupSeparator + intDigits.substr(i, g1); - if (g2 > 0) intPart += groupSeparator + intDigits.slice(i); - if (isNeg) intPart = '-' + intPart; - } - - str = fractionPart - ? intPart + (format.decimalSeparator || '') + ((g2 = +format.fractionGroupSize) - ? fractionPart.replace(new RegExp('\\d{' + g2 + '}\\B', 'g'), - '$&' + (format.fractionGroupSeparator || '')) - : fractionPart) - : intPart; - } - - return (format.prefix || '') + str + (format.suffix || ''); - }; - - - /* - * Return an array of two BigNumbers representing the value of this BigNumber as a simple - * fraction with an integer numerator and an integer denominator. - * The denominator will be a positive non-zero value less than or equal to the specified - * maximum denominator. If a maximum denominator is not specified, the denominator will be - * the lowest value necessary to represent the number exactly. - * - * [md] {number|string|BigNumber} Integer >= 1, or Infinity. The maximum denominator. - * - * '[BigNumber Error] Argument {not an integer|out of range} : {md}' - */ - P.toFraction = function (md) { - var d, d0, d1, d2, e, exp, n, n0, n1, q, r, s, - x = this, - xc = x.c; - - if (md != null) { - n = new BigNumber(md); - - // Throw if md is less than one or is not an integer, unless it is Infinity. - if (!n.isInteger() && (n.c || n.s !== 1) || n.lt(ONE)) { - throw Error - (bignumberError + 'Argument ' + - (n.isInteger() ? 'out of range: ' : 'not an integer: ') + valueOf(n)); - } - } - - if (!xc) return new BigNumber(x); - - d = new BigNumber(ONE); - n1 = d0 = new BigNumber(ONE); - d1 = n0 = new BigNumber(ONE); - s = coeffToString(xc); - - // Determine initial denominator. - // d is a power of 10 and the minimum max denominator that specifies the value exactly. - e = d.e = s.length - x.e - 1; - d.c[0] = POWS_TEN[(exp = e % LOG_BASE) < 0 ? LOG_BASE + exp : exp]; - md = !md || n.comparedTo(d) > 0 ? (e > 0 ? d : n1) : n; - - exp = MAX_EXP; - MAX_EXP = 1 / 0; - n = new BigNumber(s); - - // n0 = d1 = 0 - n0.c[0] = 0; - - for (; ;) { - q = div(n, d, 0, 1); - d2 = d0.plus(q.times(d1)); - if (d2.comparedTo(md) == 1) break; - d0 = d1; - d1 = d2; - n1 = n0.plus(q.times(d2 = n1)); - n0 = d2; - d = n.minus(q.times(d2 = d)); - n = d2; - } - - d2 = div(md.minus(d0), d1, 0, 1); - n0 = n0.plus(d2.times(n1)); - d0 = d0.plus(d2.times(d1)); - n0.s = n1.s = x.s; - e = e * 2; - - // Determine which fraction is closer to x, n0/d0 or n1/d1 - r = div(n1, d1, e, ROUNDING_MODE).minus(x).abs().comparedTo( - div(n0, d0, e, ROUNDING_MODE).minus(x).abs()) < 1 ? [n1, d1] : [n0, d0]; - - MAX_EXP = exp; - - return r; - }; - - - /* - * Return the value of this BigNumber converted to a number primitive. - */ - P.toNumber = function () { - return +valueOf(this); - }; - - - /* - * Return a string representing the value of this BigNumber rounded to sd significant digits - * using rounding mode rm or ROUNDING_MODE. If sd is less than the number of digits - * necessary to represent the integer part of the value in fixed-point notation, then use - * exponential notation. - * - * [sd] {number} Significant digits. Integer, 1 to MAX inclusive. - * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}' - */ - P.toPrecision = function (sd, rm) { - if (sd != null) intCheck(sd, 1, MAX); - return format(this, sd, rm, 2); - }; - - - /* - * Return a string representing the value of this BigNumber in base b, or base 10 if b is - * omitted. If a base is specified, including base 10, round according to DECIMAL_PLACES and - * ROUNDING_MODE. If a base is not specified, and this BigNumber has a positive exponent - * that is equal to or greater than TO_EXP_POS, or a negative exponent equal to or less than - * TO_EXP_NEG, return exponential notation. - * - * [b] {number} Integer, 2 to ALPHABET.length inclusive. - * - * '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}' - */ - P.toString = function (b) { - var str, - n = this, - s = n.s, - e = n.e; - - // Infinity or NaN? - if (e === null) { - if (s) { - str = 'Infinity'; - if (s < 0) str = '-' + str; - } else { - str = 'NaN'; - } - } else { - if (b == null) { - str = e <= TO_EXP_NEG || e >= TO_EXP_POS - ? toExponential(coeffToString(n.c), e) - : toFixedPoint(coeffToString(n.c), e, '0'); - } else if (b === 10 && alphabetHasNormalDecimalDigits) { - n = round(new BigNumber(n), DECIMAL_PLACES + e + 1, ROUNDING_MODE); - str = toFixedPoint(coeffToString(n.c), n.e, '0'); - } else { - intCheck(b, 2, ALPHABET.length, 'Base'); - str = convertBase(toFixedPoint(coeffToString(n.c), e, '0'), 10, b, s, true); - } - - if (s < 0 && n.c[0]) str = '-' + str; - } - - return str; - }; - - - /* - * Return as toString, but do not accept a base argument, and include the minus sign for - * negative zero. - */ - P.valueOf = P.toJSON = function () { - return valueOf(this); - }; - - - P._isBigNumber = true; - - if (configObject != null) BigNumber.set(configObject); - - return BigNumber; - } - - - // PRIVATE HELPER FUNCTIONS - - // These functions don't need access to variables, - // e.g. DECIMAL_PLACES, in the scope of the `clone` function above. - - - function bitFloor(n) { - var i = n | 0; - return n > 0 || n === i ? i : i - 1; - } - - - // Return a coefficient array as a string of base 10 digits. - function coeffToString(a) { - var s, z, - i = 1, - j = a.length, - r = a[0] + ''; - - for (; i < j;) { - s = a[i++] + ''; - z = LOG_BASE - s.length; - for (; z--; s = '0' + s); - r += s; - } - - // Determine trailing zeros. - for (j = r.length; r.charCodeAt(--j) === 48;); - - return r.slice(0, j + 1 || 1); - } - - - // Compare the value of BigNumbers x and y. - function compare(x, y) { - var a, b, - xc = x.c, - yc = y.c, - i = x.s, - j = y.s, - k = x.e, - l = y.e; - - // Either NaN? - if (!i || !j) return null; - - a = xc && !xc[0]; - b = yc && !yc[0]; - - // Either zero? - if (a || b) return a ? b ? 0 : -j : i; - - // Signs differ? - if (i != j) return i; - - a = i < 0; - b = k == l; - - // Either Infinity? - if (!xc || !yc) return b ? 0 : !xc ^ a ? 1 : -1; - - // Compare exponents. - if (!b) return k > l ^ a ? 1 : -1; - - j = (k = xc.length) < (l = yc.length) ? k : l; - - // Compare digit by digit. - for (i = 0; i < j; i++) if (xc[i] != yc[i]) return xc[i] > yc[i] ^ a ? 1 : -1; - - // Compare lengths. - return k == l ? 0 : k > l ^ a ? 1 : -1; - } - - - /* - * Check that n is a primitive number, an integer, and in range, otherwise throw. - */ - function intCheck(n, min, max, name) { - if (n < min || n > max || n !== mathfloor(n)) { - throw Error - (bignumberError + (name || 'Argument') + (typeof n == 'number' - ? n < min || n > max ? ' out of range: ' : ' not an integer: ' - : ' not a primitive number: ') + String(n)); - } - } - - - // Assumes finite n. - function isOdd(n) { - var k = n.c.length - 1; - return bitFloor(n.e / LOG_BASE) == k && n.c[k] % 2 != 0; - } - - - function toExponential(str, e) { - return (str.length > 1 ? str.charAt(0) + '.' + str.slice(1) : str) + - (e < 0 ? 'e' : 'e+') + e; - } - - - function toFixedPoint(str, e, z) { - var len, zs; - - // Negative exponent? - if (e < 0) { - - // Prepend zeros. - for (zs = z + '.'; ++e; zs += z); - str = zs + str; - - // Positive exponent - } else { - len = str.length; - - // Append zeros. - if (++e > len) { - for (zs = z, e -= len; --e; zs += z); - str += zs; - } else if (e < len) { - str = str.slice(0, e) + '.' + str.slice(e); - } - } - - return str; - } - - - // EXPORT - - - BigNumber = clone(); - BigNumber['default'] = BigNumber.BigNumber = BigNumber; - - // AMD. - if (typeof define == 'function' && define.amd) { - define(function () { return BigNumber; }); - - // Node.js and other environments that support module.exports. - } else if ( true && module.exports) { - module.exports = BigNumber; - - // Browser. - } else { - if (!globalObject) { - globalObject = typeof self != 'undefined' && self ? self : window; - } - - globalObject.BigNumber = BigNumber; - } -})(this); +;(function (globalObject) { + 'use strict'; + +/* + * bignumber.js v9.1.2 + * A JavaScript library for arbitrary-precision arithmetic. + * https://github.com/MikeMcl/bignumber.js + * Copyright (c) 2022 Michael Mclaughlin + * MIT Licensed. + * + * BigNumber.prototype methods | BigNumber methods + * | + * absoluteValue abs | clone + * comparedTo | config set + * decimalPlaces dp | DECIMAL_PLACES + * dividedBy div | ROUNDING_MODE + * dividedToIntegerBy idiv | EXPONENTIAL_AT + * exponentiatedBy pow | RANGE + * integerValue | CRYPTO + * isEqualTo eq | MODULO_MODE + * isFinite | POW_PRECISION + * isGreaterThan gt | FORMAT + * isGreaterThanOrEqualTo gte | ALPHABET + * isInteger | isBigNumber + * isLessThan lt | maximum max + * isLessThanOrEqualTo lte | minimum min + * isNaN | random + * isNegative | sum + * isPositive | + * isZero | + * minus | + * modulo mod | + * multipliedBy times | + * negated | + * plus | + * precision sd | + * shiftedBy | + * squareRoot sqrt | + * toExponential | + * toFixed | + * toFormat | + * toFraction | + * toJSON | + * toNumber | + * toPrecision | + * toString | + * valueOf | + * + */ + + + var BigNumber, + isNumeric = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i, + mathceil = Math.ceil, + mathfloor = Math.floor, + + bignumberError = '[BigNumber Error] ', + tooManyDigits = bignumberError + 'Number primitive has more than 15 significant digits: ', + + BASE = 1e14, + LOG_BASE = 14, + MAX_SAFE_INTEGER = 0x1fffffffffffff, // 2^53 - 1 + // MAX_INT32 = 0x7fffffff, // 2^31 - 1 + POWS_TEN = [1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13], + SQRT_BASE = 1e7, + + // EDITABLE + // The limit on the value of DECIMAL_PLACES, TO_EXP_NEG, TO_EXP_POS, MIN_EXP, MAX_EXP, and + // the arguments to toExponential, toFixed, toFormat, and toPrecision. + MAX = 1E9; // 0 to MAX_INT32 + + + /* + * Create and return a BigNumber constructor. + */ + function clone(configObject) { + var div, convertBase, parseNumeric, + P = BigNumber.prototype = { constructor: BigNumber, toString: null, valueOf: null }, + ONE = new BigNumber(1), + + + //----------------------------- EDITABLE CONFIG DEFAULTS ------------------------------- + + + // The default values below must be integers within the inclusive ranges stated. + // The values can also be changed at run-time using BigNumber.set. + + // The maximum number of decimal places for operations involving division. + DECIMAL_PLACES = 20, // 0 to MAX + + // The rounding mode used when rounding to the above decimal places, and when using + // toExponential, toFixed, toFormat and toPrecision, and round (default value). + // UP 0 Away from zero. + // DOWN 1 Towards zero. + // CEIL 2 Towards +Infinity. + // FLOOR 3 Towards -Infinity. + // HALF_UP 4 Towards nearest neighbour. If equidistant, up. + // HALF_DOWN 5 Towards nearest neighbour. If equidistant, down. + // HALF_EVEN 6 Towards nearest neighbour. If equidistant, towards even neighbour. + // HALF_CEIL 7 Towards nearest neighbour. If equidistant, towards +Infinity. + // HALF_FLOOR 8 Towards nearest neighbour. If equidistant, towards -Infinity. + ROUNDING_MODE = 4, // 0 to 8 + + // EXPONENTIAL_AT : [TO_EXP_NEG , TO_EXP_POS] + + // The exponent value at and beneath which toString returns exponential notation. + // Number type: -7 + TO_EXP_NEG = -7, // 0 to -MAX + + // The exponent value at and above which toString returns exponential notation. + // Number type: 21 + TO_EXP_POS = 21, // 0 to MAX + + // RANGE : [MIN_EXP, MAX_EXP] + + // The minimum exponent value, beneath which underflow to zero occurs. + // Number type: -324 (5e-324) + MIN_EXP = -1e7, // -1 to -MAX + + // The maximum exponent value, above which overflow to Infinity occurs. + // Number type: 308 (1.7976931348623157e+308) + // For MAX_EXP > 1e7, e.g. new BigNumber('1e100000000').plus(1) may be slow. + MAX_EXP = 1e7, // 1 to MAX + + // Whether to use cryptographically-secure random number generation, if available. + CRYPTO = false, // true or false + + // The modulo mode used when calculating the modulus: a mod n. + // The quotient (q = a / n) is calculated according to the corresponding rounding mode. + // The remainder (r) is calculated as: r = a - n * q. + // + // UP 0 The remainder is positive if the dividend is negative, else is negative. + // DOWN 1 The remainder has the same sign as the dividend. + // This modulo mode is commonly known as 'truncated division' and is + // equivalent to (a % n) in JavaScript. + // FLOOR 3 The remainder has the same sign as the divisor (Python %). + // HALF_EVEN 6 This modulo mode implements the IEEE 754 remainder function. + // EUCLID 9 Euclidian division. q = sign(n) * floor(a / abs(n)). + // The remainder is always positive. + // + // The truncated division, floored division, Euclidian division and IEEE 754 remainder + // modes are commonly used for the modulus operation. + // Although the other rounding modes can also be used, they may not give useful results. + MODULO_MODE = 1, // 0 to 9 + + // The maximum number of significant digits of the result of the exponentiatedBy operation. + // If POW_PRECISION is 0, there will be unlimited significant digits. + POW_PRECISION = 0, // 0 to MAX + + // The format specification used by the BigNumber.prototype.toFormat method. + FORMAT = { + prefix: '', + groupSize: 3, + secondaryGroupSize: 0, + groupSeparator: ',', + decimalSeparator: '.', + fractionGroupSize: 0, + fractionGroupSeparator: '\xA0', // non-breaking space + suffix: '' + }, + + // The alphabet used for base conversion. It must be at least 2 characters long, with no '+', + // '-', '.', whitespace, or repeated character. + // '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_' + ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz', + alphabetHasNormalDecimalDigits = true; + + + //------------------------------------------------------------------------------------------ + + + // CONSTRUCTOR + + + /* + * The BigNumber constructor and exported function. + * Create and return a new instance of a BigNumber object. + * + * v {number|string|BigNumber} A numeric value. + * [b] {number} The base of v. Integer, 2 to ALPHABET.length inclusive. + */ + function BigNumber(v, b) { + var alphabet, c, caseChanged, e, i, isNum, len, str, + x = this; + + // Enable constructor call without `new`. + if (!(x instanceof BigNumber)) return new BigNumber(v, b); + + if (b == null) { + + if (v && v._isBigNumber === true) { + x.s = v.s; + + if (!v.c || v.e > MAX_EXP) { + x.c = x.e = null; + } else if (v.e < MIN_EXP) { + x.c = [x.e = 0]; + } else { + x.e = v.e; + x.c = v.c.slice(); + } + + return; + } + + if ((isNum = typeof v == 'number') && v * 0 == 0) { + + // Use `1 / n` to handle minus zero also. + x.s = 1 / v < 0 ? (v = -v, -1) : 1; + + // Fast path for integers, where n < 2147483648 (2**31). + if (v === ~~v) { + for (e = 0, i = v; i >= 10; i /= 10, e++); + + if (e > MAX_EXP) { + x.c = x.e = null; + } else { + x.e = e; + x.c = [v]; + } + + return; + } + + str = String(v); + } else { + + if (!isNumeric.test(str = String(v))) return parseNumeric(x, str, isNum); + + x.s = str.charCodeAt(0) == 45 ? (str = str.slice(1), -1) : 1; + } + + // Decimal point? + if ((e = str.indexOf('.')) > -1) str = str.replace('.', ''); + + // Exponential form? + if ((i = str.search(/e/i)) > 0) { + + // Determine exponent. + if (e < 0) e = i; + e += +str.slice(i + 1); + str = str.substring(0, i); + } else if (e < 0) { + + // Integer. + e = str.length; + } + + } else { + + // '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}' + intCheck(b, 2, ALPHABET.length, 'Base'); + + // Allow exponential notation to be used with base 10 argument, while + // also rounding to DECIMAL_PLACES as with other bases. + if (b == 10 && alphabetHasNormalDecimalDigits) { + x = new BigNumber(v); + return round(x, DECIMAL_PLACES + x.e + 1, ROUNDING_MODE); + } + + str = String(v); + + if (isNum = typeof v == 'number') { + + // Avoid potential interpretation of Infinity and NaN as base 44+ values. + if (v * 0 != 0) return parseNumeric(x, str, isNum, b); + + x.s = 1 / v < 0 ? (str = str.slice(1), -1) : 1; + + // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}' + if (BigNumber.DEBUG && str.replace(/^0\.0*|\./, '').length > 15) { + throw Error + (tooManyDigits + v); + } + } else { + x.s = str.charCodeAt(0) === 45 ? (str = str.slice(1), -1) : 1; + } + + alphabet = ALPHABET.slice(0, b); + e = i = 0; + + // Check that str is a valid base b number. + // Don't use RegExp, so alphabet can contain special characters. + for (len = str.length; i < len; i++) { + if (alphabet.indexOf(c = str.charAt(i)) < 0) { + if (c == '.') { + + // If '.' is not the first character and it has not be found before. + if (i > e) { + e = len; + continue; + } + } else if (!caseChanged) { + + // Allow e.g. hexadecimal 'FF' as well as 'ff'. + if (str == str.toUpperCase() && (str = str.toLowerCase()) || + str == str.toLowerCase() && (str = str.toUpperCase())) { + caseChanged = true; + i = -1; + e = 0; + continue; + } + } + + return parseNumeric(x, String(v), isNum, b); + } + } + + // Prevent later check for length on converted number. + isNum = false; + str = convertBase(str, b, 10, x.s); + + // Decimal point? + if ((e = str.indexOf('.')) > -1) str = str.replace('.', ''); + else e = str.length; + } + + // Determine leading zeros. + for (i = 0; str.charCodeAt(i) === 48; i++); + + // Determine trailing zeros. + for (len = str.length; str.charCodeAt(--len) === 48;); + + if (str = str.slice(i, ++len)) { + len -= i; + + // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}' + if (isNum && BigNumber.DEBUG && + len > 15 && (v > MAX_SAFE_INTEGER || v !== mathfloor(v))) { + throw Error + (tooManyDigits + (x.s * v)); + } + + // Overflow? + if ((e = e - i - 1) > MAX_EXP) { + + // Infinity. + x.c = x.e = null; + + // Underflow? + } else if (e < MIN_EXP) { + + // Zero. + x.c = [x.e = 0]; + } else { + x.e = e; + x.c = []; + + // Transform base + + // e is the base 10 exponent. + // i is where to slice str to get the first element of the coefficient array. + i = (e + 1) % LOG_BASE; + if (e < 0) i += LOG_BASE; // i < 1 + + if (i < len) { + if (i) x.c.push(+str.slice(0, i)); + + for (len -= LOG_BASE; i < len;) { + x.c.push(+str.slice(i, i += LOG_BASE)); + } + + i = LOG_BASE - (str = str.slice(i)).length; + } else { + i -= len; + } + + for (; i--; str += '0'); + x.c.push(+str); + } + } else { + + // Zero. + x.c = [x.e = 0]; + } + } + + + // CONSTRUCTOR PROPERTIES + + + BigNumber.clone = clone; + + BigNumber.ROUND_UP = 0; + BigNumber.ROUND_DOWN = 1; + BigNumber.ROUND_CEIL = 2; + BigNumber.ROUND_FLOOR = 3; + BigNumber.ROUND_HALF_UP = 4; + BigNumber.ROUND_HALF_DOWN = 5; + BigNumber.ROUND_HALF_EVEN = 6; + BigNumber.ROUND_HALF_CEIL = 7; + BigNumber.ROUND_HALF_FLOOR = 8; + BigNumber.EUCLID = 9; + + + /* + * Configure infrequently-changing library-wide settings. + * + * Accept an object with the following optional properties (if the value of a property is + * a number, it must be an integer within the inclusive range stated): + * + * DECIMAL_PLACES {number} 0 to MAX + * ROUNDING_MODE {number} 0 to 8 + * EXPONENTIAL_AT {number|number[]} -MAX to MAX or [-MAX to 0, 0 to MAX] + * RANGE {number|number[]} -MAX to MAX (not zero) or [-MAX to -1, 1 to MAX] + * CRYPTO {boolean} true or false + * MODULO_MODE {number} 0 to 9 + * POW_PRECISION {number} 0 to MAX + * ALPHABET {string} A string of two or more unique characters which does + * not contain '.'. + * FORMAT {object} An object with some of the following properties: + * prefix {string} + * groupSize {number} + * secondaryGroupSize {number} + * groupSeparator {string} + * decimalSeparator {string} + * fractionGroupSize {number} + * fractionGroupSeparator {string} + * suffix {string} + * + * (The values assigned to the above FORMAT object properties are not checked for validity.) + * + * E.g. + * BigNumber.config({ DECIMAL_PLACES : 20, ROUNDING_MODE : 4 }) + * + * Ignore properties/parameters set to null or undefined, except for ALPHABET. + * + * Return an object with the properties current values. + */ + BigNumber.config = BigNumber.set = function (obj) { + var p, v; + + if (obj != null) { + + if (typeof obj == 'object') { + + // DECIMAL_PLACES {number} Integer, 0 to MAX inclusive. + // '[BigNumber Error] DECIMAL_PLACES {not a primitive number|not an integer|out of range}: {v}' + if (obj.hasOwnProperty(p = 'DECIMAL_PLACES')) { + v = obj[p]; + intCheck(v, 0, MAX, p); + DECIMAL_PLACES = v; + } + + // ROUNDING_MODE {number} Integer, 0 to 8 inclusive. + // '[BigNumber Error] ROUNDING_MODE {not a primitive number|not an integer|out of range}: {v}' + if (obj.hasOwnProperty(p = 'ROUNDING_MODE')) { + v = obj[p]; + intCheck(v, 0, 8, p); + ROUNDING_MODE = v; + } + + // EXPONENTIAL_AT {number|number[]} + // Integer, -MAX to MAX inclusive or + // [integer -MAX to 0 inclusive, 0 to MAX inclusive]. + // '[BigNumber Error] EXPONENTIAL_AT {not a primitive number|not an integer|out of range}: {v}' + if (obj.hasOwnProperty(p = 'EXPONENTIAL_AT')) { + v = obj[p]; + if (v && v.pop) { + intCheck(v[0], -MAX, 0, p); + intCheck(v[1], 0, MAX, p); + TO_EXP_NEG = v[0]; + TO_EXP_POS = v[1]; + } else { + intCheck(v, -MAX, MAX, p); + TO_EXP_NEG = -(TO_EXP_POS = v < 0 ? -v : v); + } + } + + // RANGE {number|number[]} Non-zero integer, -MAX to MAX inclusive or + // [integer -MAX to -1 inclusive, integer 1 to MAX inclusive]. + // '[BigNumber Error] RANGE {not a primitive number|not an integer|out of range|cannot be zero}: {v}' + if (obj.hasOwnProperty(p = 'RANGE')) { + v = obj[p]; + if (v && v.pop) { + intCheck(v[0], -MAX, -1, p); + intCheck(v[1], 1, MAX, p); + MIN_EXP = v[0]; + MAX_EXP = v[1]; + } else { + intCheck(v, -MAX, MAX, p); + if (v) { + MIN_EXP = -(MAX_EXP = v < 0 ? -v : v); + } else { + throw Error + (bignumberError + p + ' cannot be zero: ' + v); + } + } + } + + // CRYPTO {boolean} true or false. + // '[BigNumber Error] CRYPTO not true or false: {v}' + // '[BigNumber Error] crypto unavailable' + if (obj.hasOwnProperty(p = 'CRYPTO')) { + v = obj[p]; + if (v === !!v) { + if (v) { + if (typeof crypto != 'undefined' && crypto && + (crypto.getRandomValues || crypto.randomBytes)) { + CRYPTO = v; + } else { + CRYPTO = !v; + throw Error + (bignumberError + 'crypto unavailable'); + } + } else { + CRYPTO = v; + } + } else { + throw Error + (bignumberError + p + ' not true or false: ' + v); + } + } + + // MODULO_MODE {number} Integer, 0 to 9 inclusive. + // '[BigNumber Error] MODULO_MODE {not a primitive number|not an integer|out of range}: {v}' + if (obj.hasOwnProperty(p = 'MODULO_MODE')) { + v = obj[p]; + intCheck(v, 0, 9, p); + MODULO_MODE = v; + } + + // POW_PRECISION {number} Integer, 0 to MAX inclusive. + // '[BigNumber Error] POW_PRECISION {not a primitive number|not an integer|out of range}: {v}' + if (obj.hasOwnProperty(p = 'POW_PRECISION')) { + v = obj[p]; + intCheck(v, 0, MAX, p); + POW_PRECISION = v; + } + + // FORMAT {object} + // '[BigNumber Error] FORMAT not an object: {v}' + if (obj.hasOwnProperty(p = 'FORMAT')) { + v = obj[p]; + if (typeof v == 'object') FORMAT = v; + else throw Error + (bignumberError + p + ' not an object: ' + v); + } + + // ALPHABET {string} + // '[BigNumber Error] ALPHABET invalid: {v}' + if (obj.hasOwnProperty(p = 'ALPHABET')) { + v = obj[p]; + + // Disallow if less than two characters, + // or if it contains '+', '-', '.', whitespace, or a repeated character. + if (typeof v == 'string' && !/^.?$|[+\-.\s]|(.).*\1/.test(v)) { + alphabetHasNormalDecimalDigits = v.slice(0, 10) == '0123456789'; + ALPHABET = v; + } else { + throw Error + (bignumberError + p + ' invalid: ' + v); + } + } + + } else { + + // '[BigNumber Error] Object expected: {v}' + throw Error + (bignumberError + 'Object expected: ' + obj); + } + } + + return { + DECIMAL_PLACES: DECIMAL_PLACES, + ROUNDING_MODE: ROUNDING_MODE, + EXPONENTIAL_AT: [TO_EXP_NEG, TO_EXP_POS], + RANGE: [MIN_EXP, MAX_EXP], + CRYPTO: CRYPTO, + MODULO_MODE: MODULO_MODE, + POW_PRECISION: POW_PRECISION, + FORMAT: FORMAT, + ALPHABET: ALPHABET + }; + }; + + + /* + * Return true if v is a BigNumber instance, otherwise return false. + * + * If BigNumber.DEBUG is true, throw if a BigNumber instance is not well-formed. + * + * v {any} + * + * '[BigNumber Error] Invalid BigNumber: {v}' + */ + BigNumber.isBigNumber = function (v) { + if (!v || v._isBigNumber !== true) return false; + if (!BigNumber.DEBUG) return true; + + var i, n, + c = v.c, + e = v.e, + s = v.s; + + out: if ({}.toString.call(c) == '[object Array]') { + + if ((s === 1 || s === -1) && e >= -MAX && e <= MAX && e === mathfloor(e)) { + + // If the first element is zero, the BigNumber value must be zero. + if (c[0] === 0) { + if (e === 0 && c.length === 1) return true; + break out; + } + + // Calculate number of digits that c[0] should have, based on the exponent. + i = (e + 1) % LOG_BASE; + if (i < 1) i += LOG_BASE; + + // Calculate number of digits of c[0]. + //if (Math.ceil(Math.log(c[0] + 1) / Math.LN10) == i) { + if (String(c[0]).length == i) { + + for (i = 0; i < c.length; i++) { + n = c[i]; + if (n < 0 || n >= BASE || n !== mathfloor(n)) break out; + } + + // Last element cannot be zero, unless it is the only element. + if (n !== 0) return true; + } + } + + // Infinity/NaN + } else if (c === null && e === null && (s === null || s === 1 || s === -1)) { + return true; + } + + throw Error + (bignumberError + 'Invalid BigNumber: ' + v); + }; + + + /* + * Return a new BigNumber whose value is the maximum of the arguments. + * + * arguments {number|string|BigNumber} + */ + BigNumber.maximum = BigNumber.max = function () { + return maxOrMin(arguments, -1); + }; + + + /* + * Return a new BigNumber whose value is the minimum of the arguments. + * + * arguments {number|string|BigNumber} + */ + BigNumber.minimum = BigNumber.min = function () { + return maxOrMin(arguments, 1); + }; + + + /* + * Return a new BigNumber with a random value equal to or greater than 0 and less than 1, + * and with dp, or DECIMAL_PLACES if dp is omitted, decimal places (or less if trailing + * zeros are produced). + * + * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp}' + * '[BigNumber Error] crypto unavailable' + */ + BigNumber.random = (function () { + var pow2_53 = 0x20000000000000; + + // Return a 53 bit integer n, where 0 <= n < 9007199254740992. + // Check if Math.random() produces more than 32 bits of randomness. + // If it does, assume at least 53 bits are produced, otherwise assume at least 30 bits. + // 0x40000000 is 2^30, 0x800000 is 2^23, 0x1fffff is 2^21 - 1. + var random53bitInt = (Math.random() * pow2_53) & 0x1fffff + ? function () { return mathfloor(Math.random() * pow2_53); } + : function () { return ((Math.random() * 0x40000000 | 0) * 0x800000) + + (Math.random() * 0x800000 | 0); }; + + return function (dp) { + var a, b, e, k, v, + i = 0, + c = [], + rand = new BigNumber(ONE); + + if (dp == null) dp = DECIMAL_PLACES; + else intCheck(dp, 0, MAX); + + k = mathceil(dp / LOG_BASE); + + if (CRYPTO) { + + // Browsers supporting crypto.getRandomValues. + if (crypto.getRandomValues) { + + a = crypto.getRandomValues(new Uint32Array(k *= 2)); + + for (; i < k;) { + + // 53 bits: + // ((Math.pow(2, 32) - 1) * Math.pow(2, 21)).toString(2) + // 11111 11111111 11111111 11111111 11100000 00000000 00000000 + // ((Math.pow(2, 32) - 1) >>> 11).toString(2) + // 11111 11111111 11111111 + // 0x20000 is 2^21. + v = a[i] * 0x20000 + (a[i + 1] >>> 11); + + // Rejection sampling: + // 0 <= v < 9007199254740992 + // Probability that v >= 9e15, is + // 7199254740992 / 9007199254740992 ~= 0.0008, i.e. 1 in 1251 + if (v >= 9e15) { + b = crypto.getRandomValues(new Uint32Array(2)); + a[i] = b[0]; + a[i + 1] = b[1]; + } else { + + // 0 <= v <= 8999999999999999 + // 0 <= (v % 1e14) <= 99999999999999 + c.push(v % 1e14); + i += 2; + } + } + i = k / 2; + + // Node.js supporting crypto.randomBytes. + } else if (crypto.randomBytes) { + + // buffer + a = crypto.randomBytes(k *= 7); + + for (; i < k;) { + + // 0x1000000000000 is 2^48, 0x10000000000 is 2^40 + // 0x100000000 is 2^32, 0x1000000 is 2^24 + // 11111 11111111 11111111 11111111 11111111 11111111 11111111 + // 0 <= v < 9007199254740992 + v = ((a[i] & 31) * 0x1000000000000) + (a[i + 1] * 0x10000000000) + + (a[i + 2] * 0x100000000) + (a[i + 3] * 0x1000000) + + (a[i + 4] << 16) + (a[i + 5] << 8) + a[i + 6]; + + if (v >= 9e15) { + crypto.randomBytes(7).copy(a, i); + } else { + + // 0 <= (v % 1e14) <= 99999999999999 + c.push(v % 1e14); + i += 7; + } + } + i = k / 7; + } else { + CRYPTO = false; + throw Error + (bignumberError + 'crypto unavailable'); + } + } + + // Use Math.random. + if (!CRYPTO) { + + for (; i < k;) { + v = random53bitInt(); + if (v < 9e15) c[i++] = v % 1e14; + } + } + + k = c[--i]; + dp %= LOG_BASE; + + // Convert trailing digits to zeros according to dp. + if (k && dp) { + v = POWS_TEN[LOG_BASE - dp]; + c[i] = mathfloor(k / v) * v; + } + + // Remove trailing elements which are zero. + for (; c[i] === 0; c.pop(), i--); + + // Zero? + if (i < 0) { + c = [e = 0]; + } else { + + // Remove leading elements which are zero and adjust exponent accordingly. + for (e = -1 ; c[0] === 0; c.splice(0, 1), e -= LOG_BASE); + + // Count the digits of the first element of c to determine leading zeros, and... + for (i = 1, v = c[0]; v >= 10; v /= 10, i++); + + // adjust the exponent accordingly. + if (i < LOG_BASE) e -= LOG_BASE - i; + } + + rand.e = e; + rand.c = c; + return rand; + }; + })(); + + + /* + * Return a BigNumber whose value is the sum of the arguments. + * + * arguments {number|string|BigNumber} + */ + BigNumber.sum = function () { + var i = 1, + args = arguments, + sum = new BigNumber(args[0]); + for (; i < args.length;) sum = sum.plus(args[i++]); + return sum; + }; + + + // PRIVATE FUNCTIONS + + + // Called by BigNumber and BigNumber.prototype.toString. + convertBase = (function () { + var decimal = '0123456789'; + + /* + * Convert string of baseIn to an array of numbers of baseOut. + * Eg. toBaseOut('255', 10, 16) returns [15, 15]. + * Eg. toBaseOut('ff', 16, 10) returns [2, 5, 5]. + */ + function toBaseOut(str, baseIn, baseOut, alphabet) { + var j, + arr = [0], + arrL, + i = 0, + len = str.length; + + for (; i < len;) { + for (arrL = arr.length; arrL--; arr[arrL] *= baseIn); + + arr[0] += alphabet.indexOf(str.charAt(i++)); + + for (j = 0; j < arr.length; j++) { + + if (arr[j] > baseOut - 1) { + if (arr[j + 1] == null) arr[j + 1] = 0; + arr[j + 1] += arr[j] / baseOut | 0; + arr[j] %= baseOut; + } + } + } + + return arr.reverse(); + } + + // Convert a numeric string of baseIn to a numeric string of baseOut. + // If the caller is toString, we are converting from base 10 to baseOut. + // If the caller is BigNumber, we are converting from baseIn to base 10. + return function (str, baseIn, baseOut, sign, callerIsToString) { + var alphabet, d, e, k, r, x, xc, y, + i = str.indexOf('.'), + dp = DECIMAL_PLACES, + rm = ROUNDING_MODE; + + // Non-integer. + if (i >= 0) { + k = POW_PRECISION; + + // Unlimited precision. + POW_PRECISION = 0; + str = str.replace('.', ''); + y = new BigNumber(baseIn); + x = y.pow(str.length - i); + POW_PRECISION = k; + + // Convert str as if an integer, then restore the fraction part by dividing the + // result by its base raised to a power. + + y.c = toBaseOut(toFixedPoint(coeffToString(x.c), x.e, '0'), + 10, baseOut, decimal); + y.e = y.c.length; + } + + // Convert the number as integer. + + xc = toBaseOut(str, baseIn, baseOut, callerIsToString + ? (alphabet = ALPHABET, decimal) + : (alphabet = decimal, ALPHABET)); + + // xc now represents str as an integer and converted to baseOut. e is the exponent. + e = k = xc.length; + + // Remove trailing zeros. + for (; xc[--k] == 0; xc.pop()); + + // Zero? + if (!xc[0]) return alphabet.charAt(0); + + // Does str represent an integer? If so, no need for the division. + if (i < 0) { + --e; + } else { + x.c = xc; + x.e = e; + + // The sign is needed for correct rounding. + x.s = sign; + x = div(x, y, dp, rm, baseOut); + xc = x.c; + r = x.r; + e = x.e; + } + + // xc now represents str converted to baseOut. + + // THe index of the rounding digit. + d = e + dp + 1; + + // The rounding digit: the digit to the right of the digit that may be rounded up. + i = xc[d]; + + // Look at the rounding digits and mode to determine whether to round up. + + k = baseOut / 2; + r = r || d < 0 || xc[d + 1] != null; + + r = rm < 4 ? (i != null || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2)) + : i > k || i == k &&(rm == 4 || r || rm == 6 && xc[d - 1] & 1 || + rm == (x.s < 0 ? 8 : 7)); + + // If the index of the rounding digit is not greater than zero, or xc represents + // zero, then the result of the base conversion is zero or, if rounding up, a value + // such as 0.00001. + if (d < 1 || !xc[0]) { + + // 1^-dp or 0 + str = r ? toFixedPoint(alphabet.charAt(1), -dp, alphabet.charAt(0)) : alphabet.charAt(0); + } else { + + // Truncate xc to the required number of decimal places. + xc.length = d; + + // Round up? + if (r) { + + // Rounding up may mean the previous digit has to be rounded up and so on. + for (--baseOut; ++xc[--d] > baseOut;) { + xc[d] = 0; + + if (!d) { + ++e; + xc = [1].concat(xc); + } + } + } + + // Determine trailing zeros. + for (k = xc.length; !xc[--k];); + + // E.g. [4, 11, 15] becomes 4bf. + for (i = 0, str = ''; i <= k; str += alphabet.charAt(xc[i++])); + + // Add leading zeros, decimal point and trailing zeros as required. + str = toFixedPoint(str, e, alphabet.charAt(0)); + } + + // The caller will add the sign. + return str; + }; + })(); + + + // Perform division in the specified base. Called by div and convertBase. + div = (function () { + + // Assume non-zero x and k. + function multiply(x, k, base) { + var m, temp, xlo, xhi, + carry = 0, + i = x.length, + klo = k % SQRT_BASE, + khi = k / SQRT_BASE | 0; + + for (x = x.slice(); i--;) { + xlo = x[i] % SQRT_BASE; + xhi = x[i] / SQRT_BASE | 0; + m = khi * xlo + xhi * klo; + temp = klo * xlo + ((m % SQRT_BASE) * SQRT_BASE) + carry; + carry = (temp / base | 0) + (m / SQRT_BASE | 0) + khi * xhi; + x[i] = temp % base; + } + + if (carry) x = [carry].concat(x); + + return x; + } + + function compare(a, b, aL, bL) { + var i, cmp; + + if (aL != bL) { + cmp = aL > bL ? 1 : -1; + } else { + + for (i = cmp = 0; i < aL; i++) { + + if (a[i] != b[i]) { + cmp = a[i] > b[i] ? 1 : -1; + break; + } + } + } + + return cmp; + } + + function subtract(a, b, aL, base) { + var i = 0; + + // Subtract b from a. + for (; aL--;) { + a[aL] -= i; + i = a[aL] < b[aL] ? 1 : 0; + a[aL] = i * base + a[aL] - b[aL]; + } + + // Remove leading zeros. + for (; !a[0] && a.length > 1; a.splice(0, 1)); + } + + // x: dividend, y: divisor. + return function (x, y, dp, rm, base) { + var cmp, e, i, more, n, prod, prodL, q, qc, rem, remL, rem0, xi, xL, yc0, + yL, yz, + s = x.s == y.s ? 1 : -1, + xc = x.c, + yc = y.c; + + // Either NaN, Infinity or 0? + if (!xc || !xc[0] || !yc || !yc[0]) { + + return new BigNumber( + + // Return NaN if either NaN, or both Infinity or 0. + !x.s || !y.s || (xc ? yc && xc[0] == yc[0] : !yc) ? NaN : + + // Return ±0 if x is ±0 or y is ±Infinity, or return ±Infinity as y is ±0. + xc && xc[0] == 0 || !yc ? s * 0 : s / 0 + ); + } + + q = new BigNumber(s); + qc = q.c = []; + e = x.e - y.e; + s = dp + e + 1; + + if (!base) { + base = BASE; + e = bitFloor(x.e / LOG_BASE) - bitFloor(y.e / LOG_BASE); + s = s / LOG_BASE | 0; + } + + // Result exponent may be one less then the current value of e. + // The coefficients of the BigNumbers from convertBase may have trailing zeros. + for (i = 0; yc[i] == (xc[i] || 0); i++); + + if (yc[i] > (xc[i] || 0)) e--; + + if (s < 0) { + qc.push(1); + more = true; + } else { + xL = xc.length; + yL = yc.length; + i = 0; + s += 2; + + // Normalise xc and yc so highest order digit of yc is >= base / 2. + + n = mathfloor(base / (yc[0] + 1)); + + // Not necessary, but to handle odd bases where yc[0] == (base / 2) - 1. + // if (n > 1 || n++ == 1 && yc[0] < base / 2) { + if (n > 1) { + yc = multiply(yc, n, base); + xc = multiply(xc, n, base); + yL = yc.length; + xL = xc.length; + } + + xi = yL; + rem = xc.slice(0, yL); + remL = rem.length; + + // Add zeros to make remainder as long as divisor. + for (; remL < yL; rem[remL++] = 0); + yz = yc.slice(); + yz = [0].concat(yz); + yc0 = yc[0]; + if (yc[1] >= base / 2) yc0++; + // Not necessary, but to prevent trial digit n > base, when using base 3. + // else if (base == 3 && yc0 == 1) yc0 = 1 + 1e-15; + + do { + n = 0; + + // Compare divisor and remainder. + cmp = compare(yc, rem, yL, remL); + + // If divisor < remainder. + if (cmp < 0) { + + // Calculate trial digit, n. + + rem0 = rem[0]; + if (yL != remL) rem0 = rem0 * base + (rem[1] || 0); + + // n is how many times the divisor goes into the current remainder. + n = mathfloor(rem0 / yc0); + + // Algorithm: + // product = divisor multiplied by trial digit (n). + // Compare product and remainder. + // If product is greater than remainder: + // Subtract divisor from product, decrement trial digit. + // Subtract product from remainder. + // If product was less than remainder at the last compare: + // Compare new remainder and divisor. + // If remainder is greater than divisor: + // Subtract divisor from remainder, increment trial digit. + + if (n > 1) { + + // n may be > base only when base is 3. + if (n >= base) n = base - 1; + + // product = divisor * trial digit. + prod = multiply(yc, n, base); + prodL = prod.length; + remL = rem.length; + + // Compare product and remainder. + // If product > remainder then trial digit n too high. + // n is 1 too high about 5% of the time, and is not known to have + // ever been more than 1 too high. + while (compare(prod, rem, prodL, remL) == 1) { + n--; + + // Subtract divisor from product. + subtract(prod, yL < prodL ? yz : yc, prodL, base); + prodL = prod.length; + cmp = 1; + } + } else { + + // n is 0 or 1, cmp is -1. + // If n is 0, there is no need to compare yc and rem again below, + // so change cmp to 1 to avoid it. + // If n is 1, leave cmp as -1, so yc and rem are compared again. + if (n == 0) { + + // divisor < remainder, so n must be at least 1. + cmp = n = 1; + } + + // product = divisor + prod = yc.slice(); + prodL = prod.length; + } + + if (prodL < remL) prod = [0].concat(prod); + + // Subtract product from remainder. + subtract(rem, prod, remL, base); + remL = rem.length; + + // If product was < remainder. + if (cmp == -1) { + + // Compare divisor and new remainder. + // If divisor < new remainder, subtract divisor from remainder. + // Trial digit n too low. + // n is 1 too low about 5% of the time, and very rarely 2 too low. + while (compare(yc, rem, yL, remL) < 1) { + n++; + + // Subtract divisor from remainder. + subtract(rem, yL < remL ? yz : yc, remL, base); + remL = rem.length; + } + } + } else if (cmp === 0) { + n++; + rem = [0]; + } // else cmp === 1 and n will be 0 + + // Add the next digit, n, to the result array. + qc[i++] = n; + + // Update the remainder. + if (rem[0]) { + rem[remL++] = xc[xi] || 0; + } else { + rem = [xc[xi]]; + remL = 1; + } + } while ((xi++ < xL || rem[0] != null) && s--); + + more = rem[0] != null; + + // Leading zero? + if (!qc[0]) qc.splice(0, 1); + } + + if (base == BASE) { + + // To calculate q.e, first get the number of digits of qc[0]. + for (i = 1, s = qc[0]; s >= 10; s /= 10, i++); + + round(q, dp + (q.e = i + e * LOG_BASE - 1) + 1, rm, more); + + // Caller is convertBase. + } else { + q.e = e; + q.r = +more; + } + + return q; + }; + })(); + + + /* + * Return a string representing the value of BigNumber n in fixed-point or exponential + * notation rounded to the specified decimal places or significant digits. + * + * n: a BigNumber. + * i: the index of the last digit required (i.e. the digit that may be rounded up). + * rm: the rounding mode. + * id: 1 (toExponential) or 2 (toPrecision). + */ + function format(n, i, rm, id) { + var c0, e, ne, len, str; + + if (rm == null) rm = ROUNDING_MODE; + else intCheck(rm, 0, 8); + + if (!n.c) return n.toString(); + + c0 = n.c[0]; + ne = n.e; + + if (i == null) { + str = coeffToString(n.c); + str = id == 1 || id == 2 && (ne <= TO_EXP_NEG || ne >= TO_EXP_POS) + ? toExponential(str, ne) + : toFixedPoint(str, ne, '0'); + } else { + n = round(new BigNumber(n), i, rm); + + // n.e may have changed if the value was rounded up. + e = n.e; + + str = coeffToString(n.c); + len = str.length; + + // toPrecision returns exponential notation if the number of significant digits + // specified is less than the number of digits necessary to represent the integer + // part of the value in fixed-point notation. + + // Exponential notation. + if (id == 1 || id == 2 && (i <= e || e <= TO_EXP_NEG)) { + + // Append zeros? + for (; len < i; str += '0', len++); + str = toExponential(str, e); + + // Fixed-point notation. + } else { + i -= ne; + str = toFixedPoint(str, e, '0'); + + // Append zeros? + if (e + 1 > len) { + if (--i > 0) for (str += '.'; i--; str += '0'); + } else { + i += e - len; + if (i > 0) { + if (e + 1 == len) str += '.'; + for (; i--; str += '0'); + } + } + } + } + + return n.s < 0 && c0 ? '-' + str : str; + } + + + // Handle BigNumber.max and BigNumber.min. + // If any number is NaN, return NaN. + function maxOrMin(args, n) { + var k, y, + i = 1, + x = new BigNumber(args[0]); + + for (; i < args.length; i++) { + y = new BigNumber(args[i]); + if (!y.s || (k = compare(x, y)) === n || k === 0 && x.s === n) { + x = y; + } + } + + return x; + } + + + /* + * Strip trailing zeros, calculate base 10 exponent and check against MIN_EXP and MAX_EXP. + * Called by minus, plus and times. + */ + function normalise(n, c, e) { + var i = 1, + j = c.length; + + // Remove trailing zeros. + for (; !c[--j]; c.pop()); + + // Calculate the base 10 exponent. First get the number of digits of c[0]. + for (j = c[0]; j >= 10; j /= 10, i++); + + // Overflow? + if ((e = i + e * LOG_BASE - 1) > MAX_EXP) { + + // Infinity. + n.c = n.e = null; + + // Underflow? + } else if (e < MIN_EXP) { + + // Zero. + n.c = [n.e = 0]; + } else { + n.e = e; + n.c = c; + } + + return n; + } + + + // Handle values that fail the validity test in BigNumber. + parseNumeric = (function () { + var basePrefix = /^(-?)0([xbo])(?=\w[\w.]*$)/i, + dotAfter = /^([^.]+)\.$/, + dotBefore = /^\.([^.]+)$/, + isInfinityOrNaN = /^-?(Infinity|NaN)$/, + whitespaceOrPlus = /^\s*\+(?=[\w.])|^\s+|\s+$/g; + + return function (x, str, isNum, b) { + var base, + s = isNum ? str : str.replace(whitespaceOrPlus, ''); + + // No exception on ±Infinity or NaN. + if (isInfinityOrNaN.test(s)) { + x.s = isNaN(s) ? null : s < 0 ? -1 : 1; + } else { + if (!isNum) { + + // basePrefix = /^(-?)0([xbo])(?=\w[\w.]*$)/i + s = s.replace(basePrefix, function (m, p1, p2) { + base = (p2 = p2.toLowerCase()) == 'x' ? 16 : p2 == 'b' ? 2 : 8; + return !b || b == base ? p1 : m; + }); + + if (b) { + base = b; + + // E.g. '1.' to '1', '.1' to '0.1' + s = s.replace(dotAfter, '$1').replace(dotBefore, '0.$1'); + } + + if (str != s) return new BigNumber(s, base); + } + + // '[BigNumber Error] Not a number: {n}' + // '[BigNumber Error] Not a base {b} number: {n}' + if (BigNumber.DEBUG) { + throw Error + (bignumberError + 'Not a' + (b ? ' base ' + b : '') + ' number: ' + str); + } + + // NaN + x.s = null; + } + + x.c = x.e = null; + } + })(); + + + /* + * Round x to sd significant digits using rounding mode rm. Check for over/under-flow. + * If r is truthy, it is known that there are more digits after the rounding digit. + */ + function round(x, sd, rm, r) { + var d, i, j, k, n, ni, rd, + xc = x.c, + pows10 = POWS_TEN; + + // if x is not Infinity or NaN... + if (xc) { + + // rd is the rounding digit, i.e. the digit after the digit that may be rounded up. + // n is a base 1e14 number, the value of the element of array x.c containing rd. + // ni is the index of n within x.c. + // d is the number of digits of n. + // i is the index of rd within n including leading zeros. + // j is the actual index of rd within n (if < 0, rd is a leading zero). + out: { + + // Get the number of digits of the first element of xc. + for (d = 1, k = xc[0]; k >= 10; k /= 10, d++); + i = sd - d; + + // If the rounding digit is in the first element of xc... + if (i < 0) { + i += LOG_BASE; + j = sd; + n = xc[ni = 0]; + + // Get the rounding digit at index j of n. + rd = mathfloor(n / pows10[d - j - 1] % 10); + } else { + ni = mathceil((i + 1) / LOG_BASE); + + if (ni >= xc.length) { + + if (r) { + + // Needed by sqrt. + for (; xc.length <= ni; xc.push(0)); + n = rd = 0; + d = 1; + i %= LOG_BASE; + j = i - LOG_BASE + 1; + } else { + break out; + } + } else { + n = k = xc[ni]; + + // Get the number of digits of n. + for (d = 1; k >= 10; k /= 10, d++); + + // Get the index of rd within n. + i %= LOG_BASE; + + // Get the index of rd within n, adjusted for leading zeros. + // The number of leading zeros of n is given by LOG_BASE - d. + j = i - LOG_BASE + d; + + // Get the rounding digit at index j of n. + rd = j < 0 ? 0 : mathfloor(n / pows10[d - j - 1] % 10); + } + } + + r = r || sd < 0 || + + // Are there any non-zero digits after the rounding digit? + // The expression n % pows10[d - j - 1] returns all digits of n to the right + // of the digit at j, e.g. if n is 908714 and j is 2, the expression gives 714. + xc[ni + 1] != null || (j < 0 ? n : n % pows10[d - j - 1]); + + r = rm < 4 + ? (rd || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2)) + : rd > 5 || rd == 5 && (rm == 4 || r || rm == 6 && + + // Check whether the digit to the left of the rounding digit is odd. + ((i > 0 ? j > 0 ? n / pows10[d - j] : 0 : xc[ni - 1]) % 10) & 1 || + rm == (x.s < 0 ? 8 : 7)); + + if (sd < 1 || !xc[0]) { + xc.length = 0; + + if (r) { + + // Convert sd to decimal places. + sd -= x.e + 1; + + // 1, 0.1, 0.01, 0.001, 0.0001 etc. + xc[0] = pows10[(LOG_BASE - sd % LOG_BASE) % LOG_BASE]; + x.e = -sd || 0; + } else { + + // Zero. + xc[0] = x.e = 0; + } + + return x; + } + + // Remove excess digits. + if (i == 0) { + xc.length = ni; + k = 1; + ni--; + } else { + xc.length = ni + 1; + k = pows10[LOG_BASE - i]; + + // E.g. 56700 becomes 56000 if 7 is the rounding digit. + // j > 0 means i > number of leading zeros of n. + xc[ni] = j > 0 ? mathfloor(n / pows10[d - j] % pows10[j]) * k : 0; + } + + // Round up? + if (r) { + + for (; ;) { + + // If the digit to be rounded up is in the first element of xc... + if (ni == 0) { + + // i will be the length of xc[0] before k is added. + for (i = 1, j = xc[0]; j >= 10; j /= 10, i++); + j = xc[0] += k; + for (k = 1; j >= 10; j /= 10, k++); + + // if i != k the length has increased. + if (i != k) { + x.e++; + if (xc[0] == BASE) xc[0] = 1; + } + + break; + } else { + xc[ni] += k; + if (xc[ni] != BASE) break; + xc[ni--] = 0; + k = 1; + } + } + } + + // Remove trailing zeros. + for (i = xc.length; xc[--i] === 0; xc.pop()); + } + + // Overflow? Infinity. + if (x.e > MAX_EXP) { + x.c = x.e = null; + + // Underflow? Zero. + } else if (x.e < MIN_EXP) { + x.c = [x.e = 0]; + } + } + + return x; + } + + + function valueOf(n) { + var str, + e = n.e; + + if (e === null) return n.toString(); + + str = coeffToString(n.c); + + str = e <= TO_EXP_NEG || e >= TO_EXP_POS + ? toExponential(str, e) + : toFixedPoint(str, e, '0'); + + return n.s < 0 ? '-' + str : str; + } + + + // PROTOTYPE/INSTANCE METHODS + + + /* + * Return a new BigNumber whose value is the absolute value of this BigNumber. + */ + P.absoluteValue = P.abs = function () { + var x = new BigNumber(this); + if (x.s < 0) x.s = 1; + return x; + }; + + + /* + * Return + * 1 if the value of this BigNumber is greater than the value of BigNumber(y, b), + * -1 if the value of this BigNumber is less than the value of BigNumber(y, b), + * 0 if they have the same value, + * or null if the value of either is NaN. + */ + P.comparedTo = function (y, b) { + return compare(this, new BigNumber(y, b)); + }; + + + /* + * If dp is undefined or null or true or false, return the number of decimal places of the + * value of this BigNumber, or null if the value of this BigNumber is ±Infinity or NaN. + * + * Otherwise, if dp is a number, return a new BigNumber whose value is the value of this + * BigNumber rounded to a maximum of dp decimal places using rounding mode rm, or + * ROUNDING_MODE if rm is omitted. + * + * [dp] {number} Decimal places: integer, 0 to MAX inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' + */ + P.decimalPlaces = P.dp = function (dp, rm) { + var c, n, v, + x = this; + + if (dp != null) { + intCheck(dp, 0, MAX); + if (rm == null) rm = ROUNDING_MODE; + else intCheck(rm, 0, 8); + + return round(new BigNumber(x), dp + x.e + 1, rm); + } + + if (!(c = x.c)) return null; + n = ((v = c.length - 1) - bitFloor(this.e / LOG_BASE)) * LOG_BASE; + + // Subtract the number of trailing zeros of the last number. + if (v = c[v]) for (; v % 10 == 0; v /= 10, n--); + if (n < 0) n = 0; + + return n; + }; + + + /* + * n / 0 = I + * n / N = N + * n / I = 0 + * 0 / n = 0 + * 0 / 0 = N + * 0 / N = N + * 0 / I = 0 + * N / n = N + * N / 0 = N + * N / N = N + * N / I = N + * I / n = I + * I / 0 = I + * I / N = N + * I / I = N + * + * Return a new BigNumber whose value is the value of this BigNumber divided by the value of + * BigNumber(y, b), rounded according to DECIMAL_PLACES and ROUNDING_MODE. + */ + P.dividedBy = P.div = function (y, b) { + return div(this, new BigNumber(y, b), DECIMAL_PLACES, ROUNDING_MODE); + }; + + + /* + * Return a new BigNumber whose value is the integer part of dividing the value of this + * BigNumber by the value of BigNumber(y, b). + */ + P.dividedToIntegerBy = P.idiv = function (y, b) { + return div(this, new BigNumber(y, b), 0, 1); + }; + + + /* + * Return a BigNumber whose value is the value of this BigNumber exponentiated by n. + * + * If m is present, return the result modulo m. + * If n is negative round according to DECIMAL_PLACES and ROUNDING_MODE. + * If POW_PRECISION is non-zero and m is not present, round to POW_PRECISION using ROUNDING_MODE. + * + * The modular power operation works efficiently when x, n, and m are integers, otherwise it + * is equivalent to calculating x.exponentiatedBy(n).modulo(m) with a POW_PRECISION of 0. + * + * n {number|string|BigNumber} The exponent. An integer. + * [m] {number|string|BigNumber} The modulus. + * + * '[BigNumber Error] Exponent not an integer: {n}' + */ + P.exponentiatedBy = P.pow = function (n, m) { + var half, isModExp, i, k, more, nIsBig, nIsNeg, nIsOdd, y, + x = this; + + n = new BigNumber(n); + + // Allow NaN and ±Infinity, but not other non-integers. + if (n.c && !n.isInteger()) { + throw Error + (bignumberError + 'Exponent not an integer: ' + valueOf(n)); + } + + if (m != null) m = new BigNumber(m); + + // Exponent of MAX_SAFE_INTEGER is 15. + nIsBig = n.e > 14; + + // If x is NaN, ±Infinity, ±0 or ±1, or n is ±Infinity, NaN or ±0. + if (!x.c || !x.c[0] || x.c[0] == 1 && !x.e && x.c.length == 1 || !n.c || !n.c[0]) { + + // The sign of the result of pow when x is negative depends on the evenness of n. + // If +n overflows to ±Infinity, the evenness of n would be not be known. + y = new BigNumber(Math.pow(+valueOf(x), nIsBig ? n.s * (2 - isOdd(n)) : +valueOf(n))); + return m ? y.mod(m) : y; + } + + nIsNeg = n.s < 0; + + if (m) { + + // x % m returns NaN if abs(m) is zero, or m is NaN. + if (m.c ? !m.c[0] : !m.s) return new BigNumber(NaN); + + isModExp = !nIsNeg && x.isInteger() && m.isInteger(); + + if (isModExp) x = x.mod(m); + + // Overflow to ±Infinity: >=2**1e10 or >=1.0000024**1e15. + // Underflow to ±0: <=0.79**1e10 or <=0.9999975**1e15. + } else if (n.e > 9 && (x.e > 0 || x.e < -1 || (x.e == 0 + // [1, 240000000] + ? x.c[0] > 1 || nIsBig && x.c[1] >= 24e7 + // [80000000000000] [99999750000000] + : x.c[0] < 8e13 || nIsBig && x.c[0] <= 9999975e7))) { + + // If x is negative and n is odd, k = -0, else k = 0. + k = x.s < 0 && isOdd(n) ? -0 : 0; + + // If x >= 1, k = ±Infinity. + if (x.e > -1) k = 1 / k; + + // If n is negative return ±0, else return ±Infinity. + return new BigNumber(nIsNeg ? 1 / k : k); + + } else if (POW_PRECISION) { + + // Truncating each coefficient array to a length of k after each multiplication + // equates to truncating significant digits to POW_PRECISION + [28, 41], + // i.e. there will be a minimum of 28 guard digits retained. + k = mathceil(POW_PRECISION / LOG_BASE + 2); + } + + if (nIsBig) { + half = new BigNumber(0.5); + if (nIsNeg) n.s = 1; + nIsOdd = isOdd(n); + } else { + i = Math.abs(+valueOf(n)); + nIsOdd = i % 2; + } + + y = new BigNumber(ONE); + + // Performs 54 loop iterations for n of 9007199254740991. + for (; ;) { + + if (nIsOdd) { + y = y.times(x); + if (!y.c) break; + + if (k) { + if (y.c.length > k) y.c.length = k; + } else if (isModExp) { + y = y.mod(m); //y = y.minus(div(y, m, 0, MODULO_MODE).times(m)); + } + } + + if (i) { + i = mathfloor(i / 2); + if (i === 0) break; + nIsOdd = i % 2; + } else { + n = n.times(half); + round(n, n.e + 1, 1); + + if (n.e > 14) { + nIsOdd = isOdd(n); + } else { + i = +valueOf(n); + if (i === 0) break; + nIsOdd = i % 2; + } + } + + x = x.times(x); + + if (k) { + if (x.c && x.c.length > k) x.c.length = k; + } else if (isModExp) { + x = x.mod(m); //x = x.minus(div(x, m, 0, MODULO_MODE).times(m)); + } + } + + if (isModExp) return y; + if (nIsNeg) y = ONE.div(y); + + return m ? y.mod(m) : k ? round(y, POW_PRECISION, ROUNDING_MODE, more) : y; + }; + + + /* + * Return a new BigNumber whose value is the value of this BigNumber rounded to an integer + * using rounding mode rm, or ROUNDING_MODE if rm is omitted. + * + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {rm}' + */ + P.integerValue = function (rm) { + var n = new BigNumber(this); + if (rm == null) rm = ROUNDING_MODE; + else intCheck(rm, 0, 8); + return round(n, n.e + 1, rm); + }; + + + /* + * Return true if the value of this BigNumber is equal to the value of BigNumber(y, b), + * otherwise return false. + */ + P.isEqualTo = P.eq = function (y, b) { + return compare(this, new BigNumber(y, b)) === 0; + }; + + + /* + * Return true if the value of this BigNumber is a finite number, otherwise return false. + */ + P.isFinite = function () { + return !!this.c; + }; + + + /* + * Return true if the value of this BigNumber is greater than the value of BigNumber(y, b), + * otherwise return false. + */ + P.isGreaterThan = P.gt = function (y, b) { + return compare(this, new BigNumber(y, b)) > 0; + }; + + + /* + * Return true if the value of this BigNumber is greater than or equal to the value of + * BigNumber(y, b), otherwise return false. + */ + P.isGreaterThanOrEqualTo = P.gte = function (y, b) { + return (b = compare(this, new BigNumber(y, b))) === 1 || b === 0; + + }; + + + /* + * Return true if the value of this BigNumber is an integer, otherwise return false. + */ + P.isInteger = function () { + return !!this.c && bitFloor(this.e / LOG_BASE) > this.c.length - 2; + }; + + + /* + * Return true if the value of this BigNumber is less than the value of BigNumber(y, b), + * otherwise return false. + */ + P.isLessThan = P.lt = function (y, b) { + return compare(this, new BigNumber(y, b)) < 0; + }; + + + /* + * Return true if the value of this BigNumber is less than or equal to the value of + * BigNumber(y, b), otherwise return false. + */ + P.isLessThanOrEqualTo = P.lte = function (y, b) { + return (b = compare(this, new BigNumber(y, b))) === -1 || b === 0; + }; + + + /* + * Return true if the value of this BigNumber is NaN, otherwise return false. + */ + P.isNaN = function () { + return !this.s; + }; + + + /* + * Return true if the value of this BigNumber is negative, otherwise return false. + */ + P.isNegative = function () { + return this.s < 0; + }; + + + /* + * Return true if the value of this BigNumber is positive, otherwise return false. + */ + P.isPositive = function () { + return this.s > 0; + }; + + + /* + * Return true if the value of this BigNumber is 0 or -0, otherwise return false. + */ + P.isZero = function () { + return !!this.c && this.c[0] == 0; + }; + + + /* + * n - 0 = n + * n - N = N + * n - I = -I + * 0 - n = -n + * 0 - 0 = 0 + * 0 - N = N + * 0 - I = -I + * N - n = N + * N - 0 = N + * N - N = N + * N - I = N + * I - n = I + * I - 0 = I + * I - N = N + * I - I = N + * + * Return a new BigNumber whose value is the value of this BigNumber minus the value of + * BigNumber(y, b). + */ + P.minus = function (y, b) { + var i, j, t, xLTy, + x = this, + a = x.s; + + y = new BigNumber(y, b); + b = y.s; + + // Either NaN? + if (!a || !b) return new BigNumber(NaN); + + // Signs differ? + if (a != b) { + y.s = -b; + return x.plus(y); + } + + var xe = x.e / LOG_BASE, + ye = y.e / LOG_BASE, + xc = x.c, + yc = y.c; + + if (!xe || !ye) { + + // Either Infinity? + if (!xc || !yc) return xc ? (y.s = -b, y) : new BigNumber(yc ? x : NaN); + + // Either zero? + if (!xc[0] || !yc[0]) { + + // Return y if y is non-zero, x if x is non-zero, or zero if both are zero. + return yc[0] ? (y.s = -b, y) : new BigNumber(xc[0] ? x : + + // IEEE 754 (2008) 6.3: n - n = -0 when rounding to -Infinity + ROUNDING_MODE == 3 ? -0 : 0); + } + } + + xe = bitFloor(xe); + ye = bitFloor(ye); + xc = xc.slice(); + + // Determine which is the bigger number. + if (a = xe - ye) { + + if (xLTy = a < 0) { + a = -a; + t = xc; + } else { + ye = xe; + t = yc; + } + + t.reverse(); + + // Prepend zeros to equalise exponents. + for (b = a; b--; t.push(0)); + t.reverse(); + } else { + + // Exponents equal. Check digit by digit. + j = (xLTy = (a = xc.length) < (b = yc.length)) ? a : b; + + for (a = b = 0; b < j; b++) { + + if (xc[b] != yc[b]) { + xLTy = xc[b] < yc[b]; + break; + } + } + } + + // x < y? Point xc to the array of the bigger number. + if (xLTy) { + t = xc; + xc = yc; + yc = t; + y.s = -y.s; + } + + b = (j = yc.length) - (i = xc.length); + + // Append zeros to xc if shorter. + // No need to add zeros to yc if shorter as subtract only needs to start at yc.length. + if (b > 0) for (; b--; xc[i++] = 0); + b = BASE - 1; + + // Subtract yc from xc. + for (; j > a;) { + + if (xc[--j] < yc[j]) { + for (i = j; i && !xc[--i]; xc[i] = b); + --xc[i]; + xc[j] += BASE; + } + + xc[j] -= yc[j]; + } + + // Remove leading zeros and adjust exponent accordingly. + for (; xc[0] == 0; xc.splice(0, 1), --ye); + + // Zero? + if (!xc[0]) { + + // Following IEEE 754 (2008) 6.3, + // n - n = +0 but n - n = -0 when rounding towards -Infinity. + y.s = ROUNDING_MODE == 3 ? -1 : 1; + y.c = [y.e = 0]; + return y; + } + + // No need to check for Infinity as +x - +y != Infinity && -x - -y != Infinity + // for finite x and y. + return normalise(y, xc, ye); + }; + + + /* + * n % 0 = N + * n % N = N + * n % I = n + * 0 % n = 0 + * -0 % n = -0 + * 0 % 0 = N + * 0 % N = N + * 0 % I = 0 + * N % n = N + * N % 0 = N + * N % N = N + * N % I = N + * I % n = N + * I % 0 = N + * I % N = N + * I % I = N + * + * Return a new BigNumber whose value is the value of this BigNumber modulo the value of + * BigNumber(y, b). The result depends on the value of MODULO_MODE. + */ + P.modulo = P.mod = function (y, b) { + var q, s, + x = this; + + y = new BigNumber(y, b); + + // Return NaN if x is Infinity or NaN, or y is NaN or zero. + if (!x.c || !y.s || y.c && !y.c[0]) { + return new BigNumber(NaN); + + // Return x if y is Infinity or x is zero. + } else if (!y.c || x.c && !x.c[0]) { + return new BigNumber(x); + } + + if (MODULO_MODE == 9) { + + // Euclidian division: q = sign(y) * floor(x / abs(y)) + // r = x - qy where 0 <= r < abs(y) + s = y.s; + y.s = 1; + q = div(x, y, 0, 3); + y.s = s; + q.s *= s; + } else { + q = div(x, y, 0, MODULO_MODE); + } + + y = x.minus(q.times(y)); + + // To match JavaScript %, ensure sign of zero is sign of dividend. + if (!y.c[0] && MODULO_MODE == 1) y.s = x.s; + + return y; + }; + + + /* + * n * 0 = 0 + * n * N = N + * n * I = I + * 0 * n = 0 + * 0 * 0 = 0 + * 0 * N = N + * 0 * I = N + * N * n = N + * N * 0 = N + * N * N = N + * N * I = N + * I * n = I + * I * 0 = N + * I * N = N + * I * I = I + * + * Return a new BigNumber whose value is the value of this BigNumber multiplied by the value + * of BigNumber(y, b). + */ + P.multipliedBy = P.times = function (y, b) { + var c, e, i, j, k, m, xcL, xlo, xhi, ycL, ylo, yhi, zc, + base, sqrtBase, + x = this, + xc = x.c, + yc = (y = new BigNumber(y, b)).c; + + // Either NaN, ±Infinity or ±0? + if (!xc || !yc || !xc[0] || !yc[0]) { + + // Return NaN if either is NaN, or one is 0 and the other is Infinity. + if (!x.s || !y.s || xc && !xc[0] && !yc || yc && !yc[0] && !xc) { + y.c = y.e = y.s = null; + } else { + y.s *= x.s; + + // Return ±Infinity if either is ±Infinity. + if (!xc || !yc) { + y.c = y.e = null; + + // Return ±0 if either is ±0. + } else { + y.c = [0]; + y.e = 0; + } + } + + return y; + } + + e = bitFloor(x.e / LOG_BASE) + bitFloor(y.e / LOG_BASE); + y.s *= x.s; + xcL = xc.length; + ycL = yc.length; + + // Ensure xc points to longer array and xcL to its length. + if (xcL < ycL) { + zc = xc; + xc = yc; + yc = zc; + i = xcL; + xcL = ycL; + ycL = i; + } + + // Initialise the result array with zeros. + for (i = xcL + ycL, zc = []; i--; zc.push(0)); + + base = BASE; + sqrtBase = SQRT_BASE; + + for (i = ycL; --i >= 0;) { + c = 0; + ylo = yc[i] % sqrtBase; + yhi = yc[i] / sqrtBase | 0; + + for (k = xcL, j = i + k; j > i;) { + xlo = xc[--k] % sqrtBase; + xhi = xc[k] / sqrtBase | 0; + m = yhi * xlo + xhi * ylo; + xlo = ylo * xlo + ((m % sqrtBase) * sqrtBase) + zc[j] + c; + c = (xlo / base | 0) + (m / sqrtBase | 0) + yhi * xhi; + zc[j--] = xlo % base; + } + + zc[j] = c; + } + + if (c) { + ++e; + } else { + zc.splice(0, 1); + } + + return normalise(y, zc, e); + }; + + + /* + * Return a new BigNumber whose value is the value of this BigNumber negated, + * i.e. multiplied by -1. + */ + P.negated = function () { + var x = new BigNumber(this); + x.s = -x.s || null; + return x; + }; + + + /* + * n + 0 = n + * n + N = N + * n + I = I + * 0 + n = n + * 0 + 0 = 0 + * 0 + N = N + * 0 + I = I + * N + n = N + * N + 0 = N + * N + N = N + * N + I = N + * I + n = I + * I + 0 = I + * I + N = N + * I + I = I + * + * Return a new BigNumber whose value is the value of this BigNumber plus the value of + * BigNumber(y, b). + */ + P.plus = function (y, b) { + var t, + x = this, + a = x.s; + + y = new BigNumber(y, b); + b = y.s; + + // Either NaN? + if (!a || !b) return new BigNumber(NaN); + + // Signs differ? + if (a != b) { + y.s = -b; + return x.minus(y); + } + + var xe = x.e / LOG_BASE, + ye = y.e / LOG_BASE, + xc = x.c, + yc = y.c; + + if (!xe || !ye) { + + // Return ±Infinity if either ±Infinity. + if (!xc || !yc) return new BigNumber(a / 0); + + // Either zero? + // Return y if y is non-zero, x if x is non-zero, or zero if both are zero. + if (!xc[0] || !yc[0]) return yc[0] ? y : new BigNumber(xc[0] ? x : a * 0); + } + + xe = bitFloor(xe); + ye = bitFloor(ye); + xc = xc.slice(); + + // Prepend zeros to equalise exponents. Faster to use reverse then do unshifts. + if (a = xe - ye) { + if (a > 0) { + ye = xe; + t = yc; + } else { + a = -a; + t = xc; + } + + t.reverse(); + for (; a--; t.push(0)); + t.reverse(); + } + + a = xc.length; + b = yc.length; + + // Point xc to the longer array, and b to the shorter length. + if (a - b < 0) { + t = yc; + yc = xc; + xc = t; + b = a; + } + + // Only start adding at yc.length - 1 as the further digits of xc can be ignored. + for (a = 0; b;) { + a = (xc[--b] = xc[b] + yc[b] + a) / BASE | 0; + xc[b] = BASE === xc[b] ? 0 : xc[b] % BASE; + } + + if (a) { + xc = [a].concat(xc); + ++ye; + } + + // No need to check for zero, as +x + +y != 0 && -x + -y != 0 + // ye = MAX_EXP + 1 possible + return normalise(y, xc, ye); + }; + + + /* + * If sd is undefined or null or true or false, return the number of significant digits of + * the value of this BigNumber, or null if the value of this BigNumber is ±Infinity or NaN. + * If sd is true include integer-part trailing zeros in the count. + * + * Otherwise, if sd is a number, return a new BigNumber whose value is the value of this + * BigNumber rounded to a maximum of sd significant digits using rounding mode rm, or + * ROUNDING_MODE if rm is omitted. + * + * sd {number|boolean} number: significant digits: integer, 1 to MAX inclusive. + * boolean: whether to count integer-part trailing zeros: true or false. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}' + */ + P.precision = P.sd = function (sd, rm) { + var c, n, v, + x = this; + + if (sd != null && sd !== !!sd) { + intCheck(sd, 1, MAX); + if (rm == null) rm = ROUNDING_MODE; + else intCheck(rm, 0, 8); + + return round(new BigNumber(x), sd, rm); + } + + if (!(c = x.c)) return null; + v = c.length - 1; + n = v * LOG_BASE + 1; + + if (v = c[v]) { + + // Subtract the number of trailing zeros of the last element. + for (; v % 10 == 0; v /= 10, n--); + + // Add the number of digits of the first element. + for (v = c[0]; v >= 10; v /= 10, n++); + } + + if (sd && x.e + 1 > n) n = x.e + 1; + + return n; + }; + + + /* + * Return a new BigNumber whose value is the value of this BigNumber shifted by k places + * (powers of 10). Shift to the right if n > 0, and to the left if n < 0. + * + * k {number} Integer, -MAX_SAFE_INTEGER to MAX_SAFE_INTEGER inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {k}' + */ + P.shiftedBy = function (k) { + intCheck(k, -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER); + return this.times('1e' + k); + }; + + + /* + * sqrt(-n) = N + * sqrt(N) = N + * sqrt(-I) = N + * sqrt(I) = I + * sqrt(0) = 0 + * sqrt(-0) = -0 + * + * Return a new BigNumber whose value is the square root of the value of this BigNumber, + * rounded according to DECIMAL_PLACES and ROUNDING_MODE. + */ + P.squareRoot = P.sqrt = function () { + var m, n, r, rep, t, + x = this, + c = x.c, + s = x.s, + e = x.e, + dp = DECIMAL_PLACES + 4, + half = new BigNumber('0.5'); + + // Negative/NaN/Infinity/zero? + if (s !== 1 || !c || !c[0]) { + return new BigNumber(!s || s < 0 && (!c || c[0]) ? NaN : c ? x : 1 / 0); + } + + // Initial estimate. + s = Math.sqrt(+valueOf(x)); + + // Math.sqrt underflow/overflow? + // Pass x to Math.sqrt as integer, then adjust the exponent of the result. + if (s == 0 || s == 1 / 0) { + n = coeffToString(c); + if ((n.length + e) % 2 == 0) n += '0'; + s = Math.sqrt(+n); + e = bitFloor((e + 1) / 2) - (e < 0 || e % 2); + + if (s == 1 / 0) { + n = '5e' + e; + } else { + n = s.toExponential(); + n = n.slice(0, n.indexOf('e') + 1) + e; + } + + r = new BigNumber(n); + } else { + r = new BigNumber(s + ''); + } + + // Check for zero. + // r could be zero if MIN_EXP is changed after the this value was created. + // This would cause a division by zero (x/t) and hence Infinity below, which would cause + // coeffToString to throw. + if (r.c[0]) { + e = r.e; + s = e + dp; + if (s < 3) s = 0; + + // Newton-Raphson iteration. + for (; ;) { + t = r; + r = half.times(t.plus(div(x, t, dp, 1))); + + if (coeffToString(t.c).slice(0, s) === (n = coeffToString(r.c)).slice(0, s)) { + + // The exponent of r may here be one less than the final result exponent, + // e.g 0.0009999 (e-4) --> 0.001 (e-3), so adjust s so the rounding digits + // are indexed correctly. + if (r.e < e) --s; + n = n.slice(s - 3, s + 1); + + // The 4th rounding digit may be in error by -1 so if the 4 rounding digits + // are 9999 or 4999 (i.e. approaching a rounding boundary) continue the + // iteration. + if (n == '9999' || !rep && n == '4999') { + + // On the first iteration only, check to see if rounding up gives the + // exact result as the nines may infinitely repeat. + if (!rep) { + round(t, t.e + DECIMAL_PLACES + 2, 0); + + if (t.times(t).eq(x)) { + r = t; + break; + } + } + + dp += 4; + s += 4; + rep = 1; + } else { + + // If rounding digits are null, 0{0,4} or 50{0,3}, check for exact + // result. If not, then there are further digits and m will be truthy. + if (!+n || !+n.slice(1) && n.charAt(0) == '5') { + + // Truncate to the first rounding digit. + round(r, r.e + DECIMAL_PLACES + 2, 1); + m = !r.times(r).eq(x); + } + + break; + } + } + } + } + + return round(r, r.e + DECIMAL_PLACES + 1, ROUNDING_MODE, m); + }; + + + /* + * Return a string representing the value of this BigNumber in exponential notation and + * rounded using ROUNDING_MODE to dp fixed decimal places. + * + * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' + */ + P.toExponential = function (dp, rm) { + if (dp != null) { + intCheck(dp, 0, MAX); + dp++; + } + return format(this, dp, rm, 1); + }; + + + /* + * Return a string representing the value of this BigNumber in fixed-point notation rounding + * to dp fixed decimal places using rounding mode rm, or ROUNDING_MODE if rm is omitted. + * + * Note: as with JavaScript's number type, (-0).toFixed(0) is '0', + * but e.g. (-0.00001).toFixed(0) is '-0'. + * + * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' + */ + P.toFixed = function (dp, rm) { + if (dp != null) { + intCheck(dp, 0, MAX); + dp = dp + this.e + 1; + } + return format(this, dp, rm); + }; + + + /* + * Return a string representing the value of this BigNumber in fixed-point notation rounded + * using rm or ROUNDING_MODE to dp decimal places, and formatted according to the properties + * of the format or FORMAT object (see BigNumber.set). + * + * The formatting object may contain some or all of the properties shown below. + * + * FORMAT = { + * prefix: '', + * groupSize: 3, + * secondaryGroupSize: 0, + * groupSeparator: ',', + * decimalSeparator: '.', + * fractionGroupSize: 0, + * fractionGroupSeparator: '\xA0', // non-breaking space + * suffix: '' + * }; + * + * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * [format] {object} Formatting options. See FORMAT pbject above. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' + * '[BigNumber Error] Argument not an object: {format}' + */ + P.toFormat = function (dp, rm, format) { + var str, + x = this; + + if (format == null) { + if (dp != null && rm && typeof rm == 'object') { + format = rm; + rm = null; + } else if (dp && typeof dp == 'object') { + format = dp; + dp = rm = null; + } else { + format = FORMAT; + } + } else if (typeof format != 'object') { + throw Error + (bignumberError + 'Argument not an object: ' + format); + } + + str = x.toFixed(dp, rm); + + if (x.c) { + var i, + arr = str.split('.'), + g1 = +format.groupSize, + g2 = +format.secondaryGroupSize, + groupSeparator = format.groupSeparator || '', + intPart = arr[0], + fractionPart = arr[1], + isNeg = x.s < 0, + intDigits = isNeg ? intPart.slice(1) : intPart, + len = intDigits.length; + + if (g2) { + i = g1; + g1 = g2; + g2 = i; + len -= i; + } + + if (g1 > 0 && len > 0) { + i = len % g1 || g1; + intPart = intDigits.substr(0, i); + for (; i < len; i += g1) intPart += groupSeparator + intDigits.substr(i, g1); + if (g2 > 0) intPart += groupSeparator + intDigits.slice(i); + if (isNeg) intPart = '-' + intPart; + } + + str = fractionPart + ? intPart + (format.decimalSeparator || '') + ((g2 = +format.fractionGroupSize) + ? fractionPart.replace(new RegExp('\\d{' + g2 + '}\\B', 'g'), + '$&' + (format.fractionGroupSeparator || '')) + : fractionPart) + : intPart; + } + + return (format.prefix || '') + str + (format.suffix || ''); + }; + + + /* + * Return an array of two BigNumbers representing the value of this BigNumber as a simple + * fraction with an integer numerator and an integer denominator. + * The denominator will be a positive non-zero value less than or equal to the specified + * maximum denominator. If a maximum denominator is not specified, the denominator will be + * the lowest value necessary to represent the number exactly. + * + * [md] {number|string|BigNumber} Integer >= 1, or Infinity. The maximum denominator. + * + * '[BigNumber Error] Argument {not an integer|out of range} : {md}' + */ + P.toFraction = function (md) { + var d, d0, d1, d2, e, exp, n, n0, n1, q, r, s, + x = this, + xc = x.c; + + if (md != null) { + n = new BigNumber(md); + + // Throw if md is less than one or is not an integer, unless it is Infinity. + if (!n.isInteger() && (n.c || n.s !== 1) || n.lt(ONE)) { + throw Error + (bignumberError + 'Argument ' + + (n.isInteger() ? 'out of range: ' : 'not an integer: ') + valueOf(n)); + } + } + + if (!xc) return new BigNumber(x); + + d = new BigNumber(ONE); + n1 = d0 = new BigNumber(ONE); + d1 = n0 = new BigNumber(ONE); + s = coeffToString(xc); + + // Determine initial denominator. + // d is a power of 10 and the minimum max denominator that specifies the value exactly. + e = d.e = s.length - x.e - 1; + d.c[0] = POWS_TEN[(exp = e % LOG_BASE) < 0 ? LOG_BASE + exp : exp]; + md = !md || n.comparedTo(d) > 0 ? (e > 0 ? d : n1) : n; + + exp = MAX_EXP; + MAX_EXP = 1 / 0; + n = new BigNumber(s); + + // n0 = d1 = 0 + n0.c[0] = 0; + + for (; ;) { + q = div(n, d, 0, 1); + d2 = d0.plus(q.times(d1)); + if (d2.comparedTo(md) == 1) break; + d0 = d1; + d1 = d2; + n1 = n0.plus(q.times(d2 = n1)); + n0 = d2; + d = n.minus(q.times(d2 = d)); + n = d2; + } + + d2 = div(md.minus(d0), d1, 0, 1); + n0 = n0.plus(d2.times(n1)); + d0 = d0.plus(d2.times(d1)); + n0.s = n1.s = x.s; + e = e * 2; + + // Determine which fraction is closer to x, n0/d0 or n1/d1 + r = div(n1, d1, e, ROUNDING_MODE).minus(x).abs().comparedTo( + div(n0, d0, e, ROUNDING_MODE).minus(x).abs()) < 1 ? [n1, d1] : [n0, d0]; + + MAX_EXP = exp; + + return r; + }; + + + /* + * Return the value of this BigNumber converted to a number primitive. + */ + P.toNumber = function () { + return +valueOf(this); + }; + + + /* + * Return a string representing the value of this BigNumber rounded to sd significant digits + * using rounding mode rm or ROUNDING_MODE. If sd is less than the number of digits + * necessary to represent the integer part of the value in fixed-point notation, then use + * exponential notation. + * + * [sd] {number} Significant digits. Integer, 1 to MAX inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}' + */ + P.toPrecision = function (sd, rm) { + if (sd != null) intCheck(sd, 1, MAX); + return format(this, sd, rm, 2); + }; + + + /* + * Return a string representing the value of this BigNumber in base b, or base 10 if b is + * omitted. If a base is specified, including base 10, round according to DECIMAL_PLACES and + * ROUNDING_MODE. If a base is not specified, and this BigNumber has a positive exponent + * that is equal to or greater than TO_EXP_POS, or a negative exponent equal to or less than + * TO_EXP_NEG, return exponential notation. + * + * [b] {number} Integer, 2 to ALPHABET.length inclusive. + * + * '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}' + */ + P.toString = function (b) { + var str, + n = this, + s = n.s, + e = n.e; + + // Infinity or NaN? + if (e === null) { + if (s) { + str = 'Infinity'; + if (s < 0) str = '-' + str; + } else { + str = 'NaN'; + } + } else { + if (b == null) { + str = e <= TO_EXP_NEG || e >= TO_EXP_POS + ? toExponential(coeffToString(n.c), e) + : toFixedPoint(coeffToString(n.c), e, '0'); + } else if (b === 10 && alphabetHasNormalDecimalDigits) { + n = round(new BigNumber(n), DECIMAL_PLACES + e + 1, ROUNDING_MODE); + str = toFixedPoint(coeffToString(n.c), n.e, '0'); + } else { + intCheck(b, 2, ALPHABET.length, 'Base'); + str = convertBase(toFixedPoint(coeffToString(n.c), e, '0'), 10, b, s, true); + } + + if (s < 0 && n.c[0]) str = '-' + str; + } + + return str; + }; + + + /* + * Return as toString, but do not accept a base argument, and include the minus sign for + * negative zero. + */ + P.valueOf = P.toJSON = function () { + return valueOf(this); + }; + + + P._isBigNumber = true; + + if (configObject != null) BigNumber.set(configObject); + + return BigNumber; + } + + + // PRIVATE HELPER FUNCTIONS + + // These functions don't need access to variables, + // e.g. DECIMAL_PLACES, in the scope of the `clone` function above. + + + function bitFloor(n) { + var i = n | 0; + return n > 0 || n === i ? i : i - 1; + } + + + // Return a coefficient array as a string of base 10 digits. + function coeffToString(a) { + var s, z, + i = 1, + j = a.length, + r = a[0] + ''; + + for (; i < j;) { + s = a[i++] + ''; + z = LOG_BASE - s.length; + for (; z--; s = '0' + s); + r += s; + } + + // Determine trailing zeros. + for (j = r.length; r.charCodeAt(--j) === 48;); + + return r.slice(0, j + 1 || 1); + } + + + // Compare the value of BigNumbers x and y. + function compare(x, y) { + var a, b, + xc = x.c, + yc = y.c, + i = x.s, + j = y.s, + k = x.e, + l = y.e; + + // Either NaN? + if (!i || !j) return null; + + a = xc && !xc[0]; + b = yc && !yc[0]; + + // Either zero? + if (a || b) return a ? b ? 0 : -j : i; + + // Signs differ? + if (i != j) return i; + + a = i < 0; + b = k == l; + + // Either Infinity? + if (!xc || !yc) return b ? 0 : !xc ^ a ? 1 : -1; + + // Compare exponents. + if (!b) return k > l ^ a ? 1 : -1; + + j = (k = xc.length) < (l = yc.length) ? k : l; + + // Compare digit by digit. + for (i = 0; i < j; i++) if (xc[i] != yc[i]) return xc[i] > yc[i] ^ a ? 1 : -1; + + // Compare lengths. + return k == l ? 0 : k > l ^ a ? 1 : -1; + } + + + /* + * Check that n is a primitive number, an integer, and in range, otherwise throw. + */ + function intCheck(n, min, max, name) { + if (n < min || n > max || n !== mathfloor(n)) { + throw Error + (bignumberError + (name || 'Argument') + (typeof n == 'number' + ? n < min || n > max ? ' out of range: ' : ' not an integer: ' + : ' not a primitive number: ') + String(n)); + } + } + + + // Assumes finite n. + function isOdd(n) { + var k = n.c.length - 1; + return bitFloor(n.e / LOG_BASE) == k && n.c[k] % 2 != 0; + } + + + function toExponential(str, e) { + return (str.length > 1 ? str.charAt(0) + '.' + str.slice(1) : str) + + (e < 0 ? 'e' : 'e+') + e; + } + + + function toFixedPoint(str, e, z) { + var len, zs; + + // Negative exponent? + if (e < 0) { + + // Prepend zeros. + for (zs = z + '.'; ++e; zs += z); + str = zs + str; + + // Positive exponent + } else { + len = str.length; + + // Append zeros. + if (++e > len) { + for (zs = z, e -= len; --e; zs += z); + str += zs; + } else if (e < len) { + str = str.slice(0, e) + '.' + str.slice(e); + } + } + + return str; + } + + + // EXPORT + + + BigNumber = clone(); + BigNumber['default'] = BigNumber.BigNumber = BigNumber; + + // AMD. + if (typeof define == 'function' && define.amd) { + define(function () { return BigNumber; }); + + // Node.js and other environments that support module.exports. + } else if ( true && module.exports) { + module.exports = BigNumber; + + // Browser. + } else { + if (!globalObject) { + globalObject = typeof self != 'undefined' && self ? self : window; + } + + globalObject.BigNumber = BigNumber; + } +})(this); /***/ }), @@ -11868,7 +11868,7 @@ _a = Gaxios, _Gaxios_instances = new WeakSet(), _Gaxios_urlMayUseProxy = functio } } return true; -}, _Gaxios_applyRequestInterceptors = +}, _Gaxios_applyRequestInterceptors = /** * Applies the request interceptors. The request interceptors are applied after the * call to prepareRequest is completed. @@ -11885,7 +11885,7 @@ async function _Gaxios_applyRequestInterceptors(options) { } } return promiseChain; -}, _Gaxios_applyResponseInterceptors = +}, _Gaxios_applyResponseInterceptors = /** * Applies the response interceptors. The response interceptors are applied after the * call to request is made. @@ -11902,7 +11902,7 @@ async function _Gaxios_applyResponseInterceptors(response) { } } return promiseChain; -}, _Gaxios_prepareRequest = +}, _Gaxios_prepareRequest = /** * Validates the options, merges them with defaults, and prepare request. * @@ -15778,7 +15778,7 @@ async function isAvailable() { // runtime environment. We use the same promise for each of these calls // to reduce the network load. if (cachedIsAvailableResponse === undefined) { - cachedIsAvailableResponse = metadataAccessor('instance', undefined, detectGCPAvailableRetries(), + cachedIsAvailableResponse = metadataAccessor('instance', undefined, detectGCPAvailableRetries(), // If the default HOST_ADDRESS has been overridden, we should not // make an effort to try SECONDARY_HOST_ADDRESS (as we are likely in // a non-GCP environment): @@ -16795,7 +16795,7 @@ async function generateAuthenticationHeaderMap(options) { // Header keys need to be sorted alphabetically. const amzHeaders = Object.assign({ host: options.host, - }, + }, // Previously the date was not fixed with x-amz- and could be provided manually. // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req reformattedAdditionalAmzHeaders.date ? {} : { 'x-amz-date': amzDate }, reformattedAdditionalAmzHeaders); @@ -17568,7 +17568,7 @@ class DefaultAwsSecurityCredentialsSupplier { } } exports.DefaultAwsSecurityCredentialsSupplier = DefaultAwsSecurityCredentialsSupplier; -_DefaultAwsSecurityCredentialsSupplier_instances = new WeakSet(), _DefaultAwsSecurityCredentialsSupplier_getImdsV2SessionToken = +_DefaultAwsSecurityCredentialsSupplier_instances = new WeakSet(), _DefaultAwsSecurityCredentialsSupplier_getImdsV2SessionToken = /** * @param transporter The transporter to use for requests. * @return A promise that resolves with the IMDSv2 Session Token. @@ -17583,7 +17583,7 @@ async function _DefaultAwsSecurityCredentialsSupplier_getImdsV2SessionToken(tran }; const response = await transporter.request(opts); return response.data; -}, _DefaultAwsSecurityCredentialsSupplier_getAwsRoleName = +}, _DefaultAwsSecurityCredentialsSupplier_getAwsRoleName = /** * @param headers The headers to be used in the metadata request. * @param transporter The transporter to use for requests. @@ -17604,7 +17604,7 @@ async function _DefaultAwsSecurityCredentialsSupplier_getAwsRoleName(headers, tr }; const response = await transporter.request(opts); return response.data; -}, _DefaultAwsSecurityCredentialsSupplier_retrieveAwsSecurityCredentials = +}, _DefaultAwsSecurityCredentialsSupplier_retrieveAwsSecurityCredentials = /** * Retrieves the temporary AWS credentials by calling the security-credentials * endpoint as specified in the `credential_source` object. @@ -18248,7 +18248,7 @@ class ExternalAccountAuthorizedUserHandler extends oauth2common_1.OAuthClientAut catch (error) { // Translate error to OAuthError. if (error instanceof gaxios_1.GaxiosError && error.response) { - throw (0, oauth2common_1.getErrorFromOAuthErrorResponse)(error.response.data, + throw (0, oauth2common_1.getErrorFromOAuthErrorResponse)(error.response.data, // Preserve other fields from the original error. error); } @@ -20117,7 +20117,7 @@ class JWT extends oauth2client_1.OAuth2Client { } const useScopes = this.useJWTAccessWithScope || this.universeDomain !== authclient_1.DEFAULT_UNIVERSE; - const headers = await this.access.getRequestHeaders(url !== null && url !== void 0 ? url : undefined, this.additionalClaims, + const headers = await this.access.getRequestHeaders(url !== null && url !== void 0 ? url : undefined, this.additionalClaims, // Scopes take precedent over audience for signing, // so we only provide them if `useJWTAccessWithScope` is on or // if we are in a non-default universe @@ -22022,7 +22022,7 @@ class StsCredentials extends oauth2common_1.OAuthClientAuthHandler { * @return A promise that resolves with the token exchange response containing * the requested token and its expiration time. */ - async exchangeToken(stsCredentialsOptions, additionalHeaders, + async exchangeToken(stsCredentialsOptions, additionalHeaders, // eslint-disable-next-line @typescript-eslint/no-explicit-any options) { var _a, _b, _c; @@ -22072,7 +22072,7 @@ class StsCredentials extends oauth2common_1.OAuthClientAuthHandler { catch (error) { // Translate error to OAuthError. if (error instanceof gaxios_1.GaxiosError && error.response) { - throw (0, oauth2common_1.getErrorFromOAuthErrorResponse)(error.response.data, + throw (0, oauth2common_1.getErrorFromOAuthErrorResponse)(error.response.data, // Preserve other fields from the original error. error); } @@ -22824,7 +22824,7 @@ _LRUCache_cache = new WeakMap(), _LRUCache_instances = new WeakSet(), _LRUCache_ // limitations under the License. Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getAPI = void 0; -function getAPI(api, options, +function getAPI(api, options, // eslint-disable-next-line @typescript-eslint/no-explicit-any versions, context) { let version; @@ -160439,7 +160439,7 @@ var businessprofileperformance_v1; callback = optionsOrCallback; options = {}; } - const rootUrl = options.rootUrl || 'https://businessprofileperformance.googleapis.com/'; + const rootUrl = options.rootUrl || 'https://businessprofilemargoogleapis.com/'; const parameters = { options: Object.assign({ url: (rootUrl + '/v1/{+location}:fetchMultiDailyMetricsTimeSeries').replace(/([^:]\/)\/+/g, '$1'), @@ -727886,7 +727886,7 @@ _GoogleToken_inFlightRequest = new WeakMap(), _GoogleToken_instances = new WeakS if (options.transporter) { this.transporter = options.transporter; } -}, _GoogleToken_requestToken = +}, _GoogleToken_requestToken = /** * Request the token from Google. */ @@ -735336,1303 +735336,1303 @@ exports.parseURL = __nccwpck_require__(2158).parseURL; /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; - -const punycode = __nccwpck_require__(85477); -const tr46 = __nccwpck_require__(84256); - -const specialSchemes = { - ftp: 21, - file: null, - gopher: 70, - http: 80, - https: 443, - ws: 80, - wss: 443 -}; - -const failure = Symbol("failure"); - -function countSymbols(str) { - return punycode.ucs2.decode(str).length; -} - -function at(input, idx) { - const c = input[idx]; - return isNaN(c) ? undefined : String.fromCodePoint(c); -} - -function isASCIIDigit(c) { - return c >= 0x30 && c <= 0x39; -} - -function isASCIIAlpha(c) { - return (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A); -} - -function isASCIIAlphanumeric(c) { - return isASCIIAlpha(c) || isASCIIDigit(c); -} - -function isASCIIHex(c) { - return isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66); -} - -function isSingleDot(buffer) { - return buffer === "." || buffer.toLowerCase() === "%2e"; -} - -function isDoubleDot(buffer) { - buffer = buffer.toLowerCase(); - return buffer === ".." || buffer === "%2e." || buffer === ".%2e" || buffer === "%2e%2e"; -} - -function isWindowsDriveLetterCodePoints(cp1, cp2) { - return isASCIIAlpha(cp1) && (cp2 === 58 || cp2 === 124); -} - -function isWindowsDriveLetterString(string) { - return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && (string[1] === ":" || string[1] === "|"); -} - -function isNormalizedWindowsDriveLetterString(string) { - return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && string[1] === ":"; -} - -function containsForbiddenHostCodePoint(string) { - return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|%|\/|:|\?|@|\[|\\|\]/) !== -1; -} - -function containsForbiddenHostCodePointExcludingPercent(string) { - return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|\/|:|\?|@|\[|\\|\]/) !== -1; -} - -function isSpecialScheme(scheme) { - return specialSchemes[scheme] !== undefined; -} - -function isSpecial(url) { - return isSpecialScheme(url.scheme); -} - -function defaultPort(scheme) { - return specialSchemes[scheme]; -} - -function percentEncode(c) { - let hex = c.toString(16).toUpperCase(); - if (hex.length === 1) { - hex = "0" + hex; - } - - return "%" + hex; -} - -function utf8PercentEncode(c) { - const buf = new Buffer(c); - - let str = ""; - - for (let i = 0; i < buf.length; ++i) { - str += percentEncode(buf[i]); - } - - return str; -} - -function utf8PercentDecode(str) { - const input = new Buffer(str); - const output = []; - for (let i = 0; i < input.length; ++i) { - if (input[i] !== 37) { - output.push(input[i]); - } else if (input[i] === 37 && isASCIIHex(input[i + 1]) && isASCIIHex(input[i + 2])) { - output.push(parseInt(input.slice(i + 1, i + 3).toString(), 16)); - i += 2; - } else { - output.push(input[i]); - } - } - return new Buffer(output).toString(); -} - -function isC0ControlPercentEncode(c) { - return c <= 0x1F || c > 0x7E; -} - -const extraPathPercentEncodeSet = new Set([32, 34, 35, 60, 62, 63, 96, 123, 125]); -function isPathPercentEncode(c) { - return isC0ControlPercentEncode(c) || extraPathPercentEncodeSet.has(c); -} - -const extraUserinfoPercentEncodeSet = - new Set([47, 58, 59, 61, 64, 91, 92, 93, 94, 124]); -function isUserinfoPercentEncode(c) { - return isPathPercentEncode(c) || extraUserinfoPercentEncodeSet.has(c); -} - -function percentEncodeChar(c, encodeSetPredicate) { - const cStr = String.fromCodePoint(c); - - if (encodeSetPredicate(c)) { - return utf8PercentEncode(cStr); - } - - return cStr; -} - -function parseIPv4Number(input) { - let R = 10; - - if (input.length >= 2 && input.charAt(0) === "0" && input.charAt(1).toLowerCase() === "x") { - input = input.substring(2); - R = 16; - } else if (input.length >= 2 && input.charAt(0) === "0") { - input = input.substring(1); - R = 8; - } - - if (input === "") { - return 0; - } - - const regex = R === 10 ? /[^0-9]/ : (R === 16 ? /[^0-9A-Fa-f]/ : /[^0-7]/); - if (regex.test(input)) { - return failure; - } - - return parseInt(input, R); -} - -function parseIPv4(input) { - const parts = input.split("."); - if (parts[parts.length - 1] === "") { - if (parts.length > 1) { - parts.pop(); - } - } - - if (parts.length > 4) { - return input; - } - - const numbers = []; - for (const part of parts) { - if (part === "") { - return input; - } - const n = parseIPv4Number(part); - if (n === failure) { - return input; - } - - numbers.push(n); - } - - for (let i = 0; i < numbers.length - 1; ++i) { - if (numbers[i] > 255) { - return failure; - } - } - if (numbers[numbers.length - 1] >= Math.pow(256, 5 - numbers.length)) { - return failure; - } - - let ipv4 = numbers.pop(); - let counter = 0; - - for (const n of numbers) { - ipv4 += n * Math.pow(256, 3 - counter); - ++counter; - } - - return ipv4; -} - -function serializeIPv4(address) { - let output = ""; - let n = address; - - for (let i = 1; i <= 4; ++i) { - output = String(n % 256) + output; - if (i !== 4) { - output = "." + output; - } - n = Math.floor(n / 256); - } - - return output; -} - -function parseIPv6(input) { - const address = [0, 0, 0, 0, 0, 0, 0, 0]; - let pieceIndex = 0; - let compress = null; - let pointer = 0; - - input = punycode.ucs2.decode(input); - - if (input[pointer] === 58) { - if (input[pointer + 1] !== 58) { - return failure; - } - - pointer += 2; - ++pieceIndex; - compress = pieceIndex; - } - - while (pointer < input.length) { - if (pieceIndex === 8) { - return failure; - } - - if (input[pointer] === 58) { - if (compress !== null) { - return failure; - } - ++pointer; - ++pieceIndex; - compress = pieceIndex; - continue; - } - - let value = 0; - let length = 0; - - while (length < 4 && isASCIIHex(input[pointer])) { - value = value * 0x10 + parseInt(at(input, pointer), 16); - ++pointer; - ++length; - } - - if (input[pointer] === 46) { - if (length === 0) { - return failure; - } - - pointer -= length; - - if (pieceIndex > 6) { - return failure; - } - - let numbersSeen = 0; - - while (input[pointer] !== undefined) { - let ipv4Piece = null; - - if (numbersSeen > 0) { - if (input[pointer] === 46 && numbersSeen < 4) { - ++pointer; - } else { - return failure; - } - } - - if (!isASCIIDigit(input[pointer])) { - return failure; - } - - while (isASCIIDigit(input[pointer])) { - const number = parseInt(at(input, pointer)); - if (ipv4Piece === null) { - ipv4Piece = number; - } else if (ipv4Piece === 0) { - return failure; - } else { - ipv4Piece = ipv4Piece * 10 + number; - } - if (ipv4Piece > 255) { - return failure; - } - ++pointer; - } - - address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece; - - ++numbersSeen; - - if (numbersSeen === 2 || numbersSeen === 4) { - ++pieceIndex; - } - } - - if (numbersSeen !== 4) { - return failure; - } - - break; - } else if (input[pointer] === 58) { - ++pointer; - if (input[pointer] === undefined) { - return failure; - } - } else if (input[pointer] !== undefined) { - return failure; - } - - address[pieceIndex] = value; - ++pieceIndex; - } - - if (compress !== null) { - let swaps = pieceIndex - compress; - pieceIndex = 7; - while (pieceIndex !== 0 && swaps > 0) { - const temp = address[compress + swaps - 1]; - address[compress + swaps - 1] = address[pieceIndex]; - address[pieceIndex] = temp; - --pieceIndex; - --swaps; - } - } else if (compress === null && pieceIndex !== 8) { - return failure; - } - - return address; -} - -function serializeIPv6(address) { - let output = ""; - const seqResult = findLongestZeroSequence(address); - const compress = seqResult.idx; - let ignore0 = false; - - for (let pieceIndex = 0; pieceIndex <= 7; ++pieceIndex) { - if (ignore0 && address[pieceIndex] === 0) { - continue; - } else if (ignore0) { - ignore0 = false; - } - - if (compress === pieceIndex) { - const separator = pieceIndex === 0 ? "::" : ":"; - output += separator; - ignore0 = true; - continue; - } - - output += address[pieceIndex].toString(16); - - if (pieceIndex !== 7) { - output += ":"; - } - } - - return output; -} - -function parseHost(input, isSpecialArg) { - if (input[0] === "[") { - if (input[input.length - 1] !== "]") { - return failure; - } - - return parseIPv6(input.substring(1, input.length - 1)); - } - - if (!isSpecialArg) { - return parseOpaqueHost(input); - } - - const domain = utf8PercentDecode(input); - const asciiDomain = tr46.toASCII(domain, false, tr46.PROCESSING_OPTIONS.NONTRANSITIONAL, false); - if (asciiDomain === null) { - return failure; - } - - if (containsForbiddenHostCodePoint(asciiDomain)) { - return failure; - } - - const ipv4Host = parseIPv4(asciiDomain); - if (typeof ipv4Host === "number" || ipv4Host === failure) { - return ipv4Host; - } - - return asciiDomain; -} - -function parseOpaqueHost(input) { - if (containsForbiddenHostCodePointExcludingPercent(input)) { - return failure; - } - - let output = ""; - const decoded = punycode.ucs2.decode(input); - for (let i = 0; i < decoded.length; ++i) { - output += percentEncodeChar(decoded[i], isC0ControlPercentEncode); - } - return output; -} - -function findLongestZeroSequence(arr) { - let maxIdx = null; - let maxLen = 1; // only find elements > 1 - let currStart = null; - let currLen = 0; - - for (let i = 0; i < arr.length; ++i) { - if (arr[i] !== 0) { - if (currLen > maxLen) { - maxIdx = currStart; - maxLen = currLen; - } - - currStart = null; - currLen = 0; - } else { - if (currStart === null) { - currStart = i; - } - ++currLen; - } - } - - // if trailing zeros - if (currLen > maxLen) { - maxIdx = currStart; - maxLen = currLen; - } - - return { - idx: maxIdx, - len: maxLen - }; -} - -function serializeHost(host) { - if (typeof host === "number") { - return serializeIPv4(host); - } - - // IPv6 serializer - if (host instanceof Array) { - return "[" + serializeIPv6(host) + "]"; - } - - return host; -} - -function trimControlChars(url) { - return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/g, ""); -} - -function trimTabAndNewline(url) { - return url.replace(/\u0009|\u000A|\u000D/g, ""); -} - -function shortenPath(url) { - const path = url.path; - if (path.length === 0) { - return; - } - if (url.scheme === "file" && path.length === 1 && isNormalizedWindowsDriveLetter(path[0])) { - return; - } - - path.pop(); -} - -function includesCredentials(url) { - return url.username !== "" || url.password !== ""; -} - -function cannotHaveAUsernamePasswordPort(url) { - return url.host === null || url.host === "" || url.cannotBeABaseURL || url.scheme === "file"; -} - -function isNormalizedWindowsDriveLetter(string) { - return /^[A-Za-z]:$/.test(string); -} - -function URLStateMachine(input, base, encodingOverride, url, stateOverride) { - this.pointer = 0; - this.input = input; - this.base = base || null; - this.encodingOverride = encodingOverride || "utf-8"; - this.stateOverride = stateOverride; - this.url = url; - this.failure = false; - this.parseError = false; - - if (!this.url) { - this.url = { - scheme: "", - username: "", - password: "", - host: null, - port: null, - path: [], - query: null, - fragment: null, - - cannotBeABaseURL: false - }; - - const res = trimControlChars(this.input); - if (res !== this.input) { - this.parseError = true; - } - this.input = res; - } - - const res = trimTabAndNewline(this.input); - if (res !== this.input) { - this.parseError = true; - } - this.input = res; - - this.state = stateOverride || "scheme start"; - - this.buffer = ""; - this.atFlag = false; - this.arrFlag = false; - this.passwordTokenSeenFlag = false; - - this.input = punycode.ucs2.decode(this.input); - - for (; this.pointer <= this.input.length; ++this.pointer) { - const c = this.input[this.pointer]; - const cStr = isNaN(c) ? undefined : String.fromCodePoint(c); - - // exec state machine - const ret = this["parse " + this.state](c, cStr); - if (!ret) { - break; // terminate algorithm - } else if (ret === failure) { - this.failure = true; - break; - } - } -} - -URLStateMachine.prototype["parse scheme start"] = function parseSchemeStart(c, cStr) { - if (isASCIIAlpha(c)) { - this.buffer += cStr.toLowerCase(); - this.state = "scheme"; - } else if (!this.stateOverride) { - this.state = "no scheme"; - --this.pointer; - } else { - this.parseError = true; - return failure; - } - - return true; -}; - -URLStateMachine.prototype["parse scheme"] = function parseScheme(c, cStr) { - if (isASCIIAlphanumeric(c) || c === 43 || c === 45 || c === 46) { - this.buffer += cStr.toLowerCase(); - } else if (c === 58) { - if (this.stateOverride) { - if (isSpecial(this.url) && !isSpecialScheme(this.buffer)) { - return false; - } - - if (!isSpecial(this.url) && isSpecialScheme(this.buffer)) { - return false; - } - - if ((includesCredentials(this.url) || this.url.port !== null) && this.buffer === "file") { - return false; - } - - if (this.url.scheme === "file" && (this.url.host === "" || this.url.host === null)) { - return false; - } - } - this.url.scheme = this.buffer; - this.buffer = ""; - if (this.stateOverride) { - return false; - } - if (this.url.scheme === "file") { - if (this.input[this.pointer + 1] !== 47 || this.input[this.pointer + 2] !== 47) { - this.parseError = true; - } - this.state = "file"; - } else if (isSpecial(this.url) && this.base !== null && this.base.scheme === this.url.scheme) { - this.state = "special relative or authority"; - } else if (isSpecial(this.url)) { - this.state = "special authority slashes"; - } else if (this.input[this.pointer + 1] === 47) { - this.state = "path or authority"; - ++this.pointer; - } else { - this.url.cannotBeABaseURL = true; - this.url.path.push(""); - this.state = "cannot-be-a-base-URL path"; - } - } else if (!this.stateOverride) { - this.buffer = ""; - this.state = "no scheme"; - this.pointer = -1; - } else { - this.parseError = true; - return failure; - } - - return true; -}; - -URLStateMachine.prototype["parse no scheme"] = function parseNoScheme(c) { - if (this.base === null || (this.base.cannotBeABaseURL && c !== 35)) { - return failure; - } else if (this.base.cannotBeABaseURL && c === 35) { - this.url.scheme = this.base.scheme; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - this.url.fragment = ""; - this.url.cannotBeABaseURL = true; - this.state = "fragment"; - } else if (this.base.scheme === "file") { - this.state = "file"; - --this.pointer; - } else { - this.state = "relative"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse special relative or authority"] = function parseSpecialRelativeOrAuthority(c) { - if (c === 47 && this.input[this.pointer + 1] === 47) { - this.state = "special authority ignore slashes"; - ++this.pointer; - } else { - this.parseError = true; - this.state = "relative"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse path or authority"] = function parsePathOrAuthority(c) { - if (c === 47) { - this.state = "authority"; - } else { - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse relative"] = function parseRelative(c) { - this.url.scheme = this.base.scheme; - if (isNaN(c)) { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - } else if (c === 47) { - this.state = "relative slash"; - } else if (c === 63) { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.url.path = this.base.path.slice(); - this.url.query = ""; - this.state = "query"; - } else if (c === 35) { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - this.url.fragment = ""; - this.state = "fragment"; - } else if (isSpecial(this.url) && c === 92) { - this.parseError = true; - this.state = "relative slash"; - } else { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.url.path = this.base.path.slice(0, this.base.path.length - 1); - - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse relative slash"] = function parseRelativeSlash(c) { - if (isSpecial(this.url) && (c === 47 || c === 92)) { - if (c === 92) { - this.parseError = true; - } - this.state = "special authority ignore slashes"; - } else if (c === 47) { - this.state = "authority"; - } else { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse special authority slashes"] = function parseSpecialAuthoritySlashes(c) { - if (c === 47 && this.input[this.pointer + 1] === 47) { - this.state = "special authority ignore slashes"; - ++this.pointer; - } else { - this.parseError = true; - this.state = "special authority ignore slashes"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse special authority ignore slashes"] = function parseSpecialAuthorityIgnoreSlashes(c) { - if (c !== 47 && c !== 92) { - this.state = "authority"; - --this.pointer; - } else { - this.parseError = true; - } - - return true; -}; - -URLStateMachine.prototype["parse authority"] = function parseAuthority(c, cStr) { - if (c === 64) { - this.parseError = true; - if (this.atFlag) { - this.buffer = "%40" + this.buffer; - } - this.atFlag = true; - - // careful, this is based on buffer and has its own pointer (this.pointer != pointer) and inner chars - const len = countSymbols(this.buffer); - for (let pointer = 0; pointer < len; ++pointer) { - const codePoint = this.buffer.codePointAt(pointer); - - if (codePoint === 58 && !this.passwordTokenSeenFlag) { - this.passwordTokenSeenFlag = true; - continue; - } - const encodedCodePoints = percentEncodeChar(codePoint, isUserinfoPercentEncode); - if (this.passwordTokenSeenFlag) { - this.url.password += encodedCodePoints; - } else { - this.url.username += encodedCodePoints; - } - } - this.buffer = ""; - } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || - (isSpecial(this.url) && c === 92)) { - if (this.atFlag && this.buffer === "") { - this.parseError = true; - return failure; - } - this.pointer -= countSymbols(this.buffer) + 1; - this.buffer = ""; - this.state = "host"; - } else { - this.buffer += cStr; - } - - return true; -}; - -URLStateMachine.prototype["parse hostname"] = -URLStateMachine.prototype["parse host"] = function parseHostName(c, cStr) { - if (this.stateOverride && this.url.scheme === "file") { - --this.pointer; - this.state = "file host"; - } else if (c === 58 && !this.arrFlag) { - if (this.buffer === "") { - this.parseError = true; - return failure; - } - - const host = parseHost(this.buffer, isSpecial(this.url)); - if (host === failure) { - return failure; - } - - this.url.host = host; - this.buffer = ""; - this.state = "port"; - if (this.stateOverride === "hostname") { - return false; - } - } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || - (isSpecial(this.url) && c === 92)) { - --this.pointer; - if (isSpecial(this.url) && this.buffer === "") { - this.parseError = true; - return failure; - } else if (this.stateOverride && this.buffer === "" && - (includesCredentials(this.url) || this.url.port !== null)) { - this.parseError = true; - return false; - } - - const host = parseHost(this.buffer, isSpecial(this.url)); - if (host === failure) { - return failure; - } - - this.url.host = host; - this.buffer = ""; - this.state = "path start"; - if (this.stateOverride) { - return false; - } - } else { - if (c === 91) { - this.arrFlag = true; - } else if (c === 93) { - this.arrFlag = false; - } - this.buffer += cStr; - } - - return true; -}; - -URLStateMachine.prototype["parse port"] = function parsePort(c, cStr) { - if (isASCIIDigit(c)) { - this.buffer += cStr; - } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || - (isSpecial(this.url) && c === 92) || - this.stateOverride) { - if (this.buffer !== "") { - const port = parseInt(this.buffer); - if (port > Math.pow(2, 16) - 1) { - this.parseError = true; - return failure; - } - this.url.port = port === defaultPort(this.url.scheme) ? null : port; - this.buffer = ""; - } - if (this.stateOverride) { - return false; - } - this.state = "path start"; - --this.pointer; - } else { - this.parseError = true; - return failure; - } - - return true; -}; - -const fileOtherwiseCodePoints = new Set([47, 92, 63, 35]); - -URLStateMachine.prototype["parse file"] = function parseFile(c) { - this.url.scheme = "file"; - - if (c === 47 || c === 92) { - if (c === 92) { - this.parseError = true; - } - this.state = "file slash"; - } else if (this.base !== null && this.base.scheme === "file") { - if (isNaN(c)) { - this.url.host = this.base.host; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - } else if (c === 63) { - this.url.host = this.base.host; - this.url.path = this.base.path.slice(); - this.url.query = ""; - this.state = "query"; - } else if (c === 35) { - this.url.host = this.base.host; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - this.url.fragment = ""; - this.state = "fragment"; - } else { - if (this.input.length - this.pointer - 1 === 0 || // remaining consists of 0 code points - !isWindowsDriveLetterCodePoints(c, this.input[this.pointer + 1]) || - (this.input.length - this.pointer - 1 >= 2 && // remaining has at least 2 code points - !fileOtherwiseCodePoints.has(this.input[this.pointer + 2]))) { - this.url.host = this.base.host; - this.url.path = this.base.path.slice(); - shortenPath(this.url); - } else { - this.parseError = true; - } - - this.state = "path"; - --this.pointer; - } - } else { - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse file slash"] = function parseFileSlash(c) { - if (c === 47 || c === 92) { - if (c === 92) { - this.parseError = true; - } - this.state = "file host"; - } else { - if (this.base !== null && this.base.scheme === "file") { - if (isNormalizedWindowsDriveLetterString(this.base.path[0])) { - this.url.path.push(this.base.path[0]); - } else { - this.url.host = this.base.host; - } - } - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse file host"] = function parseFileHost(c, cStr) { - if (isNaN(c) || c === 47 || c === 92 || c === 63 || c === 35) { - --this.pointer; - if (!this.stateOverride && isWindowsDriveLetterString(this.buffer)) { - this.parseError = true; - this.state = "path"; - } else if (this.buffer === "") { - this.url.host = ""; - if (this.stateOverride) { - return false; - } - this.state = "path start"; - } else { - let host = parseHost(this.buffer, isSpecial(this.url)); - if (host === failure) { - return failure; - } - if (host === "localhost") { - host = ""; - } - this.url.host = host; - - if (this.stateOverride) { - return false; - } - - this.buffer = ""; - this.state = "path start"; - } - } else { - this.buffer += cStr; - } - - return true; -}; - -URLStateMachine.prototype["parse path start"] = function parsePathStart(c) { - if (isSpecial(this.url)) { - if (c === 92) { - this.parseError = true; - } - this.state = "path"; - - if (c !== 47 && c !== 92) { - --this.pointer; - } - } else if (!this.stateOverride && c === 63) { - this.url.query = ""; - this.state = "query"; - } else if (!this.stateOverride && c === 35) { - this.url.fragment = ""; - this.state = "fragment"; - } else if (c !== undefined) { - this.state = "path"; - if (c !== 47) { - --this.pointer; - } - } - - return true; -}; - -URLStateMachine.prototype["parse path"] = function parsePath(c) { - if (isNaN(c) || c === 47 || (isSpecial(this.url) && c === 92) || - (!this.stateOverride && (c === 63 || c === 35))) { - if (isSpecial(this.url) && c === 92) { - this.parseError = true; - } - - if (isDoubleDot(this.buffer)) { - shortenPath(this.url); - if (c !== 47 && !(isSpecial(this.url) && c === 92)) { - this.url.path.push(""); - } - } else if (isSingleDot(this.buffer) && c !== 47 && - !(isSpecial(this.url) && c === 92)) { - this.url.path.push(""); - } else if (!isSingleDot(this.buffer)) { - if (this.url.scheme === "file" && this.url.path.length === 0 && isWindowsDriveLetterString(this.buffer)) { - if (this.url.host !== "" && this.url.host !== null) { - this.parseError = true; - this.url.host = ""; - } - this.buffer = this.buffer[0] + ":"; - } - this.url.path.push(this.buffer); - } - this.buffer = ""; - if (this.url.scheme === "file" && (c === undefined || c === 63 || c === 35)) { - while (this.url.path.length > 1 && this.url.path[0] === "") { - this.parseError = true; - this.url.path.shift(); - } - } - if (c === 63) { - this.url.query = ""; - this.state = "query"; - } - if (c === 35) { - this.url.fragment = ""; - this.state = "fragment"; - } - } else { - // TODO: If c is not a URL code point and not "%", parse error. - - if (c === 37 && - (!isASCIIHex(this.input[this.pointer + 1]) || - !isASCIIHex(this.input[this.pointer + 2]))) { - this.parseError = true; - } - - this.buffer += percentEncodeChar(c, isPathPercentEncode); - } - - return true; -}; - -URLStateMachine.prototype["parse cannot-be-a-base-URL path"] = function parseCannotBeABaseURLPath(c) { - if (c === 63) { - this.url.query = ""; - this.state = "query"; - } else if (c === 35) { - this.url.fragment = ""; - this.state = "fragment"; - } else { - // TODO: Add: not a URL code point - if (!isNaN(c) && c !== 37) { - this.parseError = true; - } - - if (c === 37 && - (!isASCIIHex(this.input[this.pointer + 1]) || - !isASCIIHex(this.input[this.pointer + 2]))) { - this.parseError = true; - } - - if (!isNaN(c)) { - this.url.path[0] = this.url.path[0] + percentEncodeChar(c, isC0ControlPercentEncode); - } - } - - return true; -}; - -URLStateMachine.prototype["parse query"] = function parseQuery(c, cStr) { - if (isNaN(c) || (!this.stateOverride && c === 35)) { - if (!isSpecial(this.url) || this.url.scheme === "ws" || this.url.scheme === "wss") { - this.encodingOverride = "utf-8"; - } - - const buffer = new Buffer(this.buffer); // TODO: Use encoding override instead - for (let i = 0; i < buffer.length; ++i) { - if (buffer[i] < 0x21 || buffer[i] > 0x7E || buffer[i] === 0x22 || buffer[i] === 0x23 || - buffer[i] === 0x3C || buffer[i] === 0x3E) { - this.url.query += percentEncode(buffer[i]); - } else { - this.url.query += String.fromCodePoint(buffer[i]); - } - } - - this.buffer = ""; - if (c === 35) { - this.url.fragment = ""; - this.state = "fragment"; - } - } else { - // TODO: If c is not a URL code point and not "%", parse error. - if (c === 37 && - (!isASCIIHex(this.input[this.pointer + 1]) || - !isASCIIHex(this.input[this.pointer + 2]))) { - this.parseError = true; - } - - this.buffer += cStr; - } - - return true; -}; - -URLStateMachine.prototype["parse fragment"] = function parseFragment(c) { - if (isNaN(c)) { // do nothing - } else if (c === 0x0) { - this.parseError = true; - } else { - // TODO: If c is not a URL code point and not "%", parse error. - if (c === 37 && - (!isASCIIHex(this.input[this.pointer + 1]) || - !isASCIIHex(this.input[this.pointer + 2]))) { - this.parseError = true; - } - - this.url.fragment += percentEncodeChar(c, isC0ControlPercentEncode); - } - - return true; -}; - -function serializeURL(url, excludeFragment) { - let output = url.scheme + ":"; - if (url.host !== null) { - output += "//"; - - if (url.username !== "" || url.password !== "") { - output += url.username; - if (url.password !== "") { - output += ":" + url.password; - } - output += "@"; - } - - output += serializeHost(url.host); - - if (url.port !== null) { - output += ":" + url.port; - } - } else if (url.host === null && url.scheme === "file") { - output += "//"; - } - - if (url.cannotBeABaseURL) { - output += url.path[0]; - } else { - for (const string of url.path) { - output += "/" + string; - } - } - - if (url.query !== null) { - output += "?" + url.query; - } - - if (!excludeFragment && url.fragment !== null) { - output += "#" + url.fragment; - } - - return output; -} - -function serializeOrigin(tuple) { - let result = tuple.scheme + "://"; - result += serializeHost(tuple.host); - - if (tuple.port !== null) { - result += ":" + tuple.port; - } - - return result; -} - -module.exports.serializeURL = serializeURL; - -module.exports.serializeURLOrigin = function (url) { - // https://url.spec.whatwg.org/#concept-url-origin - switch (url.scheme) { - case "blob": - try { - return module.exports.serializeURLOrigin(module.exports.parseURL(url.path[0])); - } catch (e) { - // serializing an opaque origin returns "null" - return "null"; - } - case "ftp": - case "gopher": - case "http": - case "https": - case "ws": - case "wss": - return serializeOrigin({ - scheme: url.scheme, - host: url.host, - port: url.port - }); - case "file": - // spec says "exercise to the reader", chrome says "file://" - return "file://"; - default: - // serializing an opaque origin returns "null" - return "null"; - } -}; - -module.exports.basicURLParse = function (input, options) { - if (options === undefined) { - options = {}; - } - - const usm = new URLStateMachine(input, options.baseURL, options.encodingOverride, options.url, options.stateOverride); - if (usm.failure) { - return "failure"; - } - - return usm.url; -}; - -module.exports.setTheUsername = function (url, username) { - url.username = ""; - const decoded = punycode.ucs2.decode(username); - for (let i = 0; i < decoded.length; ++i) { - url.username += percentEncodeChar(decoded[i], isUserinfoPercentEncode); - } -}; - -module.exports.setThePassword = function (url, password) { - url.password = ""; - const decoded = punycode.ucs2.decode(password); - for (let i = 0; i < decoded.length; ++i) { - url.password += percentEncodeChar(decoded[i], isUserinfoPercentEncode); - } -}; - -module.exports.serializeHost = serializeHost; - -module.exports.cannotHaveAUsernamePasswordPort = cannotHaveAUsernamePasswordPort; - -module.exports.serializeInteger = function (integer) { - return String(integer); -}; - -module.exports.parseURL = function (input, options) { - if (options === undefined) { - options = {}; - } - - // We don't handle blobs, so this just delegates: - return module.exports.basicURLParse(input, { baseURL: options.baseURL, encodingOverride: options.encodingOverride }); -}; + +const punycode = __nccwpck_require__(85477); +const tr46 = __nccwpck_require__(84256); + +const specialSchemes = { + ftp: 21, + file: null, + gopher: 70, + http: 80, + https: 443, + ws: 80, + wss: 443 +}; + +const failure = Symbol("failure"); + +function countSymbols(str) { + return punycode.ucs2.decode(str).length; +} + +function at(input, idx) { + const c = input[idx]; + return isNaN(c) ? undefined : String.fromCodePoint(c); +} + +function isASCIIDigit(c) { + return c >= 0x30 && c <= 0x39; +} + +function isASCIIAlpha(c) { + return (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A); +} + +function isASCIIAlphanumeric(c) { + return isASCIIAlpha(c) || isASCIIDigit(c); +} + +function isASCIIHex(c) { + return isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66); +} + +function isSingleDot(buffer) { + return buffer === "." || buffer.toLowerCase() === "%2e"; +} + +function isDoubleDot(buffer) { + buffer = buffer.toLowerCase(); + return buffer === ".." || buffer === "%2e." || buffer === ".%2e" || buffer === "%2e%2e"; +} + +function isWindowsDriveLetterCodePoints(cp1, cp2) { + return isASCIIAlpha(cp1) && (cp2 === 58 || cp2 === 124); +} + +function isWindowsDriveLetterString(string) { + return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && (string[1] === ":" || string[1] === "|"); +} + +function isNormalizedWindowsDriveLetterString(string) { + return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && string[1] === ":"; +} + +function containsForbiddenHostCodePoint(string) { + return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|%|\/|:|\?|@|\[|\\|\]/) !== -1; +} + +function containsForbiddenHostCodePointExcludingPercent(string) { + return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|\/|:|\?|@|\[|\\|\]/) !== -1; +} + +function isSpecialScheme(scheme) { + return specialSchemes[scheme] !== undefined; +} + +function isSpecial(url) { + return isSpecialScheme(url.scheme); +} + +function defaultPort(scheme) { + return specialSchemes[scheme]; +} + +function percentEncode(c) { + let hex = c.toString(16).toUpperCase(); + if (hex.length === 1) { + hex = "0" + hex; + } + + return "%" + hex; +} + +function utf8PercentEncode(c) { + const buf = new Buffer(c); + + let str = ""; + + for (let i = 0; i < buf.length; ++i) { + str += percentEncode(buf[i]); + } + + return str; +} + +function utf8PercentDecode(str) { + const input = new Buffer(str); + const output = []; + for (let i = 0; i < input.length; ++i) { + if (input[i] !== 37) { + output.push(input[i]); + } else if (input[i] === 37 && isASCIIHex(input[i + 1]) && isASCIIHex(input[i + 2])) { + output.push(parseInt(input.slice(i + 1, i + 3).toString(), 16)); + i += 2; + } else { + output.push(input[i]); + } + } + return new Buffer(output).toString(); +} + +function isC0ControlPercentEncode(c) { + return c <= 0x1F || c > 0x7E; +} + +const extraPathPercentEncodeSet = new Set([32, 34, 35, 60, 62, 63, 96, 123, 125]); +function isPathPercentEncode(c) { + return isC0ControlPercentEncode(c) || extraPathPercentEncodeSet.has(c); +} + +const extraUserinfoPercentEncodeSet = + new Set([47, 58, 59, 61, 64, 91, 92, 93, 94, 124]); +function isUserinfoPercentEncode(c) { + return isPathPercentEncode(c) || extraUserinfoPercentEncodeSet.has(c); +} + +function percentEncodeChar(c, encodeSetPredicate) { + const cStr = String.fromCodePoint(c); + + if (encodeSetPredicate(c)) { + return utf8PercentEncode(cStr); + } + + return cStr; +} + +function parseIPv4Number(input) { + let R = 10; + + if (input.length >= 2 && input.charAt(0) === "0" && input.charAt(1).toLowerCase() === "x") { + input = input.substring(2); + R = 16; + } else if (input.length >= 2 && input.charAt(0) === "0") { + input = input.substring(1); + R = 8; + } + + if (input === "") { + return 0; + } + + const regex = R === 10 ? /[^0-9]/ : (R === 16 ? /[^0-9A-Fa-f]/ : /[^0-7]/); + if (regex.test(input)) { + return failure; + } + + return parseInt(input, R); +} + +function parseIPv4(input) { + const parts = input.split("."); + if (parts[parts.length - 1] === "") { + if (parts.length > 1) { + parts.pop(); + } + } + + if (parts.length > 4) { + return input; + } + + const numbers = []; + for (const part of parts) { + if (part === "") { + return input; + } + const n = parseIPv4Number(part); + if (n === failure) { + return input; + } + + numbers.push(n); + } + + for (let i = 0; i < numbers.length - 1; ++i) { + if (numbers[i] > 255) { + return failure; + } + } + if (numbers[numbers.length - 1] >= Math.pow(256, 5 - numbers.length)) { + return failure; + } + + let ipv4 = numbers.pop(); + let counter = 0; + + for (const n of numbers) { + ipv4 += n * Math.pow(256, 3 - counter); + ++counter; + } + + return ipv4; +} + +function serializeIPv4(address) { + let output = ""; + let n = address; + + for (let i = 1; i <= 4; ++i) { + output = String(n % 256) + output; + if (i !== 4) { + output = "." + output; + } + n = Math.floor(n / 256); + } + + return output; +} + +function parseIPv6(input) { + const address = [0, 0, 0, 0, 0, 0, 0, 0]; + let pieceIndex = 0; + let compress = null; + let pointer = 0; + + input = punycode.ucs2.decode(input); + + if (input[pointer] === 58) { + if (input[pointer + 1] !== 58) { + return failure; + } + + pointer += 2; + ++pieceIndex; + compress = pieceIndex; + } + + while (pointer < input.length) { + if (pieceIndex === 8) { + return failure; + } + + if (input[pointer] === 58) { + if (compress !== null) { + return failure; + } + ++pointer; + ++pieceIndex; + compress = pieceIndex; + continue; + } + + let value = 0; + let length = 0; + + while (length < 4 && isASCIIHex(input[pointer])) { + value = value * 0x10 + parseInt(at(input, pointer), 16); + ++pointer; + ++length; + } + + if (input[pointer] === 46) { + if (length === 0) { + return failure; + } + + pointer -= length; + + if (pieceIndex > 6) { + return failure; + } + + let numbersSeen = 0; + + while (input[pointer] !== undefined) { + let ipv4Piece = null; + + if (numbersSeen > 0) { + if (input[pointer] === 46 && numbersSeen < 4) { + ++pointer; + } else { + return failure; + } + } + + if (!isASCIIDigit(input[pointer])) { + return failure; + } + + while (isASCIIDigit(input[pointer])) { + const number = parseInt(at(input, pointer)); + if (ipv4Piece === null) { + ipv4Piece = number; + } else if (ipv4Piece === 0) { + return failure; + } else { + ipv4Piece = ipv4Piece * 10 + number; + } + if (ipv4Piece > 255) { + return failure; + } + ++pointer; + } + + address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece; + + ++numbersSeen; + + if (numbersSeen === 2 || numbersSeen === 4) { + ++pieceIndex; + } + } + + if (numbersSeen !== 4) { + return failure; + } + + break; + } else if (input[pointer] === 58) { + ++pointer; + if (input[pointer] === undefined) { + return failure; + } + } else if (input[pointer] !== undefined) { + return failure; + } + + address[pieceIndex] = value; + ++pieceIndex; + } + + if (compress !== null) { + let swaps = pieceIndex - compress; + pieceIndex = 7; + while (pieceIndex !== 0 && swaps > 0) { + const temp = address[compress + swaps - 1]; + address[compress + swaps - 1] = address[pieceIndex]; + address[pieceIndex] = temp; + --pieceIndex; + --swaps; + } + } else if (compress === null && pieceIndex !== 8) { + return failure; + } + + return address; +} + +function serializeIPv6(address) { + let output = ""; + const seqResult = findLongestZeroSequence(address); + const compress = seqResult.idx; + let ignore0 = false; + + for (let pieceIndex = 0; pieceIndex <= 7; ++pieceIndex) { + if (ignore0 && address[pieceIndex] === 0) { + continue; + } else if (ignore0) { + ignore0 = false; + } + + if (compress === pieceIndex) { + const separator = pieceIndex === 0 ? "::" : ":"; + output += separator; + ignore0 = true; + continue; + } + + output += address[pieceIndex].toString(16); + + if (pieceIndex !== 7) { + output += ":"; + } + } + + return output; +} + +function parseHost(input, isSpecialArg) { + if (input[0] === "[") { + if (input[input.length - 1] !== "]") { + return failure; + } + + return parseIPv6(input.substring(1, input.length - 1)); + } + + if (!isSpecialArg) { + return parseOpaqueHost(input); + } + + const domain = utf8PercentDecode(input); + const asciiDomain = tr46.toASCII(domain, false, tr46.PROCESSING_OPTIONS.NONTRANSITIONAL, false); + if (asciiDomain === null) { + return failure; + } + + if (containsForbiddenHostCodePoint(asciiDomain)) { + return failure; + } + + const ipv4Host = parseIPv4(asciiDomain); + if (typeof ipv4Host === "number" || ipv4Host === failure) { + return ipv4Host; + } + + return asciiDomain; +} + +function parseOpaqueHost(input) { + if (containsForbiddenHostCodePointExcludingPercent(input)) { + return failure; + } + + let output = ""; + const decoded = punycode.ucs2.decode(input); + for (let i = 0; i < decoded.length; ++i) { + output += percentEncodeChar(decoded[i], isC0ControlPercentEncode); + } + return output; +} + +function findLongestZeroSequence(arr) { + let maxIdx = null; + let maxLen = 1; // only find elements > 1 + let currStart = null; + let currLen = 0; + + for (let i = 0; i < arr.length; ++i) { + if (arr[i] !== 0) { + if (currLen > maxLen) { + maxIdx = currStart; + maxLen = currLen; + } + + currStart = null; + currLen = 0; + } else { + if (currStart === null) { + currStart = i; + } + ++currLen; + } + } + + // if trailing zeros + if (currLen > maxLen) { + maxIdx = currStart; + maxLen = currLen; + } + + return { + idx: maxIdx, + len: maxLen + }; +} + +function serializeHost(host) { + if (typeof host === "number") { + return serializeIPv4(host); + } + + // IPv6 serializer + if (host instanceof Array) { + return "[" + serializeIPv6(host) + "]"; + } + + return host; +} + +function trimControlChars(url) { + return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/g, ""); +} + +function trimTabAndNewline(url) { + return url.replace(/\u0009|\u000A|\u000D/g, ""); +} + +function shortenPath(url) { + const path = url.path; + if (path.length === 0) { + return; + } + if (url.scheme === "file" && path.length === 1 && isNormalizedWindowsDriveLetter(path[0])) { + return; + } + + path.pop(); +} + +function includesCredentials(url) { + return url.username !== "" || url.password !== ""; +} + +function cannotHaveAUsernamePasswordPort(url) { + return url.host === null || url.host === "" || url.cannotBeABaseURL || url.scheme === "file"; +} + +function isNormalizedWindowsDriveLetter(string) { + return /^[A-Za-z]:$/.test(string); +} + +function URLStateMachine(input, base, encodingOverride, url, stateOverride) { + this.pointer = 0; + this.input = input; + this.base = base || null; + this.encodingOverride = encodingOverride || "utf-8"; + this.stateOverride = stateOverride; + this.url = url; + this.failure = false; + this.parseError = false; + + if (!this.url) { + this.url = { + scheme: "", + username: "", + password: "", + host: null, + port: null, + path: [], + query: null, + fragment: null, + + cannotBeABaseURL: false + }; + + const res = trimControlChars(this.input); + if (res !== this.input) { + this.parseError = true; + } + this.input = res; + } + + const res = trimTabAndNewline(this.input); + if (res !== this.input) { + this.parseError = true; + } + this.input = res; + + this.state = stateOverride || "scheme start"; + + this.buffer = ""; + this.atFlag = false; + this.arrFlag = false; + this.passwordTokenSeenFlag = false; + + this.input = punycode.ucs2.decode(this.input); + + for (; this.pointer <= this.input.length; ++this.pointer) { + const c = this.input[this.pointer]; + const cStr = isNaN(c) ? undefined : String.fromCodePoint(c); + + // exec state machine + const ret = this["parse " + this.state](c, cStr); + if (!ret) { + break; // terminate algorithm + } else if (ret === failure) { + this.failure = true; + break; + } + } +} + +URLStateMachine.prototype["parse scheme start"] = function parseSchemeStart(c, cStr) { + if (isASCIIAlpha(c)) { + this.buffer += cStr.toLowerCase(); + this.state = "scheme"; + } else if (!this.stateOverride) { + this.state = "no scheme"; + --this.pointer; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +URLStateMachine.prototype["parse scheme"] = function parseScheme(c, cStr) { + if (isASCIIAlphanumeric(c) || c === 43 || c === 45 || c === 46) { + this.buffer += cStr.toLowerCase(); + } else if (c === 58) { + if (this.stateOverride) { + if (isSpecial(this.url) && !isSpecialScheme(this.buffer)) { + return false; + } + + if (!isSpecial(this.url) && isSpecialScheme(this.buffer)) { + return false; + } + + if ((includesCredentials(this.url) || this.url.port !== null) && this.buffer === "file") { + return false; + } + + if (this.url.scheme === "file" && (this.url.host === "" || this.url.host === null)) { + return false; + } + } + this.url.scheme = this.buffer; + this.buffer = ""; + if (this.stateOverride) { + return false; + } + if (this.url.scheme === "file") { + if (this.input[this.pointer + 1] !== 47 || this.input[this.pointer + 2] !== 47) { + this.parseError = true; + } + this.state = "file"; + } else if (isSpecial(this.url) && this.base !== null && this.base.scheme === this.url.scheme) { + this.state = "special relative or authority"; + } else if (isSpecial(this.url)) { + this.state = "special authority slashes"; + } else if (this.input[this.pointer + 1] === 47) { + this.state = "path or authority"; + ++this.pointer; + } else { + this.url.cannotBeABaseURL = true; + this.url.path.push(""); + this.state = "cannot-be-a-base-URL path"; + } + } else if (!this.stateOverride) { + this.buffer = ""; + this.state = "no scheme"; + this.pointer = -1; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +URLStateMachine.prototype["parse no scheme"] = function parseNoScheme(c) { + if (this.base === null || (this.base.cannotBeABaseURL && c !== 35)) { + return failure; + } else if (this.base.cannotBeABaseURL && c === 35) { + this.url.scheme = this.base.scheme; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.url.cannotBeABaseURL = true; + this.state = "fragment"; + } else if (this.base.scheme === "file") { + this.state = "file"; + --this.pointer; + } else { + this.state = "relative"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special relative or authority"] = function parseSpecialRelativeOrAuthority(c) { + if (c === 47 && this.input[this.pointer + 1] === 47) { + this.state = "special authority ignore slashes"; + ++this.pointer; + } else { + this.parseError = true; + this.state = "relative"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse path or authority"] = function parsePathOrAuthority(c) { + if (c === 47) { + this.state = "authority"; + } else { + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse relative"] = function parseRelative(c) { + this.url.scheme = this.base.scheme; + if (isNaN(c)) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + } else if (c === 47) { + this.state = "relative slash"; + } else if (c === 63) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.state = "fragment"; + } else if (isSpecial(this.url) && c === 92) { + this.parseError = true; + this.state = "relative slash"; + } else { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(0, this.base.path.length - 1); + + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse relative slash"] = function parseRelativeSlash(c) { + if (isSpecial(this.url) && (c === 47 || c === 92)) { + if (c === 92) { + this.parseError = true; + } + this.state = "special authority ignore slashes"; + } else if (c === 47) { + this.state = "authority"; + } else { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special authority slashes"] = function parseSpecialAuthoritySlashes(c) { + if (c === 47 && this.input[this.pointer + 1] === 47) { + this.state = "special authority ignore slashes"; + ++this.pointer; + } else { + this.parseError = true; + this.state = "special authority ignore slashes"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special authority ignore slashes"] = function parseSpecialAuthorityIgnoreSlashes(c) { + if (c !== 47 && c !== 92) { + this.state = "authority"; + --this.pointer; + } else { + this.parseError = true; + } + + return true; +}; + +URLStateMachine.prototype["parse authority"] = function parseAuthority(c, cStr) { + if (c === 64) { + this.parseError = true; + if (this.atFlag) { + this.buffer = "%40" + this.buffer; + } + this.atFlag = true; + + // careful, this is based on buffer and has its own pointer (this.pointer != pointer) and inner chars + const len = countSymbols(this.buffer); + for (let pointer = 0; pointer < len; ++pointer) { + const codePoint = this.buffer.codePointAt(pointer); + + if (codePoint === 58 && !this.passwordTokenSeenFlag) { + this.passwordTokenSeenFlag = true; + continue; + } + const encodedCodePoints = percentEncodeChar(codePoint, isUserinfoPercentEncode); + if (this.passwordTokenSeenFlag) { + this.url.password += encodedCodePoints; + } else { + this.url.username += encodedCodePoints; + } + } + this.buffer = ""; + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92)) { + if (this.atFlag && this.buffer === "") { + this.parseError = true; + return failure; + } + this.pointer -= countSymbols(this.buffer) + 1; + this.buffer = ""; + this.state = "host"; + } else { + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse hostname"] = +URLStateMachine.prototype["parse host"] = function parseHostName(c, cStr) { + if (this.stateOverride && this.url.scheme === "file") { + --this.pointer; + this.state = "file host"; + } else if (c === 58 && !this.arrFlag) { + if (this.buffer === "") { + this.parseError = true; + return failure; + } + + const host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + + this.url.host = host; + this.buffer = ""; + this.state = "port"; + if (this.stateOverride === "hostname") { + return false; + } + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92)) { + --this.pointer; + if (isSpecial(this.url) && this.buffer === "") { + this.parseError = true; + return failure; + } else if (this.stateOverride && this.buffer === "" && + (includesCredentials(this.url) || this.url.port !== null)) { + this.parseError = true; + return false; + } + + const host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + + this.url.host = host; + this.buffer = ""; + this.state = "path start"; + if (this.stateOverride) { + return false; + } + } else { + if (c === 91) { + this.arrFlag = true; + } else if (c === 93) { + this.arrFlag = false; + } + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse port"] = function parsePort(c, cStr) { + if (isASCIIDigit(c)) { + this.buffer += cStr; + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92) || + this.stateOverride) { + if (this.buffer !== "") { + const port = parseInt(this.buffer); + if (port > Math.pow(2, 16) - 1) { + this.parseError = true; + return failure; + } + this.url.port = port === defaultPort(this.url.scheme) ? null : port; + this.buffer = ""; + } + if (this.stateOverride) { + return false; + } + this.state = "path start"; + --this.pointer; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +const fileOtherwiseCodePoints = new Set([47, 92, 63, 35]); + +URLStateMachine.prototype["parse file"] = function parseFile(c) { + this.url.scheme = "file"; + + if (c === 47 || c === 92) { + if (c === 92) { + this.parseError = true; + } + this.state = "file slash"; + } else if (this.base !== null && this.base.scheme === "file") { + if (isNaN(c)) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + } else if (c === 63) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.state = "fragment"; + } else { + if (this.input.length - this.pointer - 1 === 0 || // remaining consists of 0 code points + !isWindowsDriveLetterCodePoints(c, this.input[this.pointer + 1]) || + (this.input.length - this.pointer - 1 >= 2 && // remaining has at least 2 code points + !fileOtherwiseCodePoints.has(this.input[this.pointer + 2]))) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + shortenPath(this.url); + } else { + this.parseError = true; + } + + this.state = "path"; + --this.pointer; + } + } else { + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse file slash"] = function parseFileSlash(c) { + if (c === 47 || c === 92) { + if (c === 92) { + this.parseError = true; + } + this.state = "file host"; + } else { + if (this.base !== null && this.base.scheme === "file") { + if (isNormalizedWindowsDriveLetterString(this.base.path[0])) { + this.url.path.push(this.base.path[0]); + } else { + this.url.host = this.base.host; + } + } + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse file host"] = function parseFileHost(c, cStr) { + if (isNaN(c) || c === 47 || c === 92 || c === 63 || c === 35) { + --this.pointer; + if (!this.stateOverride && isWindowsDriveLetterString(this.buffer)) { + this.parseError = true; + this.state = "path"; + } else if (this.buffer === "") { + this.url.host = ""; + if (this.stateOverride) { + return false; + } + this.state = "path start"; + } else { + let host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + if (host === "localhost") { + host = ""; + } + this.url.host = host; + + if (this.stateOverride) { + return false; + } + + this.buffer = ""; + this.state = "path start"; + } + } else { + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse path start"] = function parsePathStart(c) { + if (isSpecial(this.url)) { + if (c === 92) { + this.parseError = true; + } + this.state = "path"; + + if (c !== 47 && c !== 92) { + --this.pointer; + } + } else if (!this.stateOverride && c === 63) { + this.url.query = ""; + this.state = "query"; + } else if (!this.stateOverride && c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } else if (c !== undefined) { + this.state = "path"; + if (c !== 47) { + --this.pointer; + } + } + + return true; +}; + +URLStateMachine.prototype["parse path"] = function parsePath(c) { + if (isNaN(c) || c === 47 || (isSpecial(this.url) && c === 92) || + (!this.stateOverride && (c === 63 || c === 35))) { + if (isSpecial(this.url) && c === 92) { + this.parseError = true; + } + + if (isDoubleDot(this.buffer)) { + shortenPath(this.url); + if (c !== 47 && !(isSpecial(this.url) && c === 92)) { + this.url.path.push(""); + } + } else if (isSingleDot(this.buffer) && c !== 47 && + !(isSpecial(this.url) && c === 92)) { + this.url.path.push(""); + } else if (!isSingleDot(this.buffer)) { + if (this.url.scheme === "file" && this.url.path.length === 0 && isWindowsDriveLetterString(this.buffer)) { + if (this.url.host !== "" && this.url.host !== null) { + this.parseError = true; + this.url.host = ""; + } + this.buffer = this.buffer[0] + ":"; + } + this.url.path.push(this.buffer); + } + this.buffer = ""; + if (this.url.scheme === "file" && (c === undefined || c === 63 || c === 35)) { + while (this.url.path.length > 1 && this.url.path[0] === "") { + this.parseError = true; + this.url.path.shift(); + } + } + if (c === 63) { + this.url.query = ""; + this.state = "query"; + } + if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } + } else { + // TODO: If c is not a URL code point and not "%", parse error. + + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.buffer += percentEncodeChar(c, isPathPercentEncode); + } + + return true; +}; + +URLStateMachine.prototype["parse cannot-be-a-base-URL path"] = function parseCannotBeABaseURLPath(c) { + if (c === 63) { + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } else { + // TODO: Add: not a URL code point + if (!isNaN(c) && c !== 37) { + this.parseError = true; + } + + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + if (!isNaN(c)) { + this.url.path[0] = this.url.path[0] + percentEncodeChar(c, isC0ControlPercentEncode); + } + } + + return true; +}; + +URLStateMachine.prototype["parse query"] = function parseQuery(c, cStr) { + if (isNaN(c) || (!this.stateOverride && c === 35)) { + if (!isSpecial(this.url) || this.url.scheme === "ws" || this.url.scheme === "wss") { + this.encodingOverride = "utf-8"; + } + + const buffer = new Buffer(this.buffer); // TODO: Use encoding override instead + for (let i = 0; i < buffer.length; ++i) { + if (buffer[i] < 0x21 || buffer[i] > 0x7E || buffer[i] === 0x22 || buffer[i] === 0x23 || + buffer[i] === 0x3C || buffer[i] === 0x3E) { + this.url.query += percentEncode(buffer[i]); + } else { + this.url.query += String.fromCodePoint(buffer[i]); + } + } + + this.buffer = ""; + if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } + } else { + // TODO: If c is not a URL code point and not "%", parse error. + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse fragment"] = function parseFragment(c) { + if (isNaN(c)) { // do nothing + } else if (c === 0x0) { + this.parseError = true; + } else { + // TODO: If c is not a URL code point and not "%", parse error. + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.url.fragment += percentEncodeChar(c, isC0ControlPercentEncode); + } + + return true; +}; + +function serializeURL(url, excludeFragment) { + let output = url.scheme + ":"; + if (url.host !== null) { + output += "//"; + + if (url.username !== "" || url.password !== "") { + output += url.username; + if (url.password !== "") { + output += ":" + url.password; + } + output += "@"; + } + + output += serializeHost(url.host); + + if (url.port !== null) { + output += ":" + url.port; + } + } else if (url.host === null && url.scheme === "file") { + output += "//"; + } + + if (url.cannotBeABaseURL) { + output += url.path[0]; + } else { + for (const string of url.path) { + output += "/" + string; + } + } + + if (url.query !== null) { + output += "?" + url.query; + } + + if (!excludeFragment && url.fragment !== null) { + output += "#" + url.fragment; + } + + return output; +} + +function serializeOrigin(tuple) { + let result = tuple.scheme + "://"; + result += serializeHost(tuple.host); + + if (tuple.port !== null) { + result += ":" + tuple.port; + } + + return result; +} + +module.exports.serializeURL = serializeURL; + +module.exports.serializeURLOrigin = function (url) { + // https://url.spec.whatwg.org/#concept-url-origin + switch (url.scheme) { + case "blob": + try { + return module.exports.serializeURLOrigin(module.exports.parseURL(url.path[0])); + } catch (e) { + // serializing an opaque origin returns "null" + return "null"; + } + case "ftp": + case "gopher": + case "http": + case "https": + case "ws": + case "wss": + return serializeOrigin({ + scheme: url.scheme, + host: url.host, + port: url.port + }); + case "file": + // spec says "exercise to the reader", chrome says "file://" + return "file://"; + default: + // serializing an opaque origin returns "null" + return "null"; + } +}; + +module.exports.basicURLParse = function (input, options) { + if (options === undefined) { + options = {}; + } + + const usm = new URLStateMachine(input, options.baseURL, options.encodingOverride, options.url, options.stateOverride); + if (usm.failure) { + return "failure"; + } + + return usm.url; +}; + +module.exports.setTheUsername = function (url, username) { + url.username = ""; + const decoded = punycode.ucs2.decode(username); + for (let i = 0; i < decoded.length; ++i) { + url.username += percentEncodeChar(decoded[i], isUserinfoPercentEncode); + } +}; + +module.exports.setThePassword = function (url, password) { + url.password = ""; + const decoded = punycode.ucs2.decode(password); + for (let i = 0; i < decoded.length; ++i) { + url.password += percentEncodeChar(decoded[i], isUserinfoPercentEncode); + } +}; + +module.exports.serializeHost = serializeHost; + +module.exports.cannotHaveAUsernamePasswordPort = cannotHaveAUsernamePasswordPort; + +module.exports.serializeInteger = function (integer) { + return String(integer); +}; + +module.exports.parseURL = function (input, options) { + if (options === undefined) { + options = {}; + } + + // We don't handle blobs, so this just delegates: + return module.exports.basicURLParse(input, { baseURL: options.baseURL, encodingOverride: options.encodingOverride }); +}; /***/ }), @@ -737847,7 +737847,7 @@ module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"] /************************************************************************/ /******/ // The module cache /******/ var __webpack_module_cache__ = {}; -/******/ +/******/ /******/ // The require function /******/ function __nccwpck_require__(moduleId) { /******/ // Check if module is in cache @@ -737861,7 +737861,7 @@ module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"] /******/ // no module.loaded needed /******/ exports: {} /******/ }; -/******/ +/******/ /******/ // Execute the module function /******/ var threw = true; /******/ try { @@ -737870,23 +737870,23 @@ module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"] /******/ } finally { /******/ if(threw) delete __webpack_module_cache__[moduleId]; /******/ } -/******/ +/******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } -/******/ +/******/ /************************************************************************/ /******/ /* webpack/runtime/compat */ -/******/ +/******/ /******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; -/******/ +/******/ /************************************************************************/ -/******/ +/******/ /******/ // startup /******/ // Load entry module and return exports /******/ // This entry module is referenced by other modules so it can't be inlined /******/ var __webpack_exports__ = __nccwpck_require__(45555); /******/ module.exports = __webpack_exports__; -/******/ +/******/ /******/ })() ; diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e7d974bb8bd7e..366c3fc68fa63 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1473,6 +1473,9 @@ const CONST = { APPLY_AIRSHIP_UPDATES: 'apply_airship_updates', APPLY_PUSHER_UPDATES: 'apply_pusher_updates', APPLY_HTTPS_UPDATES: 'apply_https_updates', + COMPUTE_REPORT_NAME: 'compute_report_name', + COMPUTE_REPORT_NAME_FOR_NEW_REPORT: 'compute_report_name_for_new_report', + UPDATE_OPTIMISTIC_REPORT_NAMES: 'update_optimistic_report_names', COLD: 'cold', WARM: 'warm', REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500, diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 44f8e68dbe6ad..5cf6f9b3540b4 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -1,9 +1,12 @@ import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as CustomFormula from '@libs/CustomFormula'; +import Log from '@libs/Log'; +import * as Performance from '@libs/Performance'; import Permissions from '@libs/Permissions'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import * as Timing from '@libs/actions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type Beta from '@src/types/onyx/Beta'; @@ -115,21 +118,89 @@ function shouldComputeReportName(report: Report, policy: Policy | undefined): bo return CustomFormula.isFormula(titleField.defaultValue); } +/** + * Compute a report name for a new report being created + * This handles the case where the report doesn't exist in context yet + */ +function computeNameForNewReport(update: OnyxUpdate, context: UpdateContext): {reportName: string} | null { + Performance.markStart(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); + Timing.start(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); + + Log.info('[OptimisticReportNames] Computing name for new report', false, { + updateKey: update.key, + reportID: (update.value as Report)?.reportID, + }); + + const {allPolicies} = context; + + // Extract the new report data from the update + const newReport = update.value as Report; + if (!newReport?.policyID) { + Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); + Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); + return null; + } + + const policy = getPolicyByID(newReport.policyID, allPolicies); + if (!shouldComputeReportName(newReport, policy)) { + Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); + Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); + return null; + } + + const titleField = ReportUtils.getTitleReportField(policy?.fieldList ?? {}); + if (!titleField?.defaultValue) { + Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); + Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); + return null; + } + + // Build context for formula computation + const formulaContext: CustomFormula.FormulaContext = { + report: newReport, + policy, + }; + + const newName = CustomFormula.compute(titleField.defaultValue, formulaContext); + + if (newName && newName !== newReport.reportName) { + Log.info('[OptimisticReportNames] New report name computed successfully', false, { + reportID: newReport.reportID, + oldName: newReport.reportName, + newName, + formula: titleField.defaultValue, + }); + + Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); + Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); + return {reportName: newName}; + } + + Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); + Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); + return null; +} + /** * Compute a new report name if needed based on an optimistic update */ function computeReportNameIfNeeded(report: Report, incomingUpdate: OnyxUpdate, context: UpdateContext): {reportName: string} | null { - console.log('morwa: computeReportNameIfNeeded called', {reportID: report.reportID, updateKey: incomingUpdate.key}); + Performance.markStart(CONST.TIMING.COMPUTE_REPORT_NAME); + Timing.start(CONST.TIMING.COMPUTE_REPORT_NAME); const {allPolicies} = context; const policy = getPolicyByID(report.policyID ?? '', allPolicies); if (!shouldComputeReportName(report, policy)) { + Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); + Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); return null; } const titleField = ReportUtils.getTitleReportField(policy?.fieldList ?? {}); if (!titleField?.defaultValue) { + Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); + Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); return null; } @@ -150,6 +221,8 @@ function computeReportNameIfNeeded(report: Report, incomingUpdate: OnyxUpdate, c }); if (!isAffected) { + Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); + Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); return null; } @@ -168,15 +241,21 @@ function computeReportNameIfNeeded(report: Report, incomingUpdate: OnyxUpdate, c // Only return an update if the name actually changed if (newName && newName !== report.reportName) { - console.log('morwa: Report name computed', { + Log.info('[OptimisticReportNames] Report name computed for existing report', false, { reportID: report.reportID, oldName: report.reportName, newName, formula, + updateType, }); + + Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); + Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); return {reportName: newName}; } + Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); + Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); return null; } @@ -185,11 +264,21 @@ function computeReportNameIfNeeded(report: Report, incomingUpdate: OnyxUpdate, c * This is the main middleware function that processes optimistic data */ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: UpdateContext): OnyxUpdate[] { - console.log('morwa: updateOptimisticReportNamesFromUpdates called with', updates.length, 'updates', updates, context); + Performance.markStart(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); + Timing.start(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); + + Log.info('[OptimisticReportNames] Processing optimistic updates for report names', false, { + updatesCount: updates.length, + hasReports: Object.keys(context.allReports).length > 0, + hasPolicies: Object.keys(context.allPolicies).length > 0, + }); + const {betas, allReports} = context; // Check if the feature is enabled if (false && !Permissions.canUseAuthAutoReportTitles(betas)) { + Performance.markEnd(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); + Timing.end(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); return updates; } @@ -203,6 +292,25 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: case 'report': { const reportID = getReportIDFromKey(update.key); const report = getReportByID(reportID, allReports); + + // Special handling for new reports (SET method means new report creation) + if (!report && update.onyxMethod === Onyx.METHOD.SET) { + Log.info('[OptimisticReportNames] Detected new report creation', false, { + reportID, + updateKey: update.key, + }); + const reportNameUpdate = computeNameForNewReport(update, context); + + if (reportNameUpdate) { + additionalUpdates.push({ + key: getReportKey(reportID), + onyxMethod: Onyx.METHOD.MERGE, + value: reportNameUpdate, + }); + } + continue; // Skip the normal processing for this update + } + if (report) { affectedReports = [report]; } @@ -241,7 +349,14 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: } } - console.log('morwa: Generated', additionalUpdates.length, 'additional report name updates', additionalUpdates); + Log.info('[OptimisticReportNames] Processing completed', false, { + additionalUpdatesCount: additionalUpdates.length, + totalUpdatesReturned: updates.length + additionalUpdates.length, + }); + + Performance.markEnd(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); + Timing.end(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); + return updates.concat(additionalUpdates); } @@ -284,6 +399,6 @@ function createUpdateContext(): Promise { }); } -export {updateOptimisticReportNamesFromUpdates, computeReportNameIfNeeded, createUpdateContext, shouldComputeReportName}; +export {updateOptimisticReportNamesFromUpdates, computeReportNameIfNeeded, computeNameForNewReport, createUpdateContext, shouldComputeReportName}; export type {UpdateContext}; From 58b5f65e3fe9894ef6a6ffe0b30ce5038bef3646 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 11 Jul 2025 12:50:57 +0200 Subject: [PATCH 03/54] Add tests for Optimistic Report Names --- tests/unit/CustomFormulaTest.ts | 171 +++++++++++++ tests/unit/OptimisticReportNamesTest.ts | 310 ++++++++++++++++++++++++ 2 files changed, 481 insertions(+) create mode 100644 tests/unit/CustomFormulaTest.ts create mode 100644 tests/unit/OptimisticReportNamesTest.ts diff --git a/tests/unit/CustomFormulaTest.ts b/tests/unit/CustomFormulaTest.ts new file mode 100644 index 0000000000000..0eb5998def953 --- /dev/null +++ b/tests/unit/CustomFormulaTest.ts @@ -0,0 +1,171 @@ +import {extract, parse, compute, isFormula, FORMULA_PART_TYPES} from '@libs/CustomFormula'; +import type {FormulaContext} from '@libs/CustomFormula'; + +describe('CustomFormula', () => { + describe('extract()', () => { + test('should extract formula parts with default braces', () => { + expect(extract('{report:type} - {report:total}')).toEqual(['{report:type}', '{report:total}']); + }); + + test('should handle nested braces', () => { + expect(extract('{report:created:yyyy-MM-dd}')).toEqual(['{report:created:yyyy-MM-dd}']); + }); + + test('should handle escaped braces', () => { + expect(extract('\\{not-formula} {report:type}')).toEqual(['{report:type}']); + }); + + test('should handle empty formula', () => { + expect(extract('')).toEqual([]); + expect(extract(null as any)).toEqual([]); + expect(extract(undefined as any)).toEqual([]); + }); + + test('should handle formula without braces', () => { + expect(extract('no braces here')).toEqual([]); + }); + }); + + describe('parse()', () => { + test('should parse report formula parts', () => { + const parts = parse('{report:type} {report:startdate}'); + expect(parts).toHaveLength(2); // report:type, report:startdate (space is trimmed) + expect(parts[0]).toEqual({ + definition: '{report:type}', + type: 'report', + fieldPath: ['type'], + functions: [], + }); + expect(parts[1]).toEqual({ + definition: '{report:startdate}', + type: 'report', + fieldPath: ['startdate'], + functions: [], + }); + }); + + test('should parse field formula parts', () => { + const parts = parse('{field:custom_field}'); + expect(parts[0]).toEqual({ + definition: '{field:custom_field}', + type: 'field', + fieldPath: ['custom_field'], + functions: [], + }); + }); + + test('should parse user formula parts with functions', () => { + const parts = parse('{user:email|frontPart}'); + expect(parts[0]).toEqual({ + definition: '{user:email|frontPart}', + type: 'user', + fieldPath: ['email'], + functions: ['frontPart'], + }); + }); + + test('should handle empty formula', () => { + expect(parse('')).toEqual([]); + expect(parse(null as any)).toEqual([]); + }); + + test('should treat formula without braces as free text', () => { + const parts = parse('no braces here'); + expect(parts).toHaveLength(1); + expect(parts[0].type).toBe('freetext'); + }); + }); + + describe('compute()', () => { + const mockContext: FormulaContext = { + report: { + reportID: '123', + reportName: '', + total: -10000, // -$100.00 + currency: 'USD', + lastVisibleActionCreated: '2025-01-15T10:30:00Z', + policyID: 'policy1', + } as any, + policy: { + name: 'Test Policy', + }, + }; + + test('should compute basic report formula', () => { + const result = compute('{report:type} {report:total}', mockContext); + expect(result).toBe('Expense ReportUSD 100.00'); // No space between parts + }); + + test('should compute date formula', () => { + const result = compute('{report:startdate}', mockContext); + expect(result).toBe('01/15/2025'); + }); + + test('should compute policy name', () => { + const result = compute('{report:policyname}', mockContext); + expect(result).toBe('Test Policy'); + }); + + test('should handle empty formula', () => { + expect(compute('', mockContext)).toBe(''); + expect(compute(null as any, mockContext)).toBe(''); + }); + + test('should handle unknown formula parts', () => { + const result = compute('{report:unknown}', mockContext); + expect(result).toBe('{report:unknown}'); + }); + + test('should handle missing report data gracefully', () => { + const contextWithMissingData: FormulaContext = { + report: {} as any, + policy: null, + }; + const result = compute('{report:total} {report:policyname}', contextWithMissingData); + expect(result).toBe(''); // Empty strings concatenated = empty string + }); + + test('should preserve free text', () => { + const result = compute('Expense Report - {report:total}', mockContext); + expect(result).toBe('Expense Report - USD 100.00'); + }); + }); + + describe('isFormula()', () => { + test('should detect formulas', () => { + expect(isFormula('{report:type}')).toBe(true); + expect(isFormula('Text with {report:type} formula')).toBe(true); + }); + + test('should detect non-formulas', () => { + expect(isFormula('plain text')).toBe(false); + expect(isFormula('\\{escaped}')).toBe(false); + expect(isFormula('')).toBe(false); + }); + }); + + describe('Edge Cases', () => { + test('should handle malformed braces', () => { + const parts = parse('{incomplete'); + expect(parts[0].type).toBe('freetext'); + }); + + test('should handle invalid date', () => { + const context: FormulaContext = { + report: { lastVisibleActionCreated: 'invalid-date' } as any, + policy: null, + }; + const result = compute('{report:startdate}', context); + expect(result).toBe(''); + }); + + test('should handle undefined amounts', () => { + const context: FormulaContext = { + report: { total: undefined } as any, + policy: null, + }; + const result = compute('{report:total}', context); + expect(result).toBe(''); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/OptimisticReportNamesTest.ts b/tests/unit/OptimisticReportNamesTest.ts new file mode 100644 index 0000000000000..8393446dca1d0 --- /dev/null +++ b/tests/unit/OptimisticReportNamesTest.ts @@ -0,0 +1,310 @@ +import Onyx from 'react-native-onyx'; +import { + shouldComputeReportName, + computeNameForNewReport, + computeReportNameIfNeeded, + updateOptimisticReportNamesFromUpdates, +} from '@libs/OptimisticReportNames'; +import type {UpdateContext} from '@libs/OptimisticReportNames'; +import * as ReportUtils from '@libs/ReportUtils'; +import CONST from '@src/CONST'; + +// Mock dependencies +jest.mock('@libs/ReportUtils'); +jest.mock('@libs/Permissions'); + +const mockReportUtils = ReportUtils as jest.Mocked; + +describe('OptimisticReportNames', () => { + const mockPolicy = { + id: 'policy1', + fieldList: { + text_title: { + defaultValue: '{report:type} - {report:total}', + }, + }, + }; + + const mockReport = { + reportID: '123', + reportName: 'Old Name', + policyID: 'policy1', + total: -10000, + currency: 'USD', + lastVisibleActionCreated: '2025-01-15T10:30:00Z', + }; + + const mockContext: UpdateContext = { + betas: ['authAutoReportTitles'], + allReports: { + 'report_123': mockReport, + }, + allPolicies: { + 'policy_policy1': mockPolicy, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockReportUtils.isExpenseReport.mockReturnValue(true); + mockReportUtils.getTitleReportField.mockReturnValue(mockPolicy.fieldList.text_title); + }); + + describe('shouldComputeReportName()', () => { + test('should return true for expense report with title field formula', () => { + const result = shouldComputeReportName(mockReport as any, mockPolicy as any); + expect(result).toBe(true); + }); + + test('should return false for non-expense reports', () => { + mockReportUtils.isExpenseReport.mockReturnValue(false); + const result = shouldComputeReportName(mockReport as any, mockPolicy as any); + expect(result).toBe(false); + }); + + test('should return false when no policy', () => { + const result = shouldComputeReportName(mockReport as any, null); + expect(result).toBe(false); + }); + + test('should return false when no title field', () => { + mockReportUtils.getTitleReportField.mockReturnValue(undefined); + const result = shouldComputeReportName(mockReport as any, mockPolicy as any); + expect(result).toBe(false); + }); + + test('should return false when title field has no formula', () => { + const policyWithoutFormula = { + ...mockPolicy, + fieldList: { + text_title: { defaultValue: 'Static Title' }, + }, + }; + mockReportUtils.getTitleReportField.mockReturnValue(policyWithoutFormula.fieldList.text_title); + const result = shouldComputeReportName(mockReport as any, policyWithoutFormula as any); + expect(result).toBe(false); + }); + }); + + describe('computeNameForNewReport()', () => { + test('should compute name for new report with formula', () => { + const update = { + key: 'report_123', + onyxMethod: Onyx.METHOD.SET, + value: { + reportID: '123', + policyID: 'policy1', + total: -10000, + currency: 'USD', + lastVisibleActionCreated: '2025-01-15T10:30:00Z', + }, + }; + + const result = computeNameForNewReport(update, mockContext); + expect(result).toEqual({ reportName: 'Expense Report - USD 100.00' }); + }); + + test('should return null for report without policy', () => { + const update = { + key: 'report_123', + onyxMethod: Onyx.METHOD.SET, + value: { reportID: '123' }, + }; + + const result = computeNameForNewReport(update, mockContext); + expect(result).toBeNull(); + }); + + test('should return null when shouldComputeReportName returns false', () => { + mockReportUtils.isExpenseReport.mockReturnValue(false); + const update = { + key: 'report_123', + onyxMethod: Onyx.METHOD.SET, + value: { reportID: '123', policyID: 'policy1' }, + }; + + const result = computeNameForNewReport(update, mockContext); + expect(result).toBeNull(); + }); + + test('should return null when computed name is same as existing', () => { + const update = { + key: 'report_123', + onyxMethod: Onyx.METHOD.SET, + value: { + reportID: '123', + policyID: 'policy1', + reportName: 'Expense Report - USD 100.00', + total: -10000, + currency: 'USD', + }, + }; + + const result = computeNameForNewReport(update, mockContext); + expect(result).toBeNull(); + }); + }); + + describe('computeReportNameIfNeeded()', () => { + test('should compute name when report data changes', () => { + const update = { + key: 'report_123', + onyxMethod: Onyx.METHOD.MERGE, + value: { total: -20000 }, + }; + + const result = computeReportNameIfNeeded(mockReport as any, update, mockContext); + expect(result).toEqual({ reportName: 'Expense Report - USD 200.00' }); + }); + + test('should return null when name would not change', () => { + const update = { + key: 'report_456', + onyxMethod: Onyx.METHOD.MERGE, + value: { description: 'Updated description' }, + }; + + const result = computeReportNameIfNeeded(mockReport as any, update, mockContext); + expect(result).toBeNull(); + }); + }); + + describe('updateOptimisticReportNamesFromUpdates()', () => { + test('should detect new report creation and add name update', () => { + const updates = [ + { + key: 'report_456', + onyxMethod: Onyx.METHOD.SET, + value: { + reportID: '456', + policyID: 'policy1', + total: -15000, + currency: 'USD', + }, + }, + ]; + + const result = updateOptimisticReportNamesFromUpdates(updates, mockContext); + expect(result).toHaveLength(2); // Original + name update + expect(result[1]).toEqual({ + key: 'report_456', + onyxMethod: Onyx.METHOD.MERGE, + value: { reportName: 'Expense Report - USD 150.00' }, + }); + }); + + test('should handle existing report updates', () => { + const updates = [ + { + key: 'report_123', + onyxMethod: Onyx.METHOD.MERGE, + value: { total: -25000 }, + }, + ]; + + const result = updateOptimisticReportNamesFromUpdates(updates, mockContext); + expect(result).toHaveLength(2); // Original + name update + expect(result[1].value).toEqual({ reportName: 'Expense Report - USD 250.00' }); + }); + + test('should skip processing when no changes needed', () => { + const updates = [ + { + key: 'report_999', + onyxMethod: Onyx.METHOD.MERGE, + value: { description: 'No formula impact' }, + }, + ]; + + const result = updateOptimisticReportNamesFromUpdates(updates, mockContext); + expect(result).toEqual(updates); // Unchanged + }); + + test('should handle policy updates affecting multiple reports', () => { + const contextWithMultipleReports = { + ...mockContext, + allReports: { + 'report_123': { ...mockReport, reportID: '123' }, + 'report_456': { ...mockReport, reportID: '456' }, + }, + }; + + const updates = [ + { + key: 'policy_policy1', + onyxMethod: Onyx.METHOD.MERGE, + value: { name: 'Updated Policy Name' }, + }, + ]; + + const result = updateOptimisticReportNamesFromUpdates(updates, contextWithMultipleReports); + expect(result.length).toBeGreaterThan(1); + }); + + test('should handle unknown object types gracefully', () => { + const updates = [ + { + key: 'unknown_123', + onyxMethod: Onyx.METHOD.MERGE, + value: { someData: 'value' }, + }, + ]; + + const result = updateOptimisticReportNamesFromUpdates(updates, mockContext); + expect(result).toEqual(updates); // Unchanged + }); + }); + + describe('Edge Cases', () => { + test('should handle missing report gracefully', () => { + const update = { + key: 'report_999', + onyxMethod: Onyx.METHOD.MERGE, + value: { total: -10000 }, + }; + + const result = computeReportNameIfNeeded(null as any, update, mockContext); + expect(result).toBeNull(); + }); + + test('should handle missing policy gracefully', () => { + const contextWithoutPolicy = { + ...mockContext, + allPolicies: {}, + }; + + const result = computeNameForNewReport( + { + key: 'report_123', + onyxMethod: Onyx.METHOD.SET, + value: { reportID: '123', policyID: 'missing' }, + }, + contextWithoutPolicy, + ); + expect(result).toBeNull(); + }); + + test('should handle malformed formula gracefully', () => { + const policyWithBadFormula = { + ...mockPolicy, + fieldList: { + text_title: { defaultValue: '{invalid:formula}' }, + }, + }; + mockReportUtils.getTitleReportField.mockReturnValue(policyWithBadFormula.fieldList.text_title); + + const update = { + key: 'report_123', + onyxMethod: Onyx.METHOD.SET, + value: { reportID: '123', policyID: 'policy1' }, + }; + + const result = computeNameForNewReport(update, { + ...mockContext, + allPolicies: { 'policy_policy1': policyWithBadFormula }, + }); + // Should still return a result with the invalid formula as-is + expect(result).toBeDefined(); + }); + }); +}); \ No newline at end of file From 2810b00e39ac4495b136d0510d3fd0d8e9a063ef Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 11 Jul 2025 13:03:48 +0200 Subject: [PATCH 04/54] Introduce reassure tests --- tests/perf-test/CustomFormula.perf-test.ts | 148 ++++++++++ .../OptimisticReportNames.perf-test.ts | 273 ++++++++++++++++++ 2 files changed, 421 insertions(+) create mode 100644 tests/perf-test/CustomFormula.perf-test.ts create mode 100644 tests/perf-test/OptimisticReportNames.perf-test.ts diff --git a/tests/perf-test/CustomFormula.perf-test.ts b/tests/perf-test/CustomFormula.perf-test.ts new file mode 100644 index 0000000000000..f7f2988e5d9bc --- /dev/null +++ b/tests/perf-test/CustomFormula.perf-test.ts @@ -0,0 +1,148 @@ +import {measureFunction} from 'reassure'; +import {extract, parse, compute} from '@libs/CustomFormula'; +import type {FormulaContext} from '@libs/CustomFormula'; + +describe('[CustomFormula] Performance Tests', () => { + const mockReport = { + reportID: '123', + reportName: 'Test Report', + total: -10000, // -$100.00 + currency: 'USD', + lastVisibleActionCreated: '2025-01-15T10:30:00Z', + policyID: 'policy1', + }; + + const mockPolicy = { + name: 'Test Policy', + id: 'policy1', + }; + + const mockContext: FormulaContext = { + report: mockReport as any, + policy: mockPolicy, + }; + + describe('Formula Parsing Performance', () => { + test('[CustomFormula] extract() with simple formula', async () => { + const formula = '{report:type} - {report:total}'; + await measureFunction(() => extract(formula)); + }); + + test('[CustomFormula] extract() with complex formula', async () => { + const formula = '{report:type} - {report:startdate} - {report:total} - {report:currency} - {report:policyname} - {user:email|frontPart}'; + await measureFunction(() => extract(formula)); + }); + + test('[CustomFormula] extract() with nested braces', async () => { + const formula = '{report:created:yyyy-MM-dd} - {field:custom_field|substr:0:10} - {user:email|frontPart}'; + await measureFunction(() => extract(formula)); + }); + + test('[CustomFormula] parse() with simple formula', async () => { + const formula = '{report:type} - {report:total}'; + await measureFunction(() => parse(formula)); + }); + + test('[CustomFormula] parse() with complex formula', async () => { + const formula = '{report:type} - {report:startdate} - {report:total} - {report:currency} - {report:policyname} - {field:department} - {user:email|frontPart}'; + await measureFunction(() => parse(formula)); + }); + + test('[CustomFormula] parse() with mixed content', async () => { + const formula = 'Expense Report for {report:policyname} on {report:startdate} totaling {report:total} {report:currency} submitted by {user:email|frontPart}'; + await measureFunction(() => parse(formula)); + }); + }); + + describe('Formula Computation Performance', () => { + test('[CustomFormula] compute() with simple formula', async () => { + const formula = '{report:type} - {report:total}'; + await measureFunction(() => compute(formula, mockContext)); + }); + + test('[CustomFormula] compute() with complex formula', async () => { + const formula = '{report:type} - {report:startdate} - {report:total} - {report:currency} - {report:policyname}'; + await measureFunction(() => compute(formula, mockContext)); + }); + + test('[CustomFormula] compute() with mixed content', async () => { + const formula = 'Expense Report for {report:policyname} on {report:startdate} totaling {report:total} {report:currency}'; + await measureFunction(() => compute(formula, mockContext)); + }); + + test('[CustomFormula] compute() with missing data context', async () => { + const formula = '{report:type} - {report:total} - {report:unknown} - {report:policyname}'; + const contextWithMissingData: FormulaContext = { + report: {} as any, + policy: null, + }; + await measureFunction(() => compute(formula, contextWithMissingData)); + }); + }); + + describe('Batch Processing Performance', () => { + test('[CustomFormula] parse() batch processing 100 formulas', async () => { + const formulas = Array.from({length: 100}, (_, i) => + `{report:type} ${i} - {report:startdate} - {report:total} - {report:currency}` + ); + + await measureFunction(() => { + formulas.forEach(formula => parse(formula)); + }); + }); + + test('[CustomFormula] compute() batch processing 100 computations', async () => { + const formulas = Array.from({length: 100}, (_, i) => + `{report:type} ${i} - {report:total} - {report:policyname}` + ); + + await measureFunction(() => { + formulas.forEach(formula => compute(formula, mockContext)); + }); + }); + + test('[CustomFormula] end-to-end batch processing (parse + compute)', async () => { + const formulas = Array.from({length: 50}, (_, i) => + `Expense ${i}: {report:type} - {report:startdate} - {report:total} {report:currency} for {report:policyname}` + ); + + await measureFunction(() => { + formulas.forEach(formula => { + const parts = parse(formula); + const result = compute(formula, mockContext); + return {parts, result}; + }); + }); + }); + }); + + describe('Edge Cases Performance', () => { + test('[CustomFormula] large formula with many parts', async () => { + const largeParts = Array.from({length: 20}, (_, i) => `{report:field${i}}`).join(' - '); + const formula = `Large Formula: ${largeParts}`; + + await measureFunction(() => { + parse(formula); + compute(formula, mockContext); + }); + }); + + test('[CustomFormula] deeply nested functions', async () => { + const formula = '{user:email|frontPart} - {field:description|substr:0:20} - {report:created:yyyy-MM-dd}'; + + await measureFunction(() => { + parse(formula); + compute(formula, mockContext); + }); + }); + + test('[CustomFormula] formula with escaped braces', async () => { + const formula = '\\{not-formula} {report:type} \\{escaped} {report:total} \\{more-escapes}'; + + await measureFunction(() => { + parse(formula); + compute(formula, mockContext); + }); + }); + }); +}); \ No newline at end of file diff --git a/tests/perf-test/OptimisticReportNames.perf-test.ts b/tests/perf-test/OptimisticReportNames.perf-test.ts new file mode 100644 index 0000000000000..101ffb1687c16 --- /dev/null +++ b/tests/perf-test/OptimisticReportNames.perf-test.ts @@ -0,0 +1,273 @@ +import Onyx from 'react-native-onyx'; +import {measureFunction} from 'reassure'; +import { + updateOptimisticReportNamesFromUpdates, + computeNameForNewReport, + computeReportNameIfNeeded, +} from '@libs/OptimisticReportNames'; +import type {UpdateContext} from '@libs/OptimisticReportNames'; +import * as ReportUtils from '@libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type Report from '@src/types/onyx/Report'; +import createCollection from '../utils/collections/createCollection'; +import {createRandomReport} from '../utils/collections/reports'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +// Mock dependencies +jest.mock('@libs/ReportUtils'); +jest.mock('@libs/Permissions'); +jest.mock('@libs/Performance', () => ({ + markStart: jest.fn(), + markEnd: jest.fn(), +})); +jest.mock('@libs/actions/Timing', () => ({ + start: jest.fn(), + end: jest.fn(), +})); +jest.mock('@libs/Log', () => ({ + info: jest.fn(), +})); + +const mockReportUtils = ReportUtils as jest.Mocked; + +describe('[OptimisticReportNames] Performance Tests', () => { + const REPORTS_COUNT = 1000; + const POLICIES_COUNT = 100; + + const mockPolicy = { + id: 'policy1', + name: 'Test Policy', + fieldList: { + text_title: { + defaultValue: '{report:type} - {report:startdate} - {report:total} {report:currency}', + }, + }, + }; + + const mockPolicies = createCollection( + (item) => `policy_${item.id}`, + (index) => ({ + ...mockPolicy, + id: `policy${index}`, + name: `Policy ${index}`, + }), + POLICIES_COUNT, + ); + + const mockReports = createCollection( + (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, + (index) => ({ + ...createRandomReport(index), + policyID: `policy${index % POLICIES_COUNT}`, + total: -(Math.random() * 100000), // Random negative amount + currency: 'USD', + lastVisibleActionCreated: new Date().toISOString(), + }), + REPORTS_COUNT, + ); + + const mockContext: UpdateContext = { + betas: ['authAutoReportTitles'], + allReports: mockReports, + allPolicies: mockPolicies, + }; + + beforeAll(async () => { + await Onyx.init({keys: ONYXKEYS}); + mockReportUtils.isExpenseReport.mockReturnValue(true); + mockReportUtils.getTitleReportField.mockReturnValue(mockPolicy.fieldList.text_title); + await waitForBatchedUpdates(); + }); + + afterAll(() => { + Onyx.clear(); + }); + + describe('Single Report Name Computation', () => { + test('[OptimisticReportNames] computeNameForNewReport() single report', async () => { + const update = { + key: 'report_123', + onyxMethod: Onyx.METHOD.SET, + value: { + reportID: '123', + policyID: 'policy1', + total: -10000, + currency: 'USD', + lastVisibleActionCreated: new Date().toISOString(), + }, + }; + + await measureFunction(() => computeNameForNewReport(update, mockContext)); + }); + + test('[OptimisticReportNames] computeReportNameIfNeeded() single report', async () => { + const report = Object.values(mockReports)[0]; + const update = { + key: `report_${report.reportID}`, + onyxMethod: Onyx.METHOD.MERGE, + value: { total: -20000 }, + }; + + await measureFunction(() => computeReportNameIfNeeded(report, update, mockContext)); + }); + }); + + describe('Batch Processing Performance', () => { + test('[OptimisticReportNames] updateOptimisticReportNamesFromUpdates() with 10 new reports', async () => { + const updates = Array.from({length: 10}, (_, i) => ({ + key: `report_new${i}`, + onyxMethod: Onyx.METHOD.SET, + value: { + reportID: `new${i}`, + policyID: `policy${i % 10}`, + total: -(Math.random() * 50000), + currency: 'USD', + lastVisibleActionCreated: new Date().toISOString(), + }, + })); + + await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, mockContext)); + }); + + test('[OptimisticReportNames] updateOptimisticReportNamesFromUpdates() with 50 existing report updates', async () => { + const reportKeys = Object.keys(mockReports).slice(0, 50); + const updates = reportKeys.map((key, i) => ({ + key, + onyxMethod: Onyx.METHOD.MERGE, + value: { total: -(Math.random() * 100000) }, + })); + + await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, mockContext)); + }); + + test('[OptimisticReportNames] updateOptimisticReportNamesFromUpdates() with 100 mixed updates', async () => { + const newReportUpdates = Array.from({length: 50}, (_, i) => ({ + key: `report_batch${i}`, + onyxMethod: Onyx.METHOD.SET, + value: { + reportID: `batch${i}`, + policyID: `policy${i % 20}`, + total: -(Math.random() * 75000), + currency: 'USD', + lastVisibleActionCreated: new Date().toISOString(), + }, + })); + + const existingReportUpdates = Object.keys(mockReports).slice(0, 50).map((key) => ({ + key, + onyxMethod: Onyx.METHOD.MERGE, + value: { total: -(Math.random() * 125000) }, + })); + + const allUpdates = [...newReportUpdates, ...existingReportUpdates]; + + await measureFunction(() => updateOptimisticReportNamesFromUpdates(allUpdates, mockContext)); + }); + }); + + describe('Policy Update Impact Performance', () => { + test('[OptimisticReportNames] policy update affecting multiple reports', async () => { + const policyUpdate = { + key: 'policy_policy1', + onyxMethod: Onyx.METHOD.MERGE, + value: { name: 'Updated Policy Name' }, + }; + + // This should trigger name computation for all reports using policy1 + await measureFunction(() => updateOptimisticReportNamesFromUpdates([policyUpdate], mockContext)); + }); + + test('[OptimisticReportNames] multiple policy updates', async () => { + const policyUpdates = Array.from({length: 10}, (_, i) => ({ + key: `policy_policy${i}`, + onyxMethod: Onyx.METHOD.MERGE, + value: { name: `Bulk Updated Policy ${i}` }, + })); + + await measureFunction(() => updateOptimisticReportNamesFromUpdates(policyUpdates, mockContext)); + }); + }); + + describe('Large Dataset Performance', () => { + test('[OptimisticReportNames] processing with large context (1000 reports)', async () => { + const updates = Array.from({length: 20}, (_, i) => ({ + key: `report_large${i}`, + onyxMethod: Onyx.METHOD.SET, + value: { + reportID: `large${i}`, + policyID: `policy${i % 50}`, + total: -(Math.random() * 200000), + currency: 'USD', + lastVisibleActionCreated: new Date().toISOString(), + }, + })); + + await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, mockContext)); + }); + + test('[OptimisticReportNames] worst case: many irrelevant updates', async () => { + // Create updates that won't trigger name computation to test filtering performance + const irrelevantUpdates = Array.from({length: 100}, (_, i) => ({ + key: `transaction_${i}`, + onyxMethod: Onyx.METHOD.MERGE, + value: { description: `Updated transaction ${i}` }, + })); + + await measureFunction(() => updateOptimisticReportNamesFromUpdates(irrelevantUpdates, mockContext)); + }); + }); + + describe('Edge Cases Performance', () => { + test('[OptimisticReportNames] reports without formulas', async () => { + // Mock reports with policies that don't have formulas + const contextWithoutFormulas: UpdateContext = { + ...mockContext, + allPolicies: createCollection( + (item) => `policy_${item.id}`, + (index) => ({ + id: `policy${index}`, + name: `Policy ${index}`, + fieldList: { + text_title: { defaultValue: 'Static Title' }, // No formula + }, + }), + 50, + ), + }; + + const updates = Array.from({length: 20}, (_, i) => ({ + key: `report_static${i}`, + onyxMethod: Onyx.METHOD.SET, + value: { + reportID: `static${i}`, + policyID: `policy${i % 10}`, + total: -10000, + currency: 'USD', + }, + })); + + await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, contextWithoutFormulas)); + }); + + test('[OptimisticReportNames] missing policies and reports', async () => { + const contextWithMissingData: UpdateContext = { + betas: ['authAutoReportTitles'], + allReports: {}, + allPolicies: {}, + }; + + const updates = Array.from({length: 10}, (_, i) => ({ + key: `report_missing${i}`, + onyxMethod: Onyx.METHOD.SET, + value: { + reportID: `missing${i}`, + policyID: 'nonexistent', + total: -10000, + currency: 'USD', + }, + })); + + await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, contextWithMissingData)); + }); + }); +}); \ No newline at end of file From 0d66c9c6f1906c79245d40db6346e9cf806556d3 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 31 Jul 2025 14:19:44 +0200 Subject: [PATCH 05/54] fix: imports --- src/libs/OptimisticReportNames.ts | 51 +++++++++++++++++-------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 5cf6f9b3540b4..59640a54f39c5 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -1,17 +1,16 @@ import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import * as CustomFormula from '@libs/CustomFormula'; -import Log from '@libs/Log'; -import * as Performance from '@libs/Performance'; -import Permissions from '@libs/Permissions'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as Timing from '@libs/actions/Timing'; +import Timing from '@libs/actions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type Beta from '@src/types/onyx/Beta'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; +import * as CustomFormula from './CustomFormula'; +import Log from './Log'; +import Performance from './Performance'; +import Permissions from './Permissions'; +import * as ReportUtils from './ReportUtils'; type UpdateContext = { betas: OnyxEntry; @@ -125,14 +124,14 @@ function shouldComputeReportName(report: Report, policy: Policy | undefined): bo function computeNameForNewReport(update: OnyxUpdate, context: UpdateContext): {reportName: string} | null { Performance.markStart(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); Timing.start(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); - + Log.info('[OptimisticReportNames] Computing name for new report', false, { updateKey: update.key, reportID: (update.value as Report)?.reportID, }); - + const {allPolicies} = context; - + // Extract the new report data from the update const newReport = update.value as Report; if (!newReport?.policyID) { @@ -140,29 +139,29 @@ function computeNameForNewReport(update: OnyxUpdate, context: UpdateContext): {r Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); return null; } - + const policy = getPolicyByID(newReport.policyID, allPolicies); if (!shouldComputeReportName(newReport, policy)) { Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); return null; } - + const titleField = ReportUtils.getTitleReportField(policy?.fieldList ?? {}); if (!titleField?.defaultValue) { Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); return null; } - + // Build context for formula computation const formulaContext: CustomFormula.FormulaContext = { report: newReport, policy, }; - + const newName = CustomFormula.compute(titleField.defaultValue, formulaContext); - + if (newName && newName !== newReport.reportName) { Log.info('[OptimisticReportNames] New report name computed successfully', false, { reportID: newReport.reportID, @@ -170,12 +169,12 @@ function computeNameForNewReport(update: OnyxUpdate, context: UpdateContext): {r newName, formula: titleField.defaultValue, }); - + Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); return {reportName: newName}; } - + Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); return null; @@ -184,7 +183,13 @@ function computeNameForNewReport(update: OnyxUpdate, context: UpdateContext): {r /** * Compute a new report name if needed based on an optimistic update */ -function computeReportNameIfNeeded(report: Report, incomingUpdate: OnyxUpdate, context: UpdateContext): {reportName: string} | null { +function computeReportNameIfNeeded( + report: Report, + incomingUpdate: OnyxUpdate, + context: UpdateContext, +): { + reportName: string; +} | null { Performance.markStart(CONST.TIMING.COMPUTE_REPORT_NAME); Timing.start(CONST.TIMING.COMPUTE_REPORT_NAME); @@ -248,7 +253,7 @@ function computeReportNameIfNeeded(report: Report, incomingUpdate: OnyxUpdate, c formula, updateType, }); - + Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); return {reportName: newName}; @@ -292,7 +297,7 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: case 'report': { const reportID = getReportIDFromKey(update.key); const report = getReportByID(reportID, allReports); - + // Special handling for new reports (SET method means new report creation) if (!report && update.onyxMethod === Onyx.METHOD.SET) { Log.info('[OptimisticReportNames] Detected new report creation', false, { @@ -300,7 +305,7 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: updateKey: update.key, }); const reportNameUpdate = computeNameForNewReport(update, context); - + if (reportNameUpdate) { additionalUpdates.push({ key: getReportKey(reportID), @@ -310,7 +315,7 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: } continue; // Skip the normal processing for this update } - + if (report) { affectedReports = [report]; } @@ -356,7 +361,7 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: Performance.markEnd(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); Timing.end(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); - + return updates.concat(additionalUpdates); } From 33b27a1a2a2688644c4db0996bbcb33e75dcdc6d Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 31 Jul 2025 14:25:03 +0200 Subject: [PATCH 06/54] chore: run prettier --- src/libs/API/index.ts | 7 +-- src/libs/CustomFormula.ts | 39 ++++++-------- tests/perf-test/CustomFormula.perf-test.ts | 34 +++++------- .../OptimisticReportNames.perf-test.ts | 32 ++++++----- tests/unit/CustomFormulaTest.ts | 8 +-- tests/unit/OptimisticReportNamesTest.ts | 53 +++++++++---------- 6 files changed, 74 insertions(+), 99 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 71647f4751b0f..41f81cfc6441f 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -75,14 +75,11 @@ function prepareRequest( const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData; if (optimisticData && shouldApplyOptimisticData) { Log.info('[API] Applying optimistic data', false, {command, type}); - + // Process optimistic data through report name middleware OptimisticReportNames.createUpdateContext() .then((context) => { - const processedOptimisticData = OptimisticReportNames.updateOptimisticReportNamesFromUpdates( - optimisticData, - context, - ); + const processedOptimisticData = OptimisticReportNames.updateOptimisticReportNamesFromUpdates(optimisticData, context); Onyx.update(processedOptimisticData); }) .catch((error) => { diff --git a/src/libs/CustomFormula.ts b/src/libs/CustomFormula.ts index 7b584b5c041ee..d4c8297ddcea8 100644 --- a/src/libs/CustomFormula.ts +++ b/src/libs/CustomFormula.ts @@ -98,7 +98,7 @@ function parse(formula: string): FormulaPart[] { formulaParts.forEach((part) => { const partIndex = remainingFormula.indexOf(part, lastIndex); - + // Add any free text before this formula part if (partIndex > lastIndex) { const freeText = remainingFormula.substring(lastIndex, partIndex); @@ -275,7 +275,7 @@ function applyFunctions(value: string, functions: string[]): string { for (const func of functions) { const [functionName, ...args] = func.split(':'); - + switch (functionName.toLowerCase()) { case 'frontpart': result = getFrontPart(result); @@ -297,12 +297,12 @@ function applyFunctions(value: string, functions: string[]): string { */ function getFrontPart(value: string): string { const trimmed = value.trim(); - + // If it's an email, return the part before @ if (trimmed.includes('@')) { return trimmed.split('@')[0]; } - + // Otherwise, return the first word return trimmed.split(' ')[0]; } @@ -313,11 +313,11 @@ function getFrontPart(value: string): string { function getSubstring(value: string, args: string[]): string { const start = parseInt(args[0], 10) || 0; const length = args[1] ? parseInt(args[1], 10) : undefined; - + if (length !== undefined) { return value.substr(start, length); } - + return value.substr(start); } @@ -328,22 +328,22 @@ function formatDate(dateString: string | undefined, format = CONST.DATE.FNS_FORM if (!dateString) { return ''; } - + try { const date = new Date(dateString); if (isNaN(date.getTime())) { return ''; } - + // Simple date formatting - this could be enhanced with a proper date library const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); - + if (format === CONST.DATE.FNS_FORMAT_STRING) { return `${month}/${day}/${year}`; } - + return `${year}-${month}-${day}`; } catch { return ''; @@ -357,14 +357,14 @@ function formatAmount(amount: number | undefined, currency: string | undefined): if (amount === undefined) { return ''; } - + const absoluteAmount = Math.abs(amount); const formattedAmount = (absoluteAmount / 100).toFixed(2); - + if (currency) { return `${currency} ${formattedAmount}`; } - + return formattedAmount; } @@ -375,15 +375,6 @@ function isFormula(str: string): boolean { return extract(str).length > 0; } -export { - extract, - parse, - compute, - isFormula, - FORMULA_PART_TYPES, -}; +export {extract, parse, compute, isFormula, FORMULA_PART_TYPES}; -export type { - FormulaPart, - FormulaContext, -}; \ No newline at end of file +export type {FormulaPart, FormulaContext}; diff --git a/tests/perf-test/CustomFormula.perf-test.ts b/tests/perf-test/CustomFormula.perf-test.ts index f7f2988e5d9bc..a225759a97c81 100644 --- a/tests/perf-test/CustomFormula.perf-test.ts +++ b/tests/perf-test/CustomFormula.perf-test.ts @@ -1,5 +1,5 @@ import {measureFunction} from 'reassure'; -import {extract, parse, compute} from '@libs/CustomFormula'; +import {compute, extract, parse} from '@libs/CustomFormula'; import type {FormulaContext} from '@libs/CustomFormula'; describe('[CustomFormula] Performance Tests', () => { @@ -82,32 +82,26 @@ describe('[CustomFormula] Performance Tests', () => { describe('Batch Processing Performance', () => { test('[CustomFormula] parse() batch processing 100 formulas', async () => { - const formulas = Array.from({length: 100}, (_, i) => - `{report:type} ${i} - {report:startdate} - {report:total} - {report:currency}` - ); - + const formulas = Array.from({length: 100}, (_, i) => `{report:type} ${i} - {report:startdate} - {report:total} - {report:currency}`); + await measureFunction(() => { - formulas.forEach(formula => parse(formula)); + formulas.forEach((formula) => parse(formula)); }); }); test('[CustomFormula] compute() batch processing 100 computations', async () => { - const formulas = Array.from({length: 100}, (_, i) => - `{report:type} ${i} - {report:total} - {report:policyname}` - ); - + const formulas = Array.from({length: 100}, (_, i) => `{report:type} ${i} - {report:total} - {report:policyname}`); + await measureFunction(() => { - formulas.forEach(formula => compute(formula, mockContext)); + formulas.forEach((formula) => compute(formula, mockContext)); }); }); test('[CustomFormula] end-to-end batch processing (parse + compute)', async () => { - const formulas = Array.from({length: 50}, (_, i) => - `Expense ${i}: {report:type} - {report:startdate} - {report:total} {report:currency} for {report:policyname}` - ); - + const formulas = Array.from({length: 50}, (_, i) => `Expense ${i}: {report:type} - {report:startdate} - {report:total} {report:currency} for {report:policyname}`); + await measureFunction(() => { - formulas.forEach(formula => { + formulas.forEach((formula) => { const parts = parse(formula); const result = compute(formula, mockContext); return {parts, result}; @@ -120,7 +114,7 @@ describe('[CustomFormula] Performance Tests', () => { test('[CustomFormula] large formula with many parts', async () => { const largeParts = Array.from({length: 20}, (_, i) => `{report:field${i}}`).join(' - '); const formula = `Large Formula: ${largeParts}`; - + await measureFunction(() => { parse(formula); compute(formula, mockContext); @@ -129,7 +123,7 @@ describe('[CustomFormula] Performance Tests', () => { test('[CustomFormula] deeply nested functions', async () => { const formula = '{user:email|frontPart} - {field:description|substr:0:20} - {report:created:yyyy-MM-dd}'; - + await measureFunction(() => { parse(formula); compute(formula, mockContext); @@ -138,11 +132,11 @@ describe('[CustomFormula] Performance Tests', () => { test('[CustomFormula] formula with escaped braces', async () => { const formula = '\\{not-formula} {report:type} \\{escaped} {report:total} \\{more-escapes}'; - + await measureFunction(() => { parse(formula); compute(formula, mockContext); }); }); }); -}); \ No newline at end of file +}); diff --git a/tests/perf-test/OptimisticReportNames.perf-test.ts b/tests/perf-test/OptimisticReportNames.perf-test.ts index 101ffb1687c16..f26184a00f553 100644 --- a/tests/perf-test/OptimisticReportNames.perf-test.ts +++ b/tests/perf-test/OptimisticReportNames.perf-test.ts @@ -1,10 +1,6 @@ import Onyx from 'react-native-onyx'; import {measureFunction} from 'reassure'; -import { - updateOptimisticReportNamesFromUpdates, - computeNameForNewReport, - computeReportNameIfNeeded, -} from '@libs/OptimisticReportNames'; +import {computeNameForNewReport, computeReportNameIfNeeded, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames'; import type {UpdateContext} from '@libs/OptimisticReportNames'; import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -105,7 +101,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { const update = { key: `report_${report.reportID}`, onyxMethod: Onyx.METHOD.MERGE, - value: { total: -20000 }, + value: {total: -20000}, }; await measureFunction(() => computeReportNameIfNeeded(report, update, mockContext)); @@ -134,7 +130,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { const updates = reportKeys.map((key, i) => ({ key, onyxMethod: Onyx.METHOD.MERGE, - value: { total: -(Math.random() * 100000) }, + value: {total: -(Math.random() * 100000)}, })); await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, mockContext)); @@ -153,11 +149,13 @@ describe('[OptimisticReportNames] Performance Tests', () => { }, })); - const existingReportUpdates = Object.keys(mockReports).slice(0, 50).map((key) => ({ - key, - onyxMethod: Onyx.METHOD.MERGE, - value: { total: -(Math.random() * 125000) }, - })); + const existingReportUpdates = Object.keys(mockReports) + .slice(0, 50) + .map((key) => ({ + key, + onyxMethod: Onyx.METHOD.MERGE, + value: {total: -(Math.random() * 125000)}, + })); const allUpdates = [...newReportUpdates, ...existingReportUpdates]; @@ -170,7 +168,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { const policyUpdate = { key: 'policy_policy1', onyxMethod: Onyx.METHOD.MERGE, - value: { name: 'Updated Policy Name' }, + value: {name: 'Updated Policy Name'}, }; // This should trigger name computation for all reports using policy1 @@ -181,7 +179,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { const policyUpdates = Array.from({length: 10}, (_, i) => ({ key: `policy_policy${i}`, onyxMethod: Onyx.METHOD.MERGE, - value: { name: `Bulk Updated Policy ${i}` }, + value: {name: `Bulk Updated Policy ${i}`}, })); await measureFunction(() => updateOptimisticReportNamesFromUpdates(policyUpdates, mockContext)); @@ -210,7 +208,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { const irrelevantUpdates = Array.from({length: 100}, (_, i) => ({ key: `transaction_${i}`, onyxMethod: Onyx.METHOD.MERGE, - value: { description: `Updated transaction ${i}` }, + value: {description: `Updated transaction ${i}`}, })); await measureFunction(() => updateOptimisticReportNamesFromUpdates(irrelevantUpdates, mockContext)); @@ -228,7 +226,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { id: `policy${index}`, name: `Policy ${index}`, fieldList: { - text_title: { defaultValue: 'Static Title' }, // No formula + text_title: {defaultValue: 'Static Title'}, // No formula }, }), 50, @@ -270,4 +268,4 @@ describe('[OptimisticReportNames] Performance Tests', () => { await measureFunction(() => updateOptimisticReportNamesFromUpdates(updates, contextWithMissingData)); }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/CustomFormulaTest.ts b/tests/unit/CustomFormulaTest.ts index 0eb5998def953..59bb3b5525faf 100644 --- a/tests/unit/CustomFormulaTest.ts +++ b/tests/unit/CustomFormulaTest.ts @@ -1,4 +1,4 @@ -import {extract, parse, compute, isFormula, FORMULA_PART_TYPES} from '@libs/CustomFormula'; +import {compute, extract, FORMULA_PART_TYPES, isFormula, parse} from '@libs/CustomFormula'; import type {FormulaContext} from '@libs/CustomFormula'; describe('CustomFormula', () => { @@ -152,7 +152,7 @@ describe('CustomFormula', () => { test('should handle invalid date', () => { const context: FormulaContext = { - report: { lastVisibleActionCreated: 'invalid-date' } as any, + report: {lastVisibleActionCreated: 'invalid-date'} as any, policy: null, }; const result = compute('{report:startdate}', context); @@ -161,11 +161,11 @@ describe('CustomFormula', () => { test('should handle undefined amounts', () => { const context: FormulaContext = { - report: { total: undefined } as any, + report: {total: undefined} as any, policy: null, }; const result = compute('{report:total}', context); expect(result).toBe(''); }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/OptimisticReportNamesTest.ts b/tests/unit/OptimisticReportNamesTest.ts index 8393446dca1d0..ed296f2183139 100644 --- a/tests/unit/OptimisticReportNamesTest.ts +++ b/tests/unit/OptimisticReportNamesTest.ts @@ -1,10 +1,5 @@ import Onyx from 'react-native-onyx'; -import { - shouldComputeReportName, - computeNameForNewReport, - computeReportNameIfNeeded, - updateOptimisticReportNamesFromUpdates, -} from '@libs/OptimisticReportNames'; +import {computeNameForNewReport, computeReportNameIfNeeded, shouldComputeReportName, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames'; import type {UpdateContext} from '@libs/OptimisticReportNames'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; @@ -37,10 +32,10 @@ describe('OptimisticReportNames', () => { const mockContext: UpdateContext = { betas: ['authAutoReportTitles'], allReports: { - 'report_123': mockReport, + report_123: mockReport, }, allPolicies: { - 'policy_policy1': mockPolicy, + policy_policy1: mockPolicy, }, }; @@ -77,7 +72,7 @@ describe('OptimisticReportNames', () => { const policyWithoutFormula = { ...mockPolicy, fieldList: { - text_title: { defaultValue: 'Static Title' }, + text_title: {defaultValue: 'Static Title'}, }, }; mockReportUtils.getTitleReportField.mockReturnValue(policyWithoutFormula.fieldList.text_title); @@ -101,14 +96,14 @@ describe('OptimisticReportNames', () => { }; const result = computeNameForNewReport(update, mockContext); - expect(result).toEqual({ reportName: 'Expense Report - USD 100.00' }); + expect(result).toEqual({reportName: 'Expense Report - USD 100.00'}); }); test('should return null for report without policy', () => { const update = { key: 'report_123', onyxMethod: Onyx.METHOD.SET, - value: { reportID: '123' }, + value: {reportID: '123'}, }; const result = computeNameForNewReport(update, mockContext); @@ -120,7 +115,7 @@ describe('OptimisticReportNames', () => { const update = { key: 'report_123', onyxMethod: Onyx.METHOD.SET, - value: { reportID: '123', policyID: 'policy1' }, + value: {reportID: '123', policyID: 'policy1'}, }; const result = computeNameForNewReport(update, mockContext); @@ -150,18 +145,18 @@ describe('OptimisticReportNames', () => { const update = { key: 'report_123', onyxMethod: Onyx.METHOD.MERGE, - value: { total: -20000 }, + value: {total: -20000}, }; const result = computeReportNameIfNeeded(mockReport as any, update, mockContext); - expect(result).toEqual({ reportName: 'Expense Report - USD 200.00' }); + expect(result).toEqual({reportName: 'Expense Report - USD 200.00'}); }); test('should return null when name would not change', () => { const update = { key: 'report_456', onyxMethod: Onyx.METHOD.MERGE, - value: { description: 'Updated description' }, + value: {description: 'Updated description'}, }; const result = computeReportNameIfNeeded(mockReport as any, update, mockContext); @@ -189,7 +184,7 @@ describe('OptimisticReportNames', () => { expect(result[1]).toEqual({ key: 'report_456', onyxMethod: Onyx.METHOD.MERGE, - value: { reportName: 'Expense Report - USD 150.00' }, + value: {reportName: 'Expense Report - USD 150.00'}, }); }); @@ -198,13 +193,13 @@ describe('OptimisticReportNames', () => { { key: 'report_123', onyxMethod: Onyx.METHOD.MERGE, - value: { total: -25000 }, + value: {total: -25000}, }, ]; const result = updateOptimisticReportNamesFromUpdates(updates, mockContext); expect(result).toHaveLength(2); // Original + name update - expect(result[1].value).toEqual({ reportName: 'Expense Report - USD 250.00' }); + expect(result[1].value).toEqual({reportName: 'Expense Report - USD 250.00'}); }); test('should skip processing when no changes needed', () => { @@ -212,7 +207,7 @@ describe('OptimisticReportNames', () => { { key: 'report_999', onyxMethod: Onyx.METHOD.MERGE, - value: { description: 'No formula impact' }, + value: {description: 'No formula impact'}, }, ]; @@ -224,8 +219,8 @@ describe('OptimisticReportNames', () => { const contextWithMultipleReports = { ...mockContext, allReports: { - 'report_123': { ...mockReport, reportID: '123' }, - 'report_456': { ...mockReport, reportID: '456' }, + report_123: {...mockReport, reportID: '123'}, + report_456: {...mockReport, reportID: '456'}, }, }; @@ -233,7 +228,7 @@ describe('OptimisticReportNames', () => { { key: 'policy_policy1', onyxMethod: Onyx.METHOD.MERGE, - value: { name: 'Updated Policy Name' }, + value: {name: 'Updated Policy Name'}, }, ]; @@ -246,7 +241,7 @@ describe('OptimisticReportNames', () => { { key: 'unknown_123', onyxMethod: Onyx.METHOD.MERGE, - value: { someData: 'value' }, + value: {someData: 'value'}, }, ]; @@ -260,7 +255,7 @@ describe('OptimisticReportNames', () => { const update = { key: 'report_999', onyxMethod: Onyx.METHOD.MERGE, - value: { total: -10000 }, + value: {total: -10000}, }; const result = computeReportNameIfNeeded(null as any, update, mockContext); @@ -277,7 +272,7 @@ describe('OptimisticReportNames', () => { { key: 'report_123', onyxMethod: Onyx.METHOD.SET, - value: { reportID: '123', policyID: 'missing' }, + value: {reportID: '123', policyID: 'missing'}, }, contextWithoutPolicy, ); @@ -288,7 +283,7 @@ describe('OptimisticReportNames', () => { const policyWithBadFormula = { ...mockPolicy, fieldList: { - text_title: { defaultValue: '{invalid:formula}' }, + text_title: {defaultValue: '{invalid:formula}'}, }, }; mockReportUtils.getTitleReportField.mockReturnValue(policyWithBadFormula.fieldList.text_title); @@ -296,15 +291,15 @@ describe('OptimisticReportNames', () => { const update = { key: 'report_123', onyxMethod: Onyx.METHOD.SET, - value: { reportID: '123', policyID: 'policy1' }, + value: {reportID: '123', policyID: 'policy1'}, }; const result = computeNameForNewReport(update, { ...mockContext, - allPolicies: { 'policy_policy1': policyWithBadFormula }, + allPolicies: {policy_policy1: policyWithBadFormula}, }); // Should still return a result with the invalid formula as-is expect(result).toBeDefined(); }); }); -}); \ No newline at end of file +}); From 27b9b416f454e6f490e306aa4a3c15fb3a0b698c Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 31 Jul 2025 15:34:57 +0200 Subject: [PATCH 07/54] fix: do not show unsupported fields, add spaces between parts --- src/libs/CustomFormula.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/libs/CustomFormula.ts b/src/libs/CustomFormula.ts index d4c8297ddcea8..8a07c839414ef 100644 --- a/src/libs/CustomFormula.ts +++ b/src/libs/CustomFormula.ts @@ -1,4 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; +import {getCurrencySymbol} from '@libs/CurrencyUtils'; import CONST from '@src/CONST'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; @@ -206,7 +207,7 @@ function compute(formula: string, context: FormulaContext): string { value = computeUserPart(part, context); break; case FORMULA_PART_TYPES.FREETEXT: - value = part.definition; + value = part.definition.trim(); break; default: // If we don't recognize the part type, use the original definition @@ -215,7 +216,7 @@ function compute(formula: string, context: FormulaContext): string { // Apply any functions to the computed value value = applyFunctions(value, part.functions); - result += value; + result = result === '' ? value : `${result} ${value}`; // Concatenate with space } return result; @@ -238,7 +239,7 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { case 'startdate': return formatDate(report.lastVisibleActionCreated); case 'total': - return formatAmount(report.total, report.currency); + return formatAmount(report.total, getCurrencySymbol(report.currency ?? '') ?? report.currency); case 'currency': return report.currency ?? ''; case 'policyname': @@ -247,7 +248,7 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { case 'created': return formatDate(report.lastVisibleActionCreated, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING); default: - return part.definition; + return ''; } } @@ -362,7 +363,7 @@ function formatAmount(amount: number | undefined, currency: string | undefined): const formattedAmount = (absoluteAmount / 100).toFixed(2); if (currency) { - return `${currency} ${formattedAmount}`; + return `${currency}${formattedAmount}`; } return formattedAmount; From 86ac06b2b07eec1b781c78afec571c4e38d9d18c Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 31 Jul 2025 16:29:22 +0200 Subject: [PATCH 08/54] fix: minor fix --- src/libs/CustomFormula.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/CustomFormula.ts b/src/libs/CustomFormula.ts index 8a07c839414ef..53f2416ba9c7e 100644 --- a/src/libs/CustomFormula.ts +++ b/src/libs/CustomFormula.ts @@ -216,7 +216,7 @@ function compute(formula: string, context: FormulaContext): string { // Apply any functions to the computed value value = applyFunctions(value, part.functions); - result = result === '' ? value : `${result} ${value}`; // Concatenate with space + result = result === '' ? value : `${result} ${value}`.trim(); // Concatenate with space } return result; From d7e912c346ae97c0799e572daa4f71be28685f39 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Aug 2025 09:22:39 +0200 Subject: [PATCH 09/54] fix: add freetext to cspell --- cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell.json b/cspell.json index e10037fa42eae..e889073e87e3b 100644 --- a/cspell.json +++ b/cspell.json @@ -238,6 +238,7 @@ "formatjs", "Français", "Frederico", + "freetext", "frontpart", "fullstory", "FWTV", From a16b79d000b6c45ae930f1203d889f3311399f12 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Aug 2025 09:30:32 +0200 Subject: [PATCH 10/54] chore: rename CustomFormula to Formula, add missing spaces, adderess other suggestions --- src/libs/{CustomFormula.ts => Formula.ts} | 8 ++++++-- .../{CustomFormula.perf-test.ts => Formula.perf-test.ts} | 4 ++-- tests/unit/{CustomFormulaTest.ts => FormulaTest.ts} | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) rename src/libs/{CustomFormula.ts => Formula.ts} (98%) rename tests/perf-test/{CustomFormula.perf-test.ts => Formula.perf-test.ts} (98%) rename tests/unit/{CustomFormulaTest.ts => FormulaTest.ts} (98%) diff --git a/src/libs/CustomFormula.ts b/src/libs/Formula.ts similarity index 98% rename from src/libs/CustomFormula.ts rename to src/libs/Formula.ts index 53f2416ba9c7e..dc134b3009da1 100644 --- a/src/libs/CustomFormula.ts +++ b/src/libs/Formula.ts @@ -1,4 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import {getCurrencySymbol} from '@libs/CurrencyUtils'; import CONST from '@src/CONST'; import type Policy from '@src/types/onyx/Policy'; @@ -7,10 +8,13 @@ import type Report from '@src/types/onyx/Report'; type FormulaPart = { /** The original definition from the formula */ definition: string; + /** The type of formula part (report, field, user, etc.) */ - type: string; + type: ValueOf; + /** The field path for accessing data (e.g., ['type'], ['startdate'], ['total']) */ fieldPath: string[]; + /** Functions to apply to the computed value (e.g., ['frontPart']) */ functions: string[]; }; @@ -248,7 +252,7 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { case 'created': return formatDate(report.lastVisibleActionCreated, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING); default: - return ''; + return part.definition; } } diff --git a/tests/perf-test/CustomFormula.perf-test.ts b/tests/perf-test/Formula.perf-test.ts similarity index 98% rename from tests/perf-test/CustomFormula.perf-test.ts rename to tests/perf-test/Formula.perf-test.ts index a225759a97c81..6379c053034c1 100644 --- a/tests/perf-test/CustomFormula.perf-test.ts +++ b/tests/perf-test/Formula.perf-test.ts @@ -1,6 +1,6 @@ import {measureFunction} from 'reassure'; -import {compute, extract, parse} from '@libs/CustomFormula'; -import type {FormulaContext} from '@libs/CustomFormula'; +import {compute, extract, parse} from '@libs/Formula'; +import type {FormulaContext} from '@libs/Formula'; describe('[CustomFormula] Performance Tests', () => { const mockReport = { diff --git a/tests/unit/CustomFormulaTest.ts b/tests/unit/FormulaTest.ts similarity index 98% rename from tests/unit/CustomFormulaTest.ts rename to tests/unit/FormulaTest.ts index 59bb3b5525faf..0b1cf6f587a92 100644 --- a/tests/unit/CustomFormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -1,5 +1,5 @@ -import {compute, extract, FORMULA_PART_TYPES, isFormula, parse} from '@libs/CustomFormula'; -import type {FormulaContext} from '@libs/CustomFormula'; +import {compute, extract, FORMULA_PART_TYPES, isFormula, parse} from '@libs/Formula'; +import type {FormulaContext} from '@libs/Formula'; describe('CustomFormula', () => { describe('extract()', () => { From 04755c6ec71d5c1eca9477ab4195cba8af51c9d3 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Aug 2025 10:18:37 +0200 Subject: [PATCH 11/54] fix: imports --- src/libs/Formula.ts | 2 +- src/libs/OptimisticReportNames.ts | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index dc134b3009da1..ed9f34ce8f8f1 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -1,9 +1,9 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import {getCurrencySymbol} from '@libs/CurrencyUtils'; import CONST from '@src/CONST'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; +import {getCurrencySymbol} from './CurrencyUtils'; type FormulaPart = { /** The original definition from the formula */ diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 59640a54f39c5..21836a5029c4c 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -1,12 +1,12 @@ import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import Timing from '@libs/actions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type Beta from '@src/types/onyx/Beta'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; -import * as CustomFormula from './CustomFormula'; +import Timing from './actions/Timing'; +import * as Formula from './Formula'; import Log from './Log'; import Performance from './Performance'; import Permissions from './Permissions'; @@ -114,7 +114,7 @@ function shouldComputeReportName(report: Report, policy: Policy | undefined): bo } // Check if the formula contains formula parts - return CustomFormula.isFormula(titleField.defaultValue); + return Formula.isFormula(titleField.defaultValue); } /** @@ -155,12 +155,12 @@ function computeNameForNewReport(update: OnyxUpdate, context: UpdateContext): {r } // Build context for formula computation - const formulaContext: CustomFormula.FormulaContext = { + const formulaContext: Formula.FormulaContext = { report: newReport, policy, }; - const newName = CustomFormula.compute(titleField.defaultValue, formulaContext); + const newName = Formula.compute(titleField.defaultValue, formulaContext); if (newName && newName !== newReport.reportName) { Log.info('[OptimisticReportNames] New report name computed successfully', false, { @@ -212,14 +212,14 @@ function computeReportNameIfNeeded( // Quick check: see if the update might affect the report name const updateType = determineObjectTypeByKey(incomingUpdate.key); const formula = titleField.defaultValue; - const formulaParts = CustomFormula.parse(formula); + const formulaParts = Formula.parse(formula); // Check if any formula part might be affected by this update const isAffected = formulaParts.some((part) => { - if (part.type === CustomFormula.FORMULA_PART_TYPES.REPORT) { + if (part.type === Formula.FORMULA_PART_TYPES.REPORT) { return updateType === 'report' || updateType === 'transaction'; } - if (part.type === CustomFormula.FORMULA_PART_TYPES.FIELD) { + if (part.type === Formula.FORMULA_PART_TYPES.FIELD) { return updateType === 'report'; } return false; @@ -237,12 +237,12 @@ function computeReportNameIfNeeded( const updatedPolicy = updateType === 'policy' && report.policyID === getPolicyIDFromKey(incomingUpdate.key) ? {...policy, ...incomingUpdate.value} : policy; // Compute the new name - const formulaContext: CustomFormula.FormulaContext = { + const formulaContext: Formula.FormulaContext = { report: updatedReport, policy: updatedPolicy, }; - const newName = CustomFormula.compute(formula, formulaContext); + const newName = Formula.compute(formula, formulaContext); // Only return an update if the name actually changed if (newName && newName !== report.reportName) { @@ -281,6 +281,7 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: const {betas, allReports} = context; // Check if the feature is enabled + // TODO: change this condition later (implemented only for testing purposes) if (false && !Permissions.canUseAuthAutoReportTitles(betas)) { Performance.markEnd(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); Timing.end(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); From 03f09be4300de3c3ca1938841521002ea406f0e4 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Aug 2025 11:27:58 +0200 Subject: [PATCH 12/54] fix: date formatting --- src/libs/Formula.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index ed9f34ce8f8f1..413eca1f71597 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -250,7 +250,7 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { case 'workspacename': return policy?.name ?? ''; case 'created': - return formatDate(report.lastVisibleActionCreated, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING); + return formatDate(report.lastVisibleActionCreated, CONST.DATE.FNS_FORMAT_STRING); default: return part.definition; } From 0b4681f046222559791a413b6b61bc14bea848e9 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Aug 2025 11:41:43 +0200 Subject: [PATCH 13/54] fix: revert adding spaces --- src/libs/Formula.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 413eca1f71597..a58e9a3396659 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -211,7 +211,7 @@ function compute(formula: string, context: FormulaContext): string { value = computeUserPart(part, context); break; case FORMULA_PART_TYPES.FREETEXT: - value = part.definition.trim(); + value = part.definition; break; default: // If we don't recognize the part type, use the original definition @@ -220,7 +220,7 @@ function compute(formula: string, context: FormulaContext): string { // Apply any functions to the computed value value = applyFunctions(value, part.functions); - result = result === '' ? value : `${result} ${value}`.trim(); // Concatenate with space + result += value; } return result; From 9ec9e173b0860a89c19397b1f5a5f450462e489c Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 4 Aug 2025 16:18:37 +0200 Subject: [PATCH 14/54] Rename beta to match backend value --- src/CONST/index.ts | 2 +- src/libs/OptimisticReportNames.ts | 4 ++-- src/libs/Permissions.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 67a8bf1a4fe22..b8273f37ee323 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -656,7 +656,7 @@ const CONST = { BETAS: { ALL: 'all', ASAP_SUBMIT: 'asapSubmit', - AUTH_AUTO_REPORT_TITLES: 'authAutoReportTitles', + USE_CUSTOM_REPORT_NAMES: 'useCustomReportNamesNewExpensify', DEFAULT_ROOMS: 'defaultRooms', P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', SPOTNANA_TRAVEL: 'spotnanaTravel', diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 21836a5029c4c..7a47bdf98b417 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -279,10 +279,10 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: }); const {betas, allReports} = context; - + console.log('morwa Permissions.canUseCustomReportNames(betas)', Permissions.canUseCustomReportNames(betas)); // Check if the feature is enabled // TODO: change this condition later (implemented only for testing purposes) - if (false && !Permissions.canUseAuthAutoReportTitles(betas)) { + if (!Permissions.canUseCustomReportNames(betas)) { Performance.markEnd(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); Timing.end(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); return updates; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 0e970f3c0a2be..6a18963d68a6a 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -8,8 +8,8 @@ function canUseAllBetas(betas: OnyxEntry): boolean { } // eslint-disable-next-line rulesdir/no-beta-handler -function canUseAuthAutoReportTitles(betas: OnyxEntry): boolean { - return isBetaEnabled(CONST.BETAS.AUTH_AUTO_REPORT_TITLES, betas); +function canUseCustomReportNames(betas: OnyxEntry): boolean { + return isBetaEnabled(CONST.BETAS.USE_CUSTOM_REPORT_NAMES, betas); } // eslint-disable-next-line rulesdir/no-beta-handler @@ -35,7 +35,7 @@ function isBetaEnabled(beta: Beta, betas: OnyxEntry): boolean { } export default { - canUseAuthAutoReportTitles, + canUseCustomReportNames, canUseLinkPreviews, isBlockedFromSpotnanaTravel, isBetaEnabled, From e358534372cbe8fc331a28575b2da8063ee67891 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 4 Aug 2025 16:44:26 +0200 Subject: [PATCH 15/54] Add test cases for spaces around free text --- tests/unit/FormulaTest.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index 0b1cf6f587a92..56a65ce8a1f0c 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -93,7 +93,7 @@ describe('CustomFormula', () => { test('should compute basic report formula', () => { const result = compute('{report:type} {report:total}', mockContext); - expect(result).toBe('Expense ReportUSD 100.00'); // No space between parts + expect(result).toBe('Expense ReportUSD100.00'); // No space between parts }); test('should compute date formula', () => { @@ -127,7 +127,12 @@ describe('CustomFormula', () => { test('should preserve free text', () => { const result = compute('Expense Report - {report:total}', mockContext); - expect(result).toBe('Expense Report - USD 100.00'); + expect(result).toBe('Expense Report - USD100.00'); + }); + + test('should preserve exact spacing around formula parts', () => { + const result = compute('Report with type after 4 spaces {report:type}-and no space after computed part', mockContext); + expect(result).toBe('Report with type after 4 spaces Expense Report-and no space after computed part'); }); }); From b3c7c2b58c7a0899e19153b844a84581418ecfd8 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 4 Aug 2025 17:27:10 +0200 Subject: [PATCH 16/54] Get valid date for `startdate` formula part --- src/libs/Formula.ts | 25 +++++++++++++++++++++- tests/unit/FormulaTest.ts | 44 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index a58e9a3396659..57d7b8e71f043 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -4,6 +4,7 @@ import CONST from '@src/CONST'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import {getCurrencySymbol} from './CurrencyUtils'; +import {getAllReportActions, getSortedReportActions} from './ReportActionsUtils'; type FormulaPart = { /** The original definition from the formula */ @@ -241,7 +242,8 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { case 'type': return 'Expense Report'; // Default report type for now case 'startdate': - return formatDate(report.lastVisibleActionCreated); + console.log('morwa report', report); + return formatDate(getOldestReportActionDate(report.reportID)); case 'total': return formatAmount(report.total, getCurrencySymbol(report.currency ?? '') ?? report.currency); case 'currency': @@ -373,6 +375,27 @@ function formatAmount(amount: number | undefined, currency: string | undefined): return formattedAmount; } +/** + * Get the date of the oldest report action for a given report + */ +function getOldestReportActionDate(reportID: string): string | undefined { + if (!reportID) { + return undefined; + } + + const reportActions = getAllReportActions(reportID); + if (!reportActions || Object.keys(reportActions).length === 0) { + return undefined; + } + + const sortedActions = getSortedReportActions(Object.values(reportActions), false); + if (sortedActions.length === 0) { + return undefined; + } + + return sortedActions[0]?.created; +} + /** * Check if a string contains formula parts */ diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index 56a65ce8a1f0c..9817c8ba66b17 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -1,6 +1,16 @@ import {compute, extract, FORMULA_PART_TYPES, isFormula, parse} from '@libs/Formula'; import type {FormulaContext} from '@libs/Formula'; +// Mock ReportActionsUtils +jest.mock('@libs/ReportActionsUtils', () => ({ + getAllReportActions: jest.fn(), + getSortedReportActions: jest.fn(), +})); + +import * as ReportActionsUtils from '@libs/ReportActionsUtils'; + +const mockReportActionsUtils = ReportActionsUtils as jest.Mocked; + describe('CustomFormula', () => { describe('extract()', () => { test('should extract formula parts with default braces', () => { @@ -91,6 +101,30 @@ describe('CustomFormula', () => { }, }; + beforeEach(() => { + jest.clearAllMocks(); + + // Mock report actions - oldest action has earlier date + const mockReportActions = { + '1': { + reportActionID: '1', + created: '2025-01-10T08:00:00Z', // Oldest action + actionName: 'CREATED', + }, + '2': { + reportActionID: '2', + created: '2025-01-15T10:30:00Z', // Later action + actionName: 'IOU', + }, + }; + + mockReportActionsUtils.getAllReportActions.mockReturnValue(mockReportActions as any); + mockReportActionsUtils.getSortedReportActions.mockReturnValue([ + mockReportActions['1'], + mockReportActions['2'], + ] as any); + }); + test('should compute basic report formula', () => { const result = compute('{report:type} {report:total}', mockContext); expect(result).toBe('Expense ReportUSD100.00'); // No space between parts @@ -98,7 +132,7 @@ describe('CustomFormula', () => { test('should compute date formula', () => { const result = compute('{report:startdate}', mockContext); - expect(result).toBe('01/15/2025'); + expect(result).toBe('01/10/2025'); // Should use oldest report action date }); test('should compute policy name', () => { @@ -172,5 +206,13 @@ describe('CustomFormula', () => { const result = compute('{report:total}', context); expect(result).toBe(''); }); + + test('should handle missing report actions for startdate', () => { + mockReportActionsUtils.getAllReportActions.mockReturnValue({}); + mockReportActionsUtils.getSortedReportActions.mockReturnValue([]); + + const result = compute('{report:startdate}', mockContext); + expect(result).toBe(''); + }); }); }); From 4339a7db327298bc7f5115b1931a917d289fedf7 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 4 Aug 2025 17:37:56 +0200 Subject: [PATCH 17/54] More optimal way to get oldest report action --- src/libs/Formula.ts | 18 +++++++++++------- tests/unit/FormulaTest.ts | 28 +++++++++++++++------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 57d7b8e71f043..5dfba8b2c2dbe 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -4,7 +4,7 @@ import CONST from '@src/CONST'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import {getCurrencySymbol} from './CurrencyUtils'; -import {getAllReportActions, getSortedReportActions} from './ReportActionsUtils'; +import {getAllReportActions} from './ReportActionsUtils'; type FormulaPart = { /** The original definition from the formula */ @@ -242,7 +242,6 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { case 'type': return 'Expense Report'; // Default report type for now case 'startdate': - console.log('morwa report', report); return formatDate(getOldestReportActionDate(report.reportID)); case 'total': return formatAmount(report.total, getCurrencySymbol(report.currency ?? '') ?? report.currency); @@ -388,12 +387,17 @@ function getOldestReportActionDate(reportID: string): string | undefined { return undefined; } - const sortedActions = getSortedReportActions(Object.values(reportActions), false); - if (sortedActions.length === 0) { - return undefined; - } + let oldestDate: string | undefined; + + Object.values(reportActions).forEach((action) => { + if (action?.created) { + if (!oldestDate || action.created < oldestDate) { + oldestDate = action.created; + } + } + }); - return sortedActions[0]?.created; + return oldestDate; } /** diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index 9817c8ba66b17..ce9feef9e678d 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -1,14 +1,12 @@ import {compute, extract, FORMULA_PART_TYPES, isFormula, parse} from '@libs/Formula'; import type {FormulaContext} from '@libs/Formula'; +import * as ReportActionsUtils from '@libs/ReportActionsUtils'; // Mock ReportActionsUtils jest.mock('@libs/ReportActionsUtils', () => ({ getAllReportActions: jest.fn(), - getSortedReportActions: jest.fn(), })); -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; - const mockReportActionsUtils = ReportActionsUtils as jest.Mocked; describe('CustomFormula', () => { @@ -103,8 +101,8 @@ describe('CustomFormula', () => { beforeEach(() => { jest.clearAllMocks(); - - // Mock report actions - oldest action has earlier date + + // Mock report actions - test the iteration logic for finding oldest date const mockReportActions = { '1': { reportActionID: '1', @@ -112,17 +110,18 @@ describe('CustomFormula', () => { actionName: 'CREATED', }, '2': { - reportActionID: '2', + reportActionID: '2', created: '2025-01-15T10:30:00Z', // Later action actionName: 'IOU', }, + '3': { + reportActionID: '3', + created: '2025-01-12T14:20:00Z', // Middle action + actionName: 'COMMENT', + }, }; mockReportActionsUtils.getAllReportActions.mockReturnValue(mockReportActions as any); - mockReportActionsUtils.getSortedReportActions.mockReturnValue([ - mockReportActions['1'], - mockReportActions['2'], - ] as any); }); test('should compute basic report formula', () => { @@ -209,9 +208,12 @@ describe('CustomFormula', () => { test('should handle missing report actions for startdate', () => { mockReportActionsUtils.getAllReportActions.mockReturnValue({}); - mockReportActionsUtils.getSortedReportActions.mockReturnValue([]); - - const result = compute('{report:startdate}', mockContext); + const context: FormulaContext = { + report: {total: undefined} as any, + policy: null, + }; + + const result = compute('{report:startdate}', context); expect(result).toBe(''); }); }); From 6d2a77b341da9c23f05503558d38a335b61e0fea Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 4 Aug 2025 18:29:37 +0200 Subject: [PATCH 18/54] startdate and created date computation. date formats --- src/libs/Formula.ts | 72 +++++++++++++++++++++++++------ tests/unit/FormulaTest.ts | 89 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 142 insertions(+), 19 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 5dfba8b2c2dbe..36f1645e26d27 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -5,6 +5,7 @@ import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import {getCurrencySymbol} from './CurrencyUtils'; import {getAllReportActions} from './ReportActionsUtils'; +import {getReportTransactions} from './ReportUtils'; type FormulaPart = { /** The original definition from the formula */ @@ -232,7 +233,7 @@ function compute(formula: string, context: FormulaContext): string { */ function computeReportPart(part: FormulaPart, context: FormulaContext): string { const {report, policy} = context; - const [field] = part.fieldPath; + const [field, format] = part.fieldPath; if (!field) { return part.definition; @@ -242,7 +243,7 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { case 'type': return 'Expense Report'; // Default report type for now case 'startdate': - return formatDate(getOldestReportActionDate(report.reportID)); + return formatDate(getOldestTransactionDate(report.reportID), format); case 'total': return formatAmount(report.total, getCurrencySymbol(report.currency ?? '') ?? report.currency); case 'currency': @@ -251,7 +252,7 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { case 'workspacename': return policy?.name ?? ''; case 'created': - return formatDate(report.lastVisibleActionCreated, CONST.DATE.FNS_FORMAT_STRING); + return formatDate(getOldestReportActionDate(report.reportID), format); default: return part.definition; } @@ -328,9 +329,9 @@ function getSubstring(value: string, args: string[]): string { } /** - * Format a date value + * Format a date value with support for multiple date formats */ -function formatDate(dateString: string | undefined, format = CONST.DATE.FNS_FORMAT_STRING): string { +function formatDate(dateString: string | undefined, format = 'MM/dd/yyyy'): string { if (!dateString) { return ''; } @@ -341,16 +342,35 @@ function formatDate(dateString: string | undefined, format = CONST.DATE.FNS_FORM return ''; } - // Simple date formatting - this could be enhanced with a proper date library const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - - if (format === CONST.DATE.FNS_FORMAT_STRING) { - return `${month}/${day}/${year}`; + const month = date.getMonth() + 1; + const day = date.getDate(); + const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + const shortMonthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + switch (format) { + case 'M/dd/yyyy': + return `${month}/${day.toString().padStart(2, '0')}/${year}`; + case 'MMMM dd, yyyy': + return `${monthNames[month - 1]} ${day.toString().padStart(2, '0')}, ${year}`; + case 'dd MMM yyyy': + return `${day.toString().padStart(2, '0')} ${shortMonthNames[month - 1]} ${year}`; + case 'yyyy/MM/dd': + return `${year}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}`; + case 'yyyy-MM-dd': + return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`; + case 'MMMM, yyyy': + return `${monthNames[month - 1]}, ${year}`; + case 'yy/MM/dd': + return `${year.toString().slice(-2)}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}`; + case 'dd/MM/yy': + return `${day.toString().padStart(2, '0')}/${month.toString().padStart(2, '0')}/${year.toString().slice(-2)}`; + case 'yyyy': + return year.toString(); + case 'MM/dd/yyyy': + default: + return `${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}/${year}`; } - - return `${year}-${month}-${day}`; } catch { return ''; } @@ -400,6 +420,32 @@ function getOldestReportActionDate(reportID: string): string | undefined { return oldestDate; } +/** + * Get the date of the oldest transaction for a given report + */ +function getOldestTransactionDate(reportID: string): string | undefined { + if (!reportID) { + return undefined; + } + + const transactions = getReportTransactions(reportID); + if (!transactions || transactions.length === 0) { + return undefined; + } + + let oldestDate: string | undefined; + + transactions.forEach((transaction) => { + if (transaction?.created) { + if (!oldestDate || transaction.created < oldestDate) { + oldestDate = transaction.created; + } + } + }); + + return oldestDate; +} + /** * Check if a string contains formula parts */ diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index ce9feef9e678d..8d9cc86a66192 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -1,13 +1,19 @@ import {compute, extract, FORMULA_PART_TYPES, isFormula, parse} from '@libs/Formula'; import type {FormulaContext} from '@libs/Formula'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; -// Mock ReportActionsUtils +// Mock ReportActionsUtils and ReportUtils jest.mock('@libs/ReportActionsUtils', () => ({ getAllReportActions: jest.fn(), })); +jest.mock('@libs/ReportUtils', () => ({ + getReportTransactions: jest.fn(), +})); + const mockReportActionsUtils = ReportActionsUtils as jest.Mocked; +const mockReportUtils = ReportUtils as jest.Mocked; describe('CustomFormula', () => { describe('extract()', () => { @@ -102,7 +108,7 @@ describe('CustomFormula', () => { beforeEach(() => { jest.clearAllMocks(); - // Mock report actions - test the iteration logic for finding oldest date + // Mock report actions - test the iteration logic for finding oldest date (for 'created' field) const mockReportActions = { '1': { reportActionID: '1', @@ -121,7 +127,27 @@ describe('CustomFormula', () => { }, }; + // Mock transactions - test the iteration logic for finding oldest transaction date (for 'startdate' field) + const mockTransactions = [ + { + transactionID: 'trans1', + created: '2025-01-08T12:00:00Z', // Oldest transaction + amount: 5000, + }, + { + transactionID: 'trans2', + created: '2025-01-14T16:45:00Z', // Later transaction + amount: 3000, + }, + { + transactionID: 'trans3', + created: '2025-01-11T09:15:00Z', // Middle transaction + amount: 2000, + }, + ]; + mockReportActionsUtils.getAllReportActions.mockReturnValue(mockReportActions as any); + mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions as any); }); test('should compute basic report formula', () => { @@ -129,9 +155,29 @@ describe('CustomFormula', () => { expect(result).toBe('Expense ReportUSD100.00'); // No space between parts }); - test('should compute date formula', () => { + test('should compute startdate formula using transactions', () => { const result = compute('{report:startdate}', mockContext); - expect(result).toBe('01/10/2025'); // Should use oldest report action date + expect(result).toBe('01/08/2025'); // Should use oldest transaction date (2025-01-08) + }); + + test('should compute created formula using report actions', () => { + const result = compute('{report:created}', mockContext); + expect(result).toBe('01/10/2025'); // Should use oldest report action date (2025-01-10) + }); + + test('should compute startdate with custom format', () => { + const result = compute('{report:startdate:yyyy-MM-dd}', mockContext); + expect(result).toBe('2025-01-08'); // Should use oldest transaction date with yyyy-MM-dd format + }); + + test('should compute created with custom format', () => { + const result = compute('{report:created:MMMM dd, yyyy}', mockContext); + expect(result).toBe('January 10, 2025'); // Should use oldest report action date with MMMM dd, yyyy format + }); + + test('should compute startdate with short month format', () => { + const result = compute('{report:startdate:dd MMM yyyy}', mockContext); + expect(result).toBe('08 Jan 2025'); // Should use oldest transaction date with dd MMM yyyy format }); test('should compute policy name', () => { @@ -206,15 +252,46 @@ describe('CustomFormula', () => { expect(result).toBe(''); }); - test('should handle missing report actions for startdate', () => { + test('should handle missing report actions for created', () => { mockReportActionsUtils.getAllReportActions.mockReturnValue({}); const context: FormulaContext = { - report: {total: undefined} as any, + report: {reportID: '123'} as any, + policy: null, + }; + + const result = compute('{report:created}', context); + expect(result).toBe(''); + }); + + test('should handle missing transactions for startdate', () => { + mockReportUtils.getReportTransactions.mockReturnValue([]); + const context: FormulaContext = { + report: {reportID: '123'} as any, policy: null, }; const result = compute('{report:startdate}', context); expect(result).toBe(''); }); + + test('should call getReportTransactions with correct reportID for startdate', () => { + const context: FormulaContext = { + report: {reportID: 'test-report-123'} as any, + policy: null, + }; + + compute('{report:startdate}', context); + expect(mockReportUtils.getReportTransactions).toHaveBeenCalledWith('test-report-123'); + }); + + test('should call getAllReportActions with correct reportID for created', () => { + const context: FormulaContext = { + report: {reportID: 'test-report-456'} as any, + policy: null, + }; + + compute('{report:created}', context); + expect(mockReportActionsUtils.getAllReportActions).toHaveBeenCalledWith('test-report-456'); + }); }); }); From 4a9391fffb1f33aa6f058d1b120ad2bdfa5494b2 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Tue, 5 Aug 2025 09:49:59 +0200 Subject: [PATCH 19/54] format report types --- src/libs/Formula.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 36f1645e26d27..58fe4de1cd3c0 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -241,7 +241,7 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { switch (field.toLowerCase()) { case 'type': - return 'Expense Report'; // Default report type for now + return formatType(report.type); case 'startdate': return formatDate(getOldestTransactionDate(report.reportID), format); case 'total': @@ -420,6 +420,28 @@ function getOldestReportActionDate(reportID: string): string | undefined { return oldestDate; } +/** + * Format a report type to its human-readable string + */ +function formatType(type: string | undefined): string { + if (!type) { + return ''; + } + + const typeMapping: Record = { + expense: 'Expense Report', + invoice: 'Invoice', + chat: 'Chat', + bill: 'Bill', + paycheck: 'Paycheck', + iou: 'IOU', + task: 'Task', + trip: 'Trip', + }; + + return typeMapping[type.toLowerCase()] || type; +} + /** * Get the date of the oldest transaction for a given report */ From a270a9b34b13135a0c86231dd3eabd505a0f1e26 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Tue, 5 Aug 2025 09:57:16 +0200 Subject: [PATCH 20/54] Use consts as typeMapping keys --- src/libs/Formula.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 58fe4de1cd3c0..576c0e4849a26 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -429,13 +429,13 @@ function formatType(type: string | undefined): string { } const typeMapping: Record = { - expense: 'Expense Report', - invoice: 'Invoice', - chat: 'Chat', - bill: 'Bill', - paycheck: 'Paycheck', - iou: 'IOU', - task: 'Task', + [CONST.REPORT.TYPE.EXPENSE]: 'Expense Report', + [CONST.REPORT.TYPE.INVOICE]: 'Invoice', + [CONST.REPORT.TYPE.CHAT]: 'Chat', + [CONST.REPORT.UNSUPPORTED_TYPE.BILL]: 'Bill', + [CONST.REPORT.UNSUPPORTED_TYPE.PAYCHECK]: 'Paycheck', + [CONST.REPORT.TYPE.IOU]: 'IOU', + [CONST.REPORT.TYPE.TASK]: 'Task', trip: 'Trip', }; From 9e0a6bd45a3bacd6fc65c462eb8fa3e5733ae873 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Tue, 5 Aug 2025 10:30:05 +0200 Subject: [PATCH 21/54] Centralized connection manager to use onyx values in optimistic report names --- src/libs/OptimisticReportNames.ts | 48 ++--------- .../OptimisticReportNamesConnectionManager.ts | 84 +++++++++++++++++++ 2 files changed, 89 insertions(+), 43 deletions(-) create mode 100644 src/libs/OptimisticReportNamesConnectionManager.ts diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 7a47bdf98b417..6a4d43c76ad17 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -1,23 +1,18 @@ -import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type Beta from '@src/types/onyx/Beta'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import Timing from './actions/Timing'; import * as Formula from './Formula'; import Log from './Log'; +import {getUpdateContextAsync} from './OptimisticReportNamesConnectionManager'; +import type {UpdateContext} from './OptimisticReportNamesConnectionManager'; import Performance from './Performance'; import Permissions from './Permissions'; import * as ReportUtils from './ReportUtils'; -type UpdateContext = { - betas: OnyxEntry; - allReports: Record; - allPolicies: Record; -}; - /** * Get the object type from an Onyx key */ @@ -367,44 +362,11 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: } /** - * Initialize the context needed for report name computation + * Creates update context for optimistic report name processing. * This should be called before processing optimistic updates */ function createUpdateContext(): Promise { - return new Promise((resolve) => { - // Get all the data we need from Onyx - const connectionID = Onyx.connect({ - key: ONYXKEYS.BETAS, - callback: (betas) => { - Onyx.disconnect(connectionID); - - // Also get reports and policies - const reportsConnectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (allReports) => { - Onyx.disconnect(reportsConnectionID); - - const policiesConnectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY, - waitForCollectionCallback: true, - callback: (allPolicies) => { - Onyx.disconnect(policiesConnectionID); - - resolve({ - betas, - allReports: allReports ?? {}, - allPolicies: allPolicies ?? {}, - }); - }, - }); - }, - }); - }, - }); - }); + return getUpdateContextAsync(); } export {updateOptimisticReportNamesFromUpdates, computeReportNameIfNeeded, computeNameForNewReport, createUpdateContext, shouldComputeReportName}; - -export type {UpdateContext}; diff --git a/src/libs/OptimisticReportNamesConnectionManager.ts b/src/libs/OptimisticReportNamesConnectionManager.ts new file mode 100644 index 0000000000000..d59c721e246a6 --- /dev/null +++ b/src/libs/OptimisticReportNamesConnectionManager.ts @@ -0,0 +1,84 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type Beta from '@src/types/onyx/Beta'; +import type Policy from '@src/types/onyx/Policy'; +import type Report from '@src/types/onyx/Report'; + +type UpdateContext = { + betas: OnyxEntry; + allReports: Record; + allPolicies: Record; +}; + +let betas: OnyxEntry; +let allReports: Record; +let allPolicies: Record; +let isInitialized = false; + +/** + * Initialize persistent connections to Onyx data needed for OptimisticReportNames + * This is called lazily when OptimisticReportNames functionality is first used + */ +function initialize(): void { + if (isInitialized) { + return; + } + + // Connect to BETAS + // eslint-disable-next-line react-compiler/react-compiler -- This is not a React component and needs to access Onyx data for OptimisticReportNames processing without being tied to a UI view + Onyx.connectWithoutView({ + key: ONYXKEYS.BETAS, + callback: (val) => { + betas = val; + }, + }); + + // Connect to all REPORTS + // eslint-disable-next-line react-compiler/react-compiler -- This is not a React component and needs to access Onyx data for OptimisticReportNames processing without being tied to a UI view + Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (val) => { + allReports = (val as Record) ?? {}; + }, + }); + + // Connect to all POLICIES + // eslint-disable-next-line react-compiler/react-compiler -- This is not a React component and needs to access Onyx data for OptimisticReportNames processing without being tied to a UI view + Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.POLICY, + waitForCollectionCallback: true, + callback: (val) => { + allPolicies = (val as Record) ?? {}; + }, + }); + + isInitialized = true; +} + +/** + * Get the current update context for OptimisticReportNames + * This provides access to the cached Onyx data without creating new connections + * Initializes connections lazily on first use + */ +function getUpdateContext(): UpdateContext { + initialize(); + return { + betas, + allReports: allReports ?? {}, + allPolicies: allPolicies ?? {}, + }; +} + +/** + * Get the current update context as a promise for backward compatibility + * Initializes connections lazily on first use + */ +function getUpdateContextAsync(): Promise { + initialize(); + return Promise.resolve(getUpdateContext()); +} + +export {getUpdateContext, getUpdateContextAsync}; +export type {UpdateContext}; From 75f85b4dae66480da234bcca7512d0c04d3c6c4a Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Tue, 5 Aug 2025 10:35:47 +0200 Subject: [PATCH 22/54] Default startdate --- src/libs/Formula.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 576c0e4849a26..fa15bc862ac3c 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -452,7 +452,7 @@ function getOldestTransactionDate(reportID: string): string | undefined { const transactions = getReportTransactions(reportID); if (!transactions || transactions.length === 0) { - return undefined; + return new Date().toISOString(); } let oldestDate: string | undefined; From 3168ff8445d58971ea5326df02ffb70c52db81dd Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Tue, 5 Aug 2025 10:38:10 +0200 Subject: [PATCH 23/54] Revert unwanted changes --- .../javascript/checkAndroidStatus/index.js | 8488 ++++++++--------- 1 file changed, 4244 insertions(+), 4244 deletions(-) diff --git a/.github/actions/javascript/checkAndroidStatus/index.js b/.github/actions/javascript/checkAndroidStatus/index.js index 78d91cca04a0c..fda2d5e7ba53a 100644 --- a/.github/actions/javascript/checkAndroidStatus/index.js +++ b/.github/actions/javascript/checkAndroidStatus/index.js @@ -559,8 +559,8 @@ class OidcClient { const res = yield httpclient .getJson(id_token_url) .catch(error => { - throw new Error(`Failed to get ID Token. \n - Error Code : ${error.statusCode}\n + throw new Error(`Failed to get ID Token. \n + Error Code : ${error.statusCode}\n Error Message: ${error.result.message}`); }); const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value; @@ -5143,2928 +5143,2928 @@ function removeHook(state, name, method) { /***/ 87558: /***/ (function(module) { -;(function (globalObject) { - 'use strict'; - -/* - * bignumber.js v9.1.2 - * A JavaScript library for arbitrary-precision arithmetic. - * https://github.com/MikeMcl/bignumber.js - * Copyright (c) 2022 Michael Mclaughlin - * MIT Licensed. - * - * BigNumber.prototype methods | BigNumber methods - * | - * absoluteValue abs | clone - * comparedTo | config set - * decimalPlaces dp | DECIMAL_PLACES - * dividedBy div | ROUNDING_MODE - * dividedToIntegerBy idiv | EXPONENTIAL_AT - * exponentiatedBy pow | RANGE - * integerValue | CRYPTO - * isEqualTo eq | MODULO_MODE - * isFinite | POW_PRECISION - * isGreaterThan gt | FORMAT - * isGreaterThanOrEqualTo gte | ALPHABET - * isInteger | isBigNumber - * isLessThan lt | maximum max - * isLessThanOrEqualTo lte | minimum min - * isNaN | random - * isNegative | sum - * isPositive | - * isZero | - * minus | - * modulo mod | - * multipliedBy times | - * negated | - * plus | - * precision sd | - * shiftedBy | - * squareRoot sqrt | - * toExponential | - * toFixed | - * toFormat | - * toFraction | - * toJSON | - * toNumber | - * toPrecision | - * toString | - * valueOf | - * - */ - - - var BigNumber, - isNumeric = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i, - mathceil = Math.ceil, - mathfloor = Math.floor, - - bignumberError = '[BigNumber Error] ', - tooManyDigits = bignumberError + 'Number primitive has more than 15 significant digits: ', - - BASE = 1e14, - LOG_BASE = 14, - MAX_SAFE_INTEGER = 0x1fffffffffffff, // 2^53 - 1 - // MAX_INT32 = 0x7fffffff, // 2^31 - 1 - POWS_TEN = [1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13], - SQRT_BASE = 1e7, - - // EDITABLE - // The limit on the value of DECIMAL_PLACES, TO_EXP_NEG, TO_EXP_POS, MIN_EXP, MAX_EXP, and - // the arguments to toExponential, toFixed, toFormat, and toPrecision. - MAX = 1E9; // 0 to MAX_INT32 - - - /* - * Create and return a BigNumber constructor. - */ - function clone(configObject) { - var div, convertBase, parseNumeric, - P = BigNumber.prototype = { constructor: BigNumber, toString: null, valueOf: null }, - ONE = new BigNumber(1), - - - //----------------------------- EDITABLE CONFIG DEFAULTS ------------------------------- - - - // The default values below must be integers within the inclusive ranges stated. - // The values can also be changed at run-time using BigNumber.set. - - // The maximum number of decimal places for operations involving division. - DECIMAL_PLACES = 20, // 0 to MAX - - // The rounding mode used when rounding to the above decimal places, and when using - // toExponential, toFixed, toFormat and toPrecision, and round (default value). - // UP 0 Away from zero. - // DOWN 1 Towards zero. - // CEIL 2 Towards +Infinity. - // FLOOR 3 Towards -Infinity. - // HALF_UP 4 Towards nearest neighbour. If equidistant, up. - // HALF_DOWN 5 Towards nearest neighbour. If equidistant, down. - // HALF_EVEN 6 Towards nearest neighbour. If equidistant, towards even neighbour. - // HALF_CEIL 7 Towards nearest neighbour. If equidistant, towards +Infinity. - // HALF_FLOOR 8 Towards nearest neighbour. If equidistant, towards -Infinity. - ROUNDING_MODE = 4, // 0 to 8 - - // EXPONENTIAL_AT : [TO_EXP_NEG , TO_EXP_POS] - - // The exponent value at and beneath which toString returns exponential notation. - // Number type: -7 - TO_EXP_NEG = -7, // 0 to -MAX - - // The exponent value at and above which toString returns exponential notation. - // Number type: 21 - TO_EXP_POS = 21, // 0 to MAX - - // RANGE : [MIN_EXP, MAX_EXP] - - // The minimum exponent value, beneath which underflow to zero occurs. - // Number type: -324 (5e-324) - MIN_EXP = -1e7, // -1 to -MAX - - // The maximum exponent value, above which overflow to Infinity occurs. - // Number type: 308 (1.7976931348623157e+308) - // For MAX_EXP > 1e7, e.g. new BigNumber('1e100000000').plus(1) may be slow. - MAX_EXP = 1e7, // 1 to MAX - - // Whether to use cryptographically-secure random number generation, if available. - CRYPTO = false, // true or false - - // The modulo mode used when calculating the modulus: a mod n. - // The quotient (q = a / n) is calculated according to the corresponding rounding mode. - // The remainder (r) is calculated as: r = a - n * q. - // - // UP 0 The remainder is positive if the dividend is negative, else is negative. - // DOWN 1 The remainder has the same sign as the dividend. - // This modulo mode is commonly known as 'truncated division' and is - // equivalent to (a % n) in JavaScript. - // FLOOR 3 The remainder has the same sign as the divisor (Python %). - // HALF_EVEN 6 This modulo mode implements the IEEE 754 remainder function. - // EUCLID 9 Euclidian division. q = sign(n) * floor(a / abs(n)). - // The remainder is always positive. - // - // The truncated division, floored division, Euclidian division and IEEE 754 remainder - // modes are commonly used for the modulus operation. - // Although the other rounding modes can also be used, they may not give useful results. - MODULO_MODE = 1, // 0 to 9 - - // The maximum number of significant digits of the result of the exponentiatedBy operation. - // If POW_PRECISION is 0, there will be unlimited significant digits. - POW_PRECISION = 0, // 0 to MAX - - // The format specification used by the BigNumber.prototype.toFormat method. - FORMAT = { - prefix: '', - groupSize: 3, - secondaryGroupSize: 0, - groupSeparator: ',', - decimalSeparator: '.', - fractionGroupSize: 0, - fractionGroupSeparator: '\xA0', // non-breaking space - suffix: '' - }, - - // The alphabet used for base conversion. It must be at least 2 characters long, with no '+', - // '-', '.', whitespace, or repeated character. - // '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_' - ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz', - alphabetHasNormalDecimalDigits = true; - - - //------------------------------------------------------------------------------------------ - - - // CONSTRUCTOR - - - /* - * The BigNumber constructor and exported function. - * Create and return a new instance of a BigNumber object. - * - * v {number|string|BigNumber} A numeric value. - * [b] {number} The base of v. Integer, 2 to ALPHABET.length inclusive. - */ - function BigNumber(v, b) { - var alphabet, c, caseChanged, e, i, isNum, len, str, - x = this; - - // Enable constructor call without `new`. - if (!(x instanceof BigNumber)) return new BigNumber(v, b); - - if (b == null) { - - if (v && v._isBigNumber === true) { - x.s = v.s; - - if (!v.c || v.e > MAX_EXP) { - x.c = x.e = null; - } else if (v.e < MIN_EXP) { - x.c = [x.e = 0]; - } else { - x.e = v.e; - x.c = v.c.slice(); - } - - return; - } - - if ((isNum = typeof v == 'number') && v * 0 == 0) { - - // Use `1 / n` to handle minus zero also. - x.s = 1 / v < 0 ? (v = -v, -1) : 1; - - // Fast path for integers, where n < 2147483648 (2**31). - if (v === ~~v) { - for (e = 0, i = v; i >= 10; i /= 10, e++); - - if (e > MAX_EXP) { - x.c = x.e = null; - } else { - x.e = e; - x.c = [v]; - } - - return; - } - - str = String(v); - } else { - - if (!isNumeric.test(str = String(v))) return parseNumeric(x, str, isNum); - - x.s = str.charCodeAt(0) == 45 ? (str = str.slice(1), -1) : 1; - } - - // Decimal point? - if ((e = str.indexOf('.')) > -1) str = str.replace('.', ''); - - // Exponential form? - if ((i = str.search(/e/i)) > 0) { - - // Determine exponent. - if (e < 0) e = i; - e += +str.slice(i + 1); - str = str.substring(0, i); - } else if (e < 0) { - - // Integer. - e = str.length; - } - - } else { - - // '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}' - intCheck(b, 2, ALPHABET.length, 'Base'); - - // Allow exponential notation to be used with base 10 argument, while - // also rounding to DECIMAL_PLACES as with other bases. - if (b == 10 && alphabetHasNormalDecimalDigits) { - x = new BigNumber(v); - return round(x, DECIMAL_PLACES + x.e + 1, ROUNDING_MODE); - } - - str = String(v); - - if (isNum = typeof v == 'number') { - - // Avoid potential interpretation of Infinity and NaN as base 44+ values. - if (v * 0 != 0) return parseNumeric(x, str, isNum, b); - - x.s = 1 / v < 0 ? (str = str.slice(1), -1) : 1; - - // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}' - if (BigNumber.DEBUG && str.replace(/^0\.0*|\./, '').length > 15) { - throw Error - (tooManyDigits + v); - } - } else { - x.s = str.charCodeAt(0) === 45 ? (str = str.slice(1), -1) : 1; - } - - alphabet = ALPHABET.slice(0, b); - e = i = 0; - - // Check that str is a valid base b number. - // Don't use RegExp, so alphabet can contain special characters. - for (len = str.length; i < len; i++) { - if (alphabet.indexOf(c = str.charAt(i)) < 0) { - if (c == '.') { - - // If '.' is not the first character and it has not be found before. - if (i > e) { - e = len; - continue; - } - } else if (!caseChanged) { - - // Allow e.g. hexadecimal 'FF' as well as 'ff'. - if (str == str.toUpperCase() && (str = str.toLowerCase()) || - str == str.toLowerCase() && (str = str.toUpperCase())) { - caseChanged = true; - i = -1; - e = 0; - continue; - } - } - - return parseNumeric(x, String(v), isNum, b); - } - } - - // Prevent later check for length on converted number. - isNum = false; - str = convertBase(str, b, 10, x.s); - - // Decimal point? - if ((e = str.indexOf('.')) > -1) str = str.replace('.', ''); - else e = str.length; - } - - // Determine leading zeros. - for (i = 0; str.charCodeAt(i) === 48; i++); - - // Determine trailing zeros. - for (len = str.length; str.charCodeAt(--len) === 48;); - - if (str = str.slice(i, ++len)) { - len -= i; - - // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}' - if (isNum && BigNumber.DEBUG && - len > 15 && (v > MAX_SAFE_INTEGER || v !== mathfloor(v))) { - throw Error - (tooManyDigits + (x.s * v)); - } - - // Overflow? - if ((e = e - i - 1) > MAX_EXP) { - - // Infinity. - x.c = x.e = null; - - // Underflow? - } else if (e < MIN_EXP) { - - // Zero. - x.c = [x.e = 0]; - } else { - x.e = e; - x.c = []; - - // Transform base - - // e is the base 10 exponent. - // i is where to slice str to get the first element of the coefficient array. - i = (e + 1) % LOG_BASE; - if (e < 0) i += LOG_BASE; // i < 1 - - if (i < len) { - if (i) x.c.push(+str.slice(0, i)); - - for (len -= LOG_BASE; i < len;) { - x.c.push(+str.slice(i, i += LOG_BASE)); - } - - i = LOG_BASE - (str = str.slice(i)).length; - } else { - i -= len; - } - - for (; i--; str += '0'); - x.c.push(+str); - } - } else { - - // Zero. - x.c = [x.e = 0]; - } - } - - - // CONSTRUCTOR PROPERTIES - - - BigNumber.clone = clone; - - BigNumber.ROUND_UP = 0; - BigNumber.ROUND_DOWN = 1; - BigNumber.ROUND_CEIL = 2; - BigNumber.ROUND_FLOOR = 3; - BigNumber.ROUND_HALF_UP = 4; - BigNumber.ROUND_HALF_DOWN = 5; - BigNumber.ROUND_HALF_EVEN = 6; - BigNumber.ROUND_HALF_CEIL = 7; - BigNumber.ROUND_HALF_FLOOR = 8; - BigNumber.EUCLID = 9; - - - /* - * Configure infrequently-changing library-wide settings. - * - * Accept an object with the following optional properties (if the value of a property is - * a number, it must be an integer within the inclusive range stated): - * - * DECIMAL_PLACES {number} 0 to MAX - * ROUNDING_MODE {number} 0 to 8 - * EXPONENTIAL_AT {number|number[]} -MAX to MAX or [-MAX to 0, 0 to MAX] - * RANGE {number|number[]} -MAX to MAX (not zero) or [-MAX to -1, 1 to MAX] - * CRYPTO {boolean} true or false - * MODULO_MODE {number} 0 to 9 - * POW_PRECISION {number} 0 to MAX - * ALPHABET {string} A string of two or more unique characters which does - * not contain '.'. - * FORMAT {object} An object with some of the following properties: - * prefix {string} - * groupSize {number} - * secondaryGroupSize {number} - * groupSeparator {string} - * decimalSeparator {string} - * fractionGroupSize {number} - * fractionGroupSeparator {string} - * suffix {string} - * - * (The values assigned to the above FORMAT object properties are not checked for validity.) - * - * E.g. - * BigNumber.config({ DECIMAL_PLACES : 20, ROUNDING_MODE : 4 }) - * - * Ignore properties/parameters set to null or undefined, except for ALPHABET. - * - * Return an object with the properties current values. - */ - BigNumber.config = BigNumber.set = function (obj) { - var p, v; - - if (obj != null) { - - if (typeof obj == 'object') { - - // DECIMAL_PLACES {number} Integer, 0 to MAX inclusive. - // '[BigNumber Error] DECIMAL_PLACES {not a primitive number|not an integer|out of range}: {v}' - if (obj.hasOwnProperty(p = 'DECIMAL_PLACES')) { - v = obj[p]; - intCheck(v, 0, MAX, p); - DECIMAL_PLACES = v; - } - - // ROUNDING_MODE {number} Integer, 0 to 8 inclusive. - // '[BigNumber Error] ROUNDING_MODE {not a primitive number|not an integer|out of range}: {v}' - if (obj.hasOwnProperty(p = 'ROUNDING_MODE')) { - v = obj[p]; - intCheck(v, 0, 8, p); - ROUNDING_MODE = v; - } - - // EXPONENTIAL_AT {number|number[]} - // Integer, -MAX to MAX inclusive or - // [integer -MAX to 0 inclusive, 0 to MAX inclusive]. - // '[BigNumber Error] EXPONENTIAL_AT {not a primitive number|not an integer|out of range}: {v}' - if (obj.hasOwnProperty(p = 'EXPONENTIAL_AT')) { - v = obj[p]; - if (v && v.pop) { - intCheck(v[0], -MAX, 0, p); - intCheck(v[1], 0, MAX, p); - TO_EXP_NEG = v[0]; - TO_EXP_POS = v[1]; - } else { - intCheck(v, -MAX, MAX, p); - TO_EXP_NEG = -(TO_EXP_POS = v < 0 ? -v : v); - } - } - - // RANGE {number|number[]} Non-zero integer, -MAX to MAX inclusive or - // [integer -MAX to -1 inclusive, integer 1 to MAX inclusive]. - // '[BigNumber Error] RANGE {not a primitive number|not an integer|out of range|cannot be zero}: {v}' - if (obj.hasOwnProperty(p = 'RANGE')) { - v = obj[p]; - if (v && v.pop) { - intCheck(v[0], -MAX, -1, p); - intCheck(v[1], 1, MAX, p); - MIN_EXP = v[0]; - MAX_EXP = v[1]; - } else { - intCheck(v, -MAX, MAX, p); - if (v) { - MIN_EXP = -(MAX_EXP = v < 0 ? -v : v); - } else { - throw Error - (bignumberError + p + ' cannot be zero: ' + v); - } - } - } - - // CRYPTO {boolean} true or false. - // '[BigNumber Error] CRYPTO not true or false: {v}' - // '[BigNumber Error] crypto unavailable' - if (obj.hasOwnProperty(p = 'CRYPTO')) { - v = obj[p]; - if (v === !!v) { - if (v) { - if (typeof crypto != 'undefined' && crypto && - (crypto.getRandomValues || crypto.randomBytes)) { - CRYPTO = v; - } else { - CRYPTO = !v; - throw Error - (bignumberError + 'crypto unavailable'); - } - } else { - CRYPTO = v; - } - } else { - throw Error - (bignumberError + p + ' not true or false: ' + v); - } - } - - // MODULO_MODE {number} Integer, 0 to 9 inclusive. - // '[BigNumber Error] MODULO_MODE {not a primitive number|not an integer|out of range}: {v}' - if (obj.hasOwnProperty(p = 'MODULO_MODE')) { - v = obj[p]; - intCheck(v, 0, 9, p); - MODULO_MODE = v; - } - - // POW_PRECISION {number} Integer, 0 to MAX inclusive. - // '[BigNumber Error] POW_PRECISION {not a primitive number|not an integer|out of range}: {v}' - if (obj.hasOwnProperty(p = 'POW_PRECISION')) { - v = obj[p]; - intCheck(v, 0, MAX, p); - POW_PRECISION = v; - } - - // FORMAT {object} - // '[BigNumber Error] FORMAT not an object: {v}' - if (obj.hasOwnProperty(p = 'FORMAT')) { - v = obj[p]; - if (typeof v == 'object') FORMAT = v; - else throw Error - (bignumberError + p + ' not an object: ' + v); - } - - // ALPHABET {string} - // '[BigNumber Error] ALPHABET invalid: {v}' - if (obj.hasOwnProperty(p = 'ALPHABET')) { - v = obj[p]; - - // Disallow if less than two characters, - // or if it contains '+', '-', '.', whitespace, or a repeated character. - if (typeof v == 'string' && !/^.?$|[+\-.\s]|(.).*\1/.test(v)) { - alphabetHasNormalDecimalDigits = v.slice(0, 10) == '0123456789'; - ALPHABET = v; - } else { - throw Error - (bignumberError + p + ' invalid: ' + v); - } - } - - } else { - - // '[BigNumber Error] Object expected: {v}' - throw Error - (bignumberError + 'Object expected: ' + obj); - } - } - - return { - DECIMAL_PLACES: DECIMAL_PLACES, - ROUNDING_MODE: ROUNDING_MODE, - EXPONENTIAL_AT: [TO_EXP_NEG, TO_EXP_POS], - RANGE: [MIN_EXP, MAX_EXP], - CRYPTO: CRYPTO, - MODULO_MODE: MODULO_MODE, - POW_PRECISION: POW_PRECISION, - FORMAT: FORMAT, - ALPHABET: ALPHABET - }; - }; - - - /* - * Return true if v is a BigNumber instance, otherwise return false. - * - * If BigNumber.DEBUG is true, throw if a BigNumber instance is not well-formed. - * - * v {any} - * - * '[BigNumber Error] Invalid BigNumber: {v}' - */ - BigNumber.isBigNumber = function (v) { - if (!v || v._isBigNumber !== true) return false; - if (!BigNumber.DEBUG) return true; - - var i, n, - c = v.c, - e = v.e, - s = v.s; - - out: if ({}.toString.call(c) == '[object Array]') { - - if ((s === 1 || s === -1) && e >= -MAX && e <= MAX && e === mathfloor(e)) { - - // If the first element is zero, the BigNumber value must be zero. - if (c[0] === 0) { - if (e === 0 && c.length === 1) return true; - break out; - } - - // Calculate number of digits that c[0] should have, based on the exponent. - i = (e + 1) % LOG_BASE; - if (i < 1) i += LOG_BASE; - - // Calculate number of digits of c[0]. - //if (Math.ceil(Math.log(c[0] + 1) / Math.LN10) == i) { - if (String(c[0]).length == i) { - - for (i = 0; i < c.length; i++) { - n = c[i]; - if (n < 0 || n >= BASE || n !== mathfloor(n)) break out; - } - - // Last element cannot be zero, unless it is the only element. - if (n !== 0) return true; - } - } - - // Infinity/NaN - } else if (c === null && e === null && (s === null || s === 1 || s === -1)) { - return true; - } - - throw Error - (bignumberError + 'Invalid BigNumber: ' + v); - }; - - - /* - * Return a new BigNumber whose value is the maximum of the arguments. - * - * arguments {number|string|BigNumber} - */ - BigNumber.maximum = BigNumber.max = function () { - return maxOrMin(arguments, -1); - }; - - - /* - * Return a new BigNumber whose value is the minimum of the arguments. - * - * arguments {number|string|BigNumber} - */ - BigNumber.minimum = BigNumber.min = function () { - return maxOrMin(arguments, 1); - }; - - - /* - * Return a new BigNumber with a random value equal to or greater than 0 and less than 1, - * and with dp, or DECIMAL_PLACES if dp is omitted, decimal places (or less if trailing - * zeros are produced). - * - * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp}' - * '[BigNumber Error] crypto unavailable' - */ - BigNumber.random = (function () { - var pow2_53 = 0x20000000000000; - - // Return a 53 bit integer n, where 0 <= n < 9007199254740992. - // Check if Math.random() produces more than 32 bits of randomness. - // If it does, assume at least 53 bits are produced, otherwise assume at least 30 bits. - // 0x40000000 is 2^30, 0x800000 is 2^23, 0x1fffff is 2^21 - 1. - var random53bitInt = (Math.random() * pow2_53) & 0x1fffff - ? function () { return mathfloor(Math.random() * pow2_53); } - : function () { return ((Math.random() * 0x40000000 | 0) * 0x800000) + - (Math.random() * 0x800000 | 0); }; - - return function (dp) { - var a, b, e, k, v, - i = 0, - c = [], - rand = new BigNumber(ONE); - - if (dp == null) dp = DECIMAL_PLACES; - else intCheck(dp, 0, MAX); - - k = mathceil(dp / LOG_BASE); - - if (CRYPTO) { - - // Browsers supporting crypto.getRandomValues. - if (crypto.getRandomValues) { - - a = crypto.getRandomValues(new Uint32Array(k *= 2)); - - for (; i < k;) { - - // 53 bits: - // ((Math.pow(2, 32) - 1) * Math.pow(2, 21)).toString(2) - // 11111 11111111 11111111 11111111 11100000 00000000 00000000 - // ((Math.pow(2, 32) - 1) >>> 11).toString(2) - // 11111 11111111 11111111 - // 0x20000 is 2^21. - v = a[i] * 0x20000 + (a[i + 1] >>> 11); - - // Rejection sampling: - // 0 <= v < 9007199254740992 - // Probability that v >= 9e15, is - // 7199254740992 / 9007199254740992 ~= 0.0008, i.e. 1 in 1251 - if (v >= 9e15) { - b = crypto.getRandomValues(new Uint32Array(2)); - a[i] = b[0]; - a[i + 1] = b[1]; - } else { - - // 0 <= v <= 8999999999999999 - // 0 <= (v % 1e14) <= 99999999999999 - c.push(v % 1e14); - i += 2; - } - } - i = k / 2; - - // Node.js supporting crypto.randomBytes. - } else if (crypto.randomBytes) { - - // buffer - a = crypto.randomBytes(k *= 7); - - for (; i < k;) { - - // 0x1000000000000 is 2^48, 0x10000000000 is 2^40 - // 0x100000000 is 2^32, 0x1000000 is 2^24 - // 11111 11111111 11111111 11111111 11111111 11111111 11111111 - // 0 <= v < 9007199254740992 - v = ((a[i] & 31) * 0x1000000000000) + (a[i + 1] * 0x10000000000) + - (a[i + 2] * 0x100000000) + (a[i + 3] * 0x1000000) + - (a[i + 4] << 16) + (a[i + 5] << 8) + a[i + 6]; - - if (v >= 9e15) { - crypto.randomBytes(7).copy(a, i); - } else { - - // 0 <= (v % 1e14) <= 99999999999999 - c.push(v % 1e14); - i += 7; - } - } - i = k / 7; - } else { - CRYPTO = false; - throw Error - (bignumberError + 'crypto unavailable'); - } - } - - // Use Math.random. - if (!CRYPTO) { - - for (; i < k;) { - v = random53bitInt(); - if (v < 9e15) c[i++] = v % 1e14; - } - } - - k = c[--i]; - dp %= LOG_BASE; - - // Convert trailing digits to zeros according to dp. - if (k && dp) { - v = POWS_TEN[LOG_BASE - dp]; - c[i] = mathfloor(k / v) * v; - } - - // Remove trailing elements which are zero. - for (; c[i] === 0; c.pop(), i--); - - // Zero? - if (i < 0) { - c = [e = 0]; - } else { - - // Remove leading elements which are zero and adjust exponent accordingly. - for (e = -1 ; c[0] === 0; c.splice(0, 1), e -= LOG_BASE); - - // Count the digits of the first element of c to determine leading zeros, and... - for (i = 1, v = c[0]; v >= 10; v /= 10, i++); - - // adjust the exponent accordingly. - if (i < LOG_BASE) e -= LOG_BASE - i; - } - - rand.e = e; - rand.c = c; - return rand; - }; - })(); - - - /* - * Return a BigNumber whose value is the sum of the arguments. - * - * arguments {number|string|BigNumber} - */ - BigNumber.sum = function () { - var i = 1, - args = arguments, - sum = new BigNumber(args[0]); - for (; i < args.length;) sum = sum.plus(args[i++]); - return sum; - }; - - - // PRIVATE FUNCTIONS - - - // Called by BigNumber and BigNumber.prototype.toString. - convertBase = (function () { - var decimal = '0123456789'; - - /* - * Convert string of baseIn to an array of numbers of baseOut. - * Eg. toBaseOut('255', 10, 16) returns [15, 15]. - * Eg. toBaseOut('ff', 16, 10) returns [2, 5, 5]. - */ - function toBaseOut(str, baseIn, baseOut, alphabet) { - var j, - arr = [0], - arrL, - i = 0, - len = str.length; - - for (; i < len;) { - for (arrL = arr.length; arrL--; arr[arrL] *= baseIn); - - arr[0] += alphabet.indexOf(str.charAt(i++)); - - for (j = 0; j < arr.length; j++) { - - if (arr[j] > baseOut - 1) { - if (arr[j + 1] == null) arr[j + 1] = 0; - arr[j + 1] += arr[j] / baseOut | 0; - arr[j] %= baseOut; - } - } - } - - return arr.reverse(); - } - - // Convert a numeric string of baseIn to a numeric string of baseOut. - // If the caller is toString, we are converting from base 10 to baseOut. - // If the caller is BigNumber, we are converting from baseIn to base 10. - return function (str, baseIn, baseOut, sign, callerIsToString) { - var alphabet, d, e, k, r, x, xc, y, - i = str.indexOf('.'), - dp = DECIMAL_PLACES, - rm = ROUNDING_MODE; - - // Non-integer. - if (i >= 0) { - k = POW_PRECISION; - - // Unlimited precision. - POW_PRECISION = 0; - str = str.replace('.', ''); - y = new BigNumber(baseIn); - x = y.pow(str.length - i); - POW_PRECISION = k; - - // Convert str as if an integer, then restore the fraction part by dividing the - // result by its base raised to a power. - - y.c = toBaseOut(toFixedPoint(coeffToString(x.c), x.e, '0'), - 10, baseOut, decimal); - y.e = y.c.length; - } - - // Convert the number as integer. - - xc = toBaseOut(str, baseIn, baseOut, callerIsToString - ? (alphabet = ALPHABET, decimal) - : (alphabet = decimal, ALPHABET)); - - // xc now represents str as an integer and converted to baseOut. e is the exponent. - e = k = xc.length; - - // Remove trailing zeros. - for (; xc[--k] == 0; xc.pop()); - - // Zero? - if (!xc[0]) return alphabet.charAt(0); - - // Does str represent an integer? If so, no need for the division. - if (i < 0) { - --e; - } else { - x.c = xc; - x.e = e; - - // The sign is needed for correct rounding. - x.s = sign; - x = div(x, y, dp, rm, baseOut); - xc = x.c; - r = x.r; - e = x.e; - } - - // xc now represents str converted to baseOut. - - // THe index of the rounding digit. - d = e + dp + 1; - - // The rounding digit: the digit to the right of the digit that may be rounded up. - i = xc[d]; - - // Look at the rounding digits and mode to determine whether to round up. - - k = baseOut / 2; - r = r || d < 0 || xc[d + 1] != null; - - r = rm < 4 ? (i != null || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2)) - : i > k || i == k &&(rm == 4 || r || rm == 6 && xc[d - 1] & 1 || - rm == (x.s < 0 ? 8 : 7)); - - // If the index of the rounding digit is not greater than zero, or xc represents - // zero, then the result of the base conversion is zero or, if rounding up, a value - // such as 0.00001. - if (d < 1 || !xc[0]) { - - // 1^-dp or 0 - str = r ? toFixedPoint(alphabet.charAt(1), -dp, alphabet.charAt(0)) : alphabet.charAt(0); - } else { - - // Truncate xc to the required number of decimal places. - xc.length = d; - - // Round up? - if (r) { - - // Rounding up may mean the previous digit has to be rounded up and so on. - for (--baseOut; ++xc[--d] > baseOut;) { - xc[d] = 0; - - if (!d) { - ++e; - xc = [1].concat(xc); - } - } - } - - // Determine trailing zeros. - for (k = xc.length; !xc[--k];); - - // E.g. [4, 11, 15] becomes 4bf. - for (i = 0, str = ''; i <= k; str += alphabet.charAt(xc[i++])); - - // Add leading zeros, decimal point and trailing zeros as required. - str = toFixedPoint(str, e, alphabet.charAt(0)); - } - - // The caller will add the sign. - return str; - }; - })(); - - - // Perform division in the specified base. Called by div and convertBase. - div = (function () { - - // Assume non-zero x and k. - function multiply(x, k, base) { - var m, temp, xlo, xhi, - carry = 0, - i = x.length, - klo = k % SQRT_BASE, - khi = k / SQRT_BASE | 0; - - for (x = x.slice(); i--;) { - xlo = x[i] % SQRT_BASE; - xhi = x[i] / SQRT_BASE | 0; - m = khi * xlo + xhi * klo; - temp = klo * xlo + ((m % SQRT_BASE) * SQRT_BASE) + carry; - carry = (temp / base | 0) + (m / SQRT_BASE | 0) + khi * xhi; - x[i] = temp % base; - } - - if (carry) x = [carry].concat(x); - - return x; - } - - function compare(a, b, aL, bL) { - var i, cmp; - - if (aL != bL) { - cmp = aL > bL ? 1 : -1; - } else { - - for (i = cmp = 0; i < aL; i++) { - - if (a[i] != b[i]) { - cmp = a[i] > b[i] ? 1 : -1; - break; - } - } - } - - return cmp; - } - - function subtract(a, b, aL, base) { - var i = 0; - - // Subtract b from a. - for (; aL--;) { - a[aL] -= i; - i = a[aL] < b[aL] ? 1 : 0; - a[aL] = i * base + a[aL] - b[aL]; - } - - // Remove leading zeros. - for (; !a[0] && a.length > 1; a.splice(0, 1)); - } - - // x: dividend, y: divisor. - return function (x, y, dp, rm, base) { - var cmp, e, i, more, n, prod, prodL, q, qc, rem, remL, rem0, xi, xL, yc0, - yL, yz, - s = x.s == y.s ? 1 : -1, - xc = x.c, - yc = y.c; - - // Either NaN, Infinity or 0? - if (!xc || !xc[0] || !yc || !yc[0]) { - - return new BigNumber( - - // Return NaN if either NaN, or both Infinity or 0. - !x.s || !y.s || (xc ? yc && xc[0] == yc[0] : !yc) ? NaN : - - // Return ±0 if x is ±0 or y is ±Infinity, or return ±Infinity as y is ±0. - xc && xc[0] == 0 || !yc ? s * 0 : s / 0 - ); - } - - q = new BigNumber(s); - qc = q.c = []; - e = x.e - y.e; - s = dp + e + 1; - - if (!base) { - base = BASE; - e = bitFloor(x.e / LOG_BASE) - bitFloor(y.e / LOG_BASE); - s = s / LOG_BASE | 0; - } - - // Result exponent may be one less then the current value of e. - // The coefficients of the BigNumbers from convertBase may have trailing zeros. - for (i = 0; yc[i] == (xc[i] || 0); i++); - - if (yc[i] > (xc[i] || 0)) e--; - - if (s < 0) { - qc.push(1); - more = true; - } else { - xL = xc.length; - yL = yc.length; - i = 0; - s += 2; - - // Normalise xc and yc so highest order digit of yc is >= base / 2. - - n = mathfloor(base / (yc[0] + 1)); - - // Not necessary, but to handle odd bases where yc[0] == (base / 2) - 1. - // if (n > 1 || n++ == 1 && yc[0] < base / 2) { - if (n > 1) { - yc = multiply(yc, n, base); - xc = multiply(xc, n, base); - yL = yc.length; - xL = xc.length; - } - - xi = yL; - rem = xc.slice(0, yL); - remL = rem.length; - - // Add zeros to make remainder as long as divisor. - for (; remL < yL; rem[remL++] = 0); - yz = yc.slice(); - yz = [0].concat(yz); - yc0 = yc[0]; - if (yc[1] >= base / 2) yc0++; - // Not necessary, but to prevent trial digit n > base, when using base 3. - // else if (base == 3 && yc0 == 1) yc0 = 1 + 1e-15; - - do { - n = 0; - - // Compare divisor and remainder. - cmp = compare(yc, rem, yL, remL); - - // If divisor < remainder. - if (cmp < 0) { - - // Calculate trial digit, n. - - rem0 = rem[0]; - if (yL != remL) rem0 = rem0 * base + (rem[1] || 0); - - // n is how many times the divisor goes into the current remainder. - n = mathfloor(rem0 / yc0); - - // Algorithm: - // product = divisor multiplied by trial digit (n). - // Compare product and remainder. - // If product is greater than remainder: - // Subtract divisor from product, decrement trial digit. - // Subtract product from remainder. - // If product was less than remainder at the last compare: - // Compare new remainder and divisor. - // If remainder is greater than divisor: - // Subtract divisor from remainder, increment trial digit. - - if (n > 1) { - - // n may be > base only when base is 3. - if (n >= base) n = base - 1; - - // product = divisor * trial digit. - prod = multiply(yc, n, base); - prodL = prod.length; - remL = rem.length; - - // Compare product and remainder. - // If product > remainder then trial digit n too high. - // n is 1 too high about 5% of the time, and is not known to have - // ever been more than 1 too high. - while (compare(prod, rem, prodL, remL) == 1) { - n--; - - // Subtract divisor from product. - subtract(prod, yL < prodL ? yz : yc, prodL, base); - prodL = prod.length; - cmp = 1; - } - } else { - - // n is 0 or 1, cmp is -1. - // If n is 0, there is no need to compare yc and rem again below, - // so change cmp to 1 to avoid it. - // If n is 1, leave cmp as -1, so yc and rem are compared again. - if (n == 0) { - - // divisor < remainder, so n must be at least 1. - cmp = n = 1; - } - - // product = divisor - prod = yc.slice(); - prodL = prod.length; - } - - if (prodL < remL) prod = [0].concat(prod); - - // Subtract product from remainder. - subtract(rem, prod, remL, base); - remL = rem.length; - - // If product was < remainder. - if (cmp == -1) { - - // Compare divisor and new remainder. - // If divisor < new remainder, subtract divisor from remainder. - // Trial digit n too low. - // n is 1 too low about 5% of the time, and very rarely 2 too low. - while (compare(yc, rem, yL, remL) < 1) { - n++; - - // Subtract divisor from remainder. - subtract(rem, yL < remL ? yz : yc, remL, base); - remL = rem.length; - } - } - } else if (cmp === 0) { - n++; - rem = [0]; - } // else cmp === 1 and n will be 0 - - // Add the next digit, n, to the result array. - qc[i++] = n; - - // Update the remainder. - if (rem[0]) { - rem[remL++] = xc[xi] || 0; - } else { - rem = [xc[xi]]; - remL = 1; - } - } while ((xi++ < xL || rem[0] != null) && s--); - - more = rem[0] != null; - - // Leading zero? - if (!qc[0]) qc.splice(0, 1); - } - - if (base == BASE) { - - // To calculate q.e, first get the number of digits of qc[0]. - for (i = 1, s = qc[0]; s >= 10; s /= 10, i++); - - round(q, dp + (q.e = i + e * LOG_BASE - 1) + 1, rm, more); - - // Caller is convertBase. - } else { - q.e = e; - q.r = +more; - } - - return q; - }; - })(); - - - /* - * Return a string representing the value of BigNumber n in fixed-point or exponential - * notation rounded to the specified decimal places or significant digits. - * - * n: a BigNumber. - * i: the index of the last digit required (i.e. the digit that may be rounded up). - * rm: the rounding mode. - * id: 1 (toExponential) or 2 (toPrecision). - */ - function format(n, i, rm, id) { - var c0, e, ne, len, str; - - if (rm == null) rm = ROUNDING_MODE; - else intCheck(rm, 0, 8); - - if (!n.c) return n.toString(); - - c0 = n.c[0]; - ne = n.e; - - if (i == null) { - str = coeffToString(n.c); - str = id == 1 || id == 2 && (ne <= TO_EXP_NEG || ne >= TO_EXP_POS) - ? toExponential(str, ne) - : toFixedPoint(str, ne, '0'); - } else { - n = round(new BigNumber(n), i, rm); - - // n.e may have changed if the value was rounded up. - e = n.e; - - str = coeffToString(n.c); - len = str.length; - - // toPrecision returns exponential notation if the number of significant digits - // specified is less than the number of digits necessary to represent the integer - // part of the value in fixed-point notation. - - // Exponential notation. - if (id == 1 || id == 2 && (i <= e || e <= TO_EXP_NEG)) { - - // Append zeros? - for (; len < i; str += '0', len++); - str = toExponential(str, e); - - // Fixed-point notation. - } else { - i -= ne; - str = toFixedPoint(str, e, '0'); - - // Append zeros? - if (e + 1 > len) { - if (--i > 0) for (str += '.'; i--; str += '0'); - } else { - i += e - len; - if (i > 0) { - if (e + 1 == len) str += '.'; - for (; i--; str += '0'); - } - } - } - } - - return n.s < 0 && c0 ? '-' + str : str; - } - - - // Handle BigNumber.max and BigNumber.min. - // If any number is NaN, return NaN. - function maxOrMin(args, n) { - var k, y, - i = 1, - x = new BigNumber(args[0]); - - for (; i < args.length; i++) { - y = new BigNumber(args[i]); - if (!y.s || (k = compare(x, y)) === n || k === 0 && x.s === n) { - x = y; - } - } - - return x; - } - - - /* - * Strip trailing zeros, calculate base 10 exponent and check against MIN_EXP and MAX_EXP. - * Called by minus, plus and times. - */ - function normalise(n, c, e) { - var i = 1, - j = c.length; - - // Remove trailing zeros. - for (; !c[--j]; c.pop()); - - // Calculate the base 10 exponent. First get the number of digits of c[0]. - for (j = c[0]; j >= 10; j /= 10, i++); - - // Overflow? - if ((e = i + e * LOG_BASE - 1) > MAX_EXP) { - - // Infinity. - n.c = n.e = null; - - // Underflow? - } else if (e < MIN_EXP) { - - // Zero. - n.c = [n.e = 0]; - } else { - n.e = e; - n.c = c; - } - - return n; - } - - - // Handle values that fail the validity test in BigNumber. - parseNumeric = (function () { - var basePrefix = /^(-?)0([xbo])(?=\w[\w.]*$)/i, - dotAfter = /^([^.]+)\.$/, - dotBefore = /^\.([^.]+)$/, - isInfinityOrNaN = /^-?(Infinity|NaN)$/, - whitespaceOrPlus = /^\s*\+(?=[\w.])|^\s+|\s+$/g; - - return function (x, str, isNum, b) { - var base, - s = isNum ? str : str.replace(whitespaceOrPlus, ''); - - // No exception on ±Infinity or NaN. - if (isInfinityOrNaN.test(s)) { - x.s = isNaN(s) ? null : s < 0 ? -1 : 1; - } else { - if (!isNum) { - - // basePrefix = /^(-?)0([xbo])(?=\w[\w.]*$)/i - s = s.replace(basePrefix, function (m, p1, p2) { - base = (p2 = p2.toLowerCase()) == 'x' ? 16 : p2 == 'b' ? 2 : 8; - return !b || b == base ? p1 : m; - }); - - if (b) { - base = b; - - // E.g. '1.' to '1', '.1' to '0.1' - s = s.replace(dotAfter, '$1').replace(dotBefore, '0.$1'); - } - - if (str != s) return new BigNumber(s, base); - } - - // '[BigNumber Error] Not a number: {n}' - // '[BigNumber Error] Not a base {b} number: {n}' - if (BigNumber.DEBUG) { - throw Error - (bignumberError + 'Not a' + (b ? ' base ' + b : '') + ' number: ' + str); - } - - // NaN - x.s = null; - } - - x.c = x.e = null; - } - })(); - - - /* - * Round x to sd significant digits using rounding mode rm. Check for over/under-flow. - * If r is truthy, it is known that there are more digits after the rounding digit. - */ - function round(x, sd, rm, r) { - var d, i, j, k, n, ni, rd, - xc = x.c, - pows10 = POWS_TEN; - - // if x is not Infinity or NaN... - if (xc) { - - // rd is the rounding digit, i.e. the digit after the digit that may be rounded up. - // n is a base 1e14 number, the value of the element of array x.c containing rd. - // ni is the index of n within x.c. - // d is the number of digits of n. - // i is the index of rd within n including leading zeros. - // j is the actual index of rd within n (if < 0, rd is a leading zero). - out: { - - // Get the number of digits of the first element of xc. - for (d = 1, k = xc[0]; k >= 10; k /= 10, d++); - i = sd - d; - - // If the rounding digit is in the first element of xc... - if (i < 0) { - i += LOG_BASE; - j = sd; - n = xc[ni = 0]; - - // Get the rounding digit at index j of n. - rd = mathfloor(n / pows10[d - j - 1] % 10); - } else { - ni = mathceil((i + 1) / LOG_BASE); - - if (ni >= xc.length) { - - if (r) { - - // Needed by sqrt. - for (; xc.length <= ni; xc.push(0)); - n = rd = 0; - d = 1; - i %= LOG_BASE; - j = i - LOG_BASE + 1; - } else { - break out; - } - } else { - n = k = xc[ni]; - - // Get the number of digits of n. - for (d = 1; k >= 10; k /= 10, d++); - - // Get the index of rd within n. - i %= LOG_BASE; - - // Get the index of rd within n, adjusted for leading zeros. - // The number of leading zeros of n is given by LOG_BASE - d. - j = i - LOG_BASE + d; - - // Get the rounding digit at index j of n. - rd = j < 0 ? 0 : mathfloor(n / pows10[d - j - 1] % 10); - } - } - - r = r || sd < 0 || - - // Are there any non-zero digits after the rounding digit? - // The expression n % pows10[d - j - 1] returns all digits of n to the right - // of the digit at j, e.g. if n is 908714 and j is 2, the expression gives 714. - xc[ni + 1] != null || (j < 0 ? n : n % pows10[d - j - 1]); - - r = rm < 4 - ? (rd || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2)) - : rd > 5 || rd == 5 && (rm == 4 || r || rm == 6 && - - // Check whether the digit to the left of the rounding digit is odd. - ((i > 0 ? j > 0 ? n / pows10[d - j] : 0 : xc[ni - 1]) % 10) & 1 || - rm == (x.s < 0 ? 8 : 7)); - - if (sd < 1 || !xc[0]) { - xc.length = 0; - - if (r) { - - // Convert sd to decimal places. - sd -= x.e + 1; - - // 1, 0.1, 0.01, 0.001, 0.0001 etc. - xc[0] = pows10[(LOG_BASE - sd % LOG_BASE) % LOG_BASE]; - x.e = -sd || 0; - } else { - - // Zero. - xc[0] = x.e = 0; - } - - return x; - } - - // Remove excess digits. - if (i == 0) { - xc.length = ni; - k = 1; - ni--; - } else { - xc.length = ni + 1; - k = pows10[LOG_BASE - i]; - - // E.g. 56700 becomes 56000 if 7 is the rounding digit. - // j > 0 means i > number of leading zeros of n. - xc[ni] = j > 0 ? mathfloor(n / pows10[d - j] % pows10[j]) * k : 0; - } - - // Round up? - if (r) { - - for (; ;) { - - // If the digit to be rounded up is in the first element of xc... - if (ni == 0) { - - // i will be the length of xc[0] before k is added. - for (i = 1, j = xc[0]; j >= 10; j /= 10, i++); - j = xc[0] += k; - for (k = 1; j >= 10; j /= 10, k++); - - // if i != k the length has increased. - if (i != k) { - x.e++; - if (xc[0] == BASE) xc[0] = 1; - } - - break; - } else { - xc[ni] += k; - if (xc[ni] != BASE) break; - xc[ni--] = 0; - k = 1; - } - } - } - - // Remove trailing zeros. - for (i = xc.length; xc[--i] === 0; xc.pop()); - } - - // Overflow? Infinity. - if (x.e > MAX_EXP) { - x.c = x.e = null; - - // Underflow? Zero. - } else if (x.e < MIN_EXP) { - x.c = [x.e = 0]; - } - } - - return x; - } - - - function valueOf(n) { - var str, - e = n.e; - - if (e === null) return n.toString(); - - str = coeffToString(n.c); - - str = e <= TO_EXP_NEG || e >= TO_EXP_POS - ? toExponential(str, e) - : toFixedPoint(str, e, '0'); - - return n.s < 0 ? '-' + str : str; - } - - - // PROTOTYPE/INSTANCE METHODS - - - /* - * Return a new BigNumber whose value is the absolute value of this BigNumber. - */ - P.absoluteValue = P.abs = function () { - var x = new BigNumber(this); - if (x.s < 0) x.s = 1; - return x; - }; - - - /* - * Return - * 1 if the value of this BigNumber is greater than the value of BigNumber(y, b), - * -1 if the value of this BigNumber is less than the value of BigNumber(y, b), - * 0 if they have the same value, - * or null if the value of either is NaN. - */ - P.comparedTo = function (y, b) { - return compare(this, new BigNumber(y, b)); - }; - - - /* - * If dp is undefined or null or true or false, return the number of decimal places of the - * value of this BigNumber, or null if the value of this BigNumber is ±Infinity or NaN. - * - * Otherwise, if dp is a number, return a new BigNumber whose value is the value of this - * BigNumber rounded to a maximum of dp decimal places using rounding mode rm, or - * ROUNDING_MODE if rm is omitted. - * - * [dp] {number} Decimal places: integer, 0 to MAX inclusive. - * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' - */ - P.decimalPlaces = P.dp = function (dp, rm) { - var c, n, v, - x = this; - - if (dp != null) { - intCheck(dp, 0, MAX); - if (rm == null) rm = ROUNDING_MODE; - else intCheck(rm, 0, 8); - - return round(new BigNumber(x), dp + x.e + 1, rm); - } - - if (!(c = x.c)) return null; - n = ((v = c.length - 1) - bitFloor(this.e / LOG_BASE)) * LOG_BASE; - - // Subtract the number of trailing zeros of the last number. - if (v = c[v]) for (; v % 10 == 0; v /= 10, n--); - if (n < 0) n = 0; - - return n; - }; - - - /* - * n / 0 = I - * n / N = N - * n / I = 0 - * 0 / n = 0 - * 0 / 0 = N - * 0 / N = N - * 0 / I = 0 - * N / n = N - * N / 0 = N - * N / N = N - * N / I = N - * I / n = I - * I / 0 = I - * I / N = N - * I / I = N - * - * Return a new BigNumber whose value is the value of this BigNumber divided by the value of - * BigNumber(y, b), rounded according to DECIMAL_PLACES and ROUNDING_MODE. - */ - P.dividedBy = P.div = function (y, b) { - return div(this, new BigNumber(y, b), DECIMAL_PLACES, ROUNDING_MODE); - }; - - - /* - * Return a new BigNumber whose value is the integer part of dividing the value of this - * BigNumber by the value of BigNumber(y, b). - */ - P.dividedToIntegerBy = P.idiv = function (y, b) { - return div(this, new BigNumber(y, b), 0, 1); - }; - - - /* - * Return a BigNumber whose value is the value of this BigNumber exponentiated by n. - * - * If m is present, return the result modulo m. - * If n is negative round according to DECIMAL_PLACES and ROUNDING_MODE. - * If POW_PRECISION is non-zero and m is not present, round to POW_PRECISION using ROUNDING_MODE. - * - * The modular power operation works efficiently when x, n, and m are integers, otherwise it - * is equivalent to calculating x.exponentiatedBy(n).modulo(m) with a POW_PRECISION of 0. - * - * n {number|string|BigNumber} The exponent. An integer. - * [m] {number|string|BigNumber} The modulus. - * - * '[BigNumber Error] Exponent not an integer: {n}' - */ - P.exponentiatedBy = P.pow = function (n, m) { - var half, isModExp, i, k, more, nIsBig, nIsNeg, nIsOdd, y, - x = this; - - n = new BigNumber(n); - - // Allow NaN and ±Infinity, but not other non-integers. - if (n.c && !n.isInteger()) { - throw Error - (bignumberError + 'Exponent not an integer: ' + valueOf(n)); - } - - if (m != null) m = new BigNumber(m); - - // Exponent of MAX_SAFE_INTEGER is 15. - nIsBig = n.e > 14; - - // If x is NaN, ±Infinity, ±0 or ±1, or n is ±Infinity, NaN or ±0. - if (!x.c || !x.c[0] || x.c[0] == 1 && !x.e && x.c.length == 1 || !n.c || !n.c[0]) { - - // The sign of the result of pow when x is negative depends on the evenness of n. - // If +n overflows to ±Infinity, the evenness of n would be not be known. - y = new BigNumber(Math.pow(+valueOf(x), nIsBig ? n.s * (2 - isOdd(n)) : +valueOf(n))); - return m ? y.mod(m) : y; - } - - nIsNeg = n.s < 0; - - if (m) { - - // x % m returns NaN if abs(m) is zero, or m is NaN. - if (m.c ? !m.c[0] : !m.s) return new BigNumber(NaN); - - isModExp = !nIsNeg && x.isInteger() && m.isInteger(); - - if (isModExp) x = x.mod(m); - - // Overflow to ±Infinity: >=2**1e10 or >=1.0000024**1e15. - // Underflow to ±0: <=0.79**1e10 or <=0.9999975**1e15. - } else if (n.e > 9 && (x.e > 0 || x.e < -1 || (x.e == 0 - // [1, 240000000] - ? x.c[0] > 1 || nIsBig && x.c[1] >= 24e7 - // [80000000000000] [99999750000000] - : x.c[0] < 8e13 || nIsBig && x.c[0] <= 9999975e7))) { - - // If x is negative and n is odd, k = -0, else k = 0. - k = x.s < 0 && isOdd(n) ? -0 : 0; - - // If x >= 1, k = ±Infinity. - if (x.e > -1) k = 1 / k; - - // If n is negative return ±0, else return ±Infinity. - return new BigNumber(nIsNeg ? 1 / k : k); - - } else if (POW_PRECISION) { - - // Truncating each coefficient array to a length of k after each multiplication - // equates to truncating significant digits to POW_PRECISION + [28, 41], - // i.e. there will be a minimum of 28 guard digits retained. - k = mathceil(POW_PRECISION / LOG_BASE + 2); - } - - if (nIsBig) { - half = new BigNumber(0.5); - if (nIsNeg) n.s = 1; - nIsOdd = isOdd(n); - } else { - i = Math.abs(+valueOf(n)); - nIsOdd = i % 2; - } - - y = new BigNumber(ONE); - - // Performs 54 loop iterations for n of 9007199254740991. - for (; ;) { - - if (nIsOdd) { - y = y.times(x); - if (!y.c) break; - - if (k) { - if (y.c.length > k) y.c.length = k; - } else if (isModExp) { - y = y.mod(m); //y = y.minus(div(y, m, 0, MODULO_MODE).times(m)); - } - } - - if (i) { - i = mathfloor(i / 2); - if (i === 0) break; - nIsOdd = i % 2; - } else { - n = n.times(half); - round(n, n.e + 1, 1); - - if (n.e > 14) { - nIsOdd = isOdd(n); - } else { - i = +valueOf(n); - if (i === 0) break; - nIsOdd = i % 2; - } - } - - x = x.times(x); - - if (k) { - if (x.c && x.c.length > k) x.c.length = k; - } else if (isModExp) { - x = x.mod(m); //x = x.minus(div(x, m, 0, MODULO_MODE).times(m)); - } - } - - if (isModExp) return y; - if (nIsNeg) y = ONE.div(y); - - return m ? y.mod(m) : k ? round(y, POW_PRECISION, ROUNDING_MODE, more) : y; - }; - - - /* - * Return a new BigNumber whose value is the value of this BigNumber rounded to an integer - * using rounding mode rm, or ROUNDING_MODE if rm is omitted. - * - * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {rm}' - */ - P.integerValue = function (rm) { - var n = new BigNumber(this); - if (rm == null) rm = ROUNDING_MODE; - else intCheck(rm, 0, 8); - return round(n, n.e + 1, rm); - }; - - - /* - * Return true if the value of this BigNumber is equal to the value of BigNumber(y, b), - * otherwise return false. - */ - P.isEqualTo = P.eq = function (y, b) { - return compare(this, new BigNumber(y, b)) === 0; - }; - - - /* - * Return true if the value of this BigNumber is a finite number, otherwise return false. - */ - P.isFinite = function () { - return !!this.c; - }; - - - /* - * Return true if the value of this BigNumber is greater than the value of BigNumber(y, b), - * otherwise return false. - */ - P.isGreaterThan = P.gt = function (y, b) { - return compare(this, new BigNumber(y, b)) > 0; - }; - - - /* - * Return true if the value of this BigNumber is greater than or equal to the value of - * BigNumber(y, b), otherwise return false. - */ - P.isGreaterThanOrEqualTo = P.gte = function (y, b) { - return (b = compare(this, new BigNumber(y, b))) === 1 || b === 0; - - }; - - - /* - * Return true if the value of this BigNumber is an integer, otherwise return false. - */ - P.isInteger = function () { - return !!this.c && bitFloor(this.e / LOG_BASE) > this.c.length - 2; - }; - - - /* - * Return true if the value of this BigNumber is less than the value of BigNumber(y, b), - * otherwise return false. - */ - P.isLessThan = P.lt = function (y, b) { - return compare(this, new BigNumber(y, b)) < 0; - }; - - - /* - * Return true if the value of this BigNumber is less than or equal to the value of - * BigNumber(y, b), otherwise return false. - */ - P.isLessThanOrEqualTo = P.lte = function (y, b) { - return (b = compare(this, new BigNumber(y, b))) === -1 || b === 0; - }; - - - /* - * Return true if the value of this BigNumber is NaN, otherwise return false. - */ - P.isNaN = function () { - return !this.s; - }; - - - /* - * Return true if the value of this BigNumber is negative, otherwise return false. - */ - P.isNegative = function () { - return this.s < 0; - }; - - - /* - * Return true if the value of this BigNumber is positive, otherwise return false. - */ - P.isPositive = function () { - return this.s > 0; - }; - - - /* - * Return true if the value of this BigNumber is 0 or -0, otherwise return false. - */ - P.isZero = function () { - return !!this.c && this.c[0] == 0; - }; - - - /* - * n - 0 = n - * n - N = N - * n - I = -I - * 0 - n = -n - * 0 - 0 = 0 - * 0 - N = N - * 0 - I = -I - * N - n = N - * N - 0 = N - * N - N = N - * N - I = N - * I - n = I - * I - 0 = I - * I - N = N - * I - I = N - * - * Return a new BigNumber whose value is the value of this BigNumber minus the value of - * BigNumber(y, b). - */ - P.minus = function (y, b) { - var i, j, t, xLTy, - x = this, - a = x.s; - - y = new BigNumber(y, b); - b = y.s; - - // Either NaN? - if (!a || !b) return new BigNumber(NaN); - - // Signs differ? - if (a != b) { - y.s = -b; - return x.plus(y); - } - - var xe = x.e / LOG_BASE, - ye = y.e / LOG_BASE, - xc = x.c, - yc = y.c; - - if (!xe || !ye) { - - // Either Infinity? - if (!xc || !yc) return xc ? (y.s = -b, y) : new BigNumber(yc ? x : NaN); - - // Either zero? - if (!xc[0] || !yc[0]) { - - // Return y if y is non-zero, x if x is non-zero, or zero if both are zero. - return yc[0] ? (y.s = -b, y) : new BigNumber(xc[0] ? x : - - // IEEE 754 (2008) 6.3: n - n = -0 when rounding to -Infinity - ROUNDING_MODE == 3 ? -0 : 0); - } - } - - xe = bitFloor(xe); - ye = bitFloor(ye); - xc = xc.slice(); - - // Determine which is the bigger number. - if (a = xe - ye) { - - if (xLTy = a < 0) { - a = -a; - t = xc; - } else { - ye = xe; - t = yc; - } - - t.reverse(); - - // Prepend zeros to equalise exponents. - for (b = a; b--; t.push(0)); - t.reverse(); - } else { - - // Exponents equal. Check digit by digit. - j = (xLTy = (a = xc.length) < (b = yc.length)) ? a : b; - - for (a = b = 0; b < j; b++) { - - if (xc[b] != yc[b]) { - xLTy = xc[b] < yc[b]; - break; - } - } - } - - // x < y? Point xc to the array of the bigger number. - if (xLTy) { - t = xc; - xc = yc; - yc = t; - y.s = -y.s; - } - - b = (j = yc.length) - (i = xc.length); - - // Append zeros to xc if shorter. - // No need to add zeros to yc if shorter as subtract only needs to start at yc.length. - if (b > 0) for (; b--; xc[i++] = 0); - b = BASE - 1; - - // Subtract yc from xc. - for (; j > a;) { - - if (xc[--j] < yc[j]) { - for (i = j; i && !xc[--i]; xc[i] = b); - --xc[i]; - xc[j] += BASE; - } - - xc[j] -= yc[j]; - } - - // Remove leading zeros and adjust exponent accordingly. - for (; xc[0] == 0; xc.splice(0, 1), --ye); - - // Zero? - if (!xc[0]) { - - // Following IEEE 754 (2008) 6.3, - // n - n = +0 but n - n = -0 when rounding towards -Infinity. - y.s = ROUNDING_MODE == 3 ? -1 : 1; - y.c = [y.e = 0]; - return y; - } - - // No need to check for Infinity as +x - +y != Infinity && -x - -y != Infinity - // for finite x and y. - return normalise(y, xc, ye); - }; - - - /* - * n % 0 = N - * n % N = N - * n % I = n - * 0 % n = 0 - * -0 % n = -0 - * 0 % 0 = N - * 0 % N = N - * 0 % I = 0 - * N % n = N - * N % 0 = N - * N % N = N - * N % I = N - * I % n = N - * I % 0 = N - * I % N = N - * I % I = N - * - * Return a new BigNumber whose value is the value of this BigNumber modulo the value of - * BigNumber(y, b). The result depends on the value of MODULO_MODE. - */ - P.modulo = P.mod = function (y, b) { - var q, s, - x = this; - - y = new BigNumber(y, b); - - // Return NaN if x is Infinity or NaN, or y is NaN or zero. - if (!x.c || !y.s || y.c && !y.c[0]) { - return new BigNumber(NaN); - - // Return x if y is Infinity or x is zero. - } else if (!y.c || x.c && !x.c[0]) { - return new BigNumber(x); - } - - if (MODULO_MODE == 9) { - - // Euclidian division: q = sign(y) * floor(x / abs(y)) - // r = x - qy where 0 <= r < abs(y) - s = y.s; - y.s = 1; - q = div(x, y, 0, 3); - y.s = s; - q.s *= s; - } else { - q = div(x, y, 0, MODULO_MODE); - } - - y = x.minus(q.times(y)); - - // To match JavaScript %, ensure sign of zero is sign of dividend. - if (!y.c[0] && MODULO_MODE == 1) y.s = x.s; - - return y; - }; - - - /* - * n * 0 = 0 - * n * N = N - * n * I = I - * 0 * n = 0 - * 0 * 0 = 0 - * 0 * N = N - * 0 * I = N - * N * n = N - * N * 0 = N - * N * N = N - * N * I = N - * I * n = I - * I * 0 = N - * I * N = N - * I * I = I - * - * Return a new BigNumber whose value is the value of this BigNumber multiplied by the value - * of BigNumber(y, b). - */ - P.multipliedBy = P.times = function (y, b) { - var c, e, i, j, k, m, xcL, xlo, xhi, ycL, ylo, yhi, zc, - base, sqrtBase, - x = this, - xc = x.c, - yc = (y = new BigNumber(y, b)).c; - - // Either NaN, ±Infinity or ±0? - if (!xc || !yc || !xc[0] || !yc[0]) { - - // Return NaN if either is NaN, or one is 0 and the other is Infinity. - if (!x.s || !y.s || xc && !xc[0] && !yc || yc && !yc[0] && !xc) { - y.c = y.e = y.s = null; - } else { - y.s *= x.s; - - // Return ±Infinity if either is ±Infinity. - if (!xc || !yc) { - y.c = y.e = null; - - // Return ±0 if either is ±0. - } else { - y.c = [0]; - y.e = 0; - } - } - - return y; - } - - e = bitFloor(x.e / LOG_BASE) + bitFloor(y.e / LOG_BASE); - y.s *= x.s; - xcL = xc.length; - ycL = yc.length; - - // Ensure xc points to longer array and xcL to its length. - if (xcL < ycL) { - zc = xc; - xc = yc; - yc = zc; - i = xcL; - xcL = ycL; - ycL = i; - } - - // Initialise the result array with zeros. - for (i = xcL + ycL, zc = []; i--; zc.push(0)); - - base = BASE; - sqrtBase = SQRT_BASE; - - for (i = ycL; --i >= 0;) { - c = 0; - ylo = yc[i] % sqrtBase; - yhi = yc[i] / sqrtBase | 0; - - for (k = xcL, j = i + k; j > i;) { - xlo = xc[--k] % sqrtBase; - xhi = xc[k] / sqrtBase | 0; - m = yhi * xlo + xhi * ylo; - xlo = ylo * xlo + ((m % sqrtBase) * sqrtBase) + zc[j] + c; - c = (xlo / base | 0) + (m / sqrtBase | 0) + yhi * xhi; - zc[j--] = xlo % base; - } - - zc[j] = c; - } - - if (c) { - ++e; - } else { - zc.splice(0, 1); - } - - return normalise(y, zc, e); - }; - - - /* - * Return a new BigNumber whose value is the value of this BigNumber negated, - * i.e. multiplied by -1. - */ - P.negated = function () { - var x = new BigNumber(this); - x.s = -x.s || null; - return x; - }; - - - /* - * n + 0 = n - * n + N = N - * n + I = I - * 0 + n = n - * 0 + 0 = 0 - * 0 + N = N - * 0 + I = I - * N + n = N - * N + 0 = N - * N + N = N - * N + I = N - * I + n = I - * I + 0 = I - * I + N = N - * I + I = I - * - * Return a new BigNumber whose value is the value of this BigNumber plus the value of - * BigNumber(y, b). - */ - P.plus = function (y, b) { - var t, - x = this, - a = x.s; - - y = new BigNumber(y, b); - b = y.s; - - // Either NaN? - if (!a || !b) return new BigNumber(NaN); - - // Signs differ? - if (a != b) { - y.s = -b; - return x.minus(y); - } - - var xe = x.e / LOG_BASE, - ye = y.e / LOG_BASE, - xc = x.c, - yc = y.c; - - if (!xe || !ye) { - - // Return ±Infinity if either ±Infinity. - if (!xc || !yc) return new BigNumber(a / 0); - - // Either zero? - // Return y if y is non-zero, x if x is non-zero, or zero if both are zero. - if (!xc[0] || !yc[0]) return yc[0] ? y : new BigNumber(xc[0] ? x : a * 0); - } - - xe = bitFloor(xe); - ye = bitFloor(ye); - xc = xc.slice(); - - // Prepend zeros to equalise exponents. Faster to use reverse then do unshifts. - if (a = xe - ye) { - if (a > 0) { - ye = xe; - t = yc; - } else { - a = -a; - t = xc; - } - - t.reverse(); - for (; a--; t.push(0)); - t.reverse(); - } - - a = xc.length; - b = yc.length; - - // Point xc to the longer array, and b to the shorter length. - if (a - b < 0) { - t = yc; - yc = xc; - xc = t; - b = a; - } - - // Only start adding at yc.length - 1 as the further digits of xc can be ignored. - for (a = 0; b;) { - a = (xc[--b] = xc[b] + yc[b] + a) / BASE | 0; - xc[b] = BASE === xc[b] ? 0 : xc[b] % BASE; - } - - if (a) { - xc = [a].concat(xc); - ++ye; - } - - // No need to check for zero, as +x + +y != 0 && -x + -y != 0 - // ye = MAX_EXP + 1 possible - return normalise(y, xc, ye); - }; - - - /* - * If sd is undefined or null or true or false, return the number of significant digits of - * the value of this BigNumber, or null if the value of this BigNumber is ±Infinity or NaN. - * If sd is true include integer-part trailing zeros in the count. - * - * Otherwise, if sd is a number, return a new BigNumber whose value is the value of this - * BigNumber rounded to a maximum of sd significant digits using rounding mode rm, or - * ROUNDING_MODE if rm is omitted. - * - * sd {number|boolean} number: significant digits: integer, 1 to MAX inclusive. - * boolean: whether to count integer-part trailing zeros: true or false. - * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}' - */ - P.precision = P.sd = function (sd, rm) { - var c, n, v, - x = this; - - if (sd != null && sd !== !!sd) { - intCheck(sd, 1, MAX); - if (rm == null) rm = ROUNDING_MODE; - else intCheck(rm, 0, 8); - - return round(new BigNumber(x), sd, rm); - } - - if (!(c = x.c)) return null; - v = c.length - 1; - n = v * LOG_BASE + 1; - - if (v = c[v]) { - - // Subtract the number of trailing zeros of the last element. - for (; v % 10 == 0; v /= 10, n--); - - // Add the number of digits of the first element. - for (v = c[0]; v >= 10; v /= 10, n++); - } - - if (sd && x.e + 1 > n) n = x.e + 1; - - return n; - }; - - - /* - * Return a new BigNumber whose value is the value of this BigNumber shifted by k places - * (powers of 10). Shift to the right if n > 0, and to the left if n < 0. - * - * k {number} Integer, -MAX_SAFE_INTEGER to MAX_SAFE_INTEGER inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {k}' - */ - P.shiftedBy = function (k) { - intCheck(k, -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER); - return this.times('1e' + k); - }; - - - /* - * sqrt(-n) = N - * sqrt(N) = N - * sqrt(-I) = N - * sqrt(I) = I - * sqrt(0) = 0 - * sqrt(-0) = -0 - * - * Return a new BigNumber whose value is the square root of the value of this BigNumber, - * rounded according to DECIMAL_PLACES and ROUNDING_MODE. - */ - P.squareRoot = P.sqrt = function () { - var m, n, r, rep, t, - x = this, - c = x.c, - s = x.s, - e = x.e, - dp = DECIMAL_PLACES + 4, - half = new BigNumber('0.5'); - - // Negative/NaN/Infinity/zero? - if (s !== 1 || !c || !c[0]) { - return new BigNumber(!s || s < 0 && (!c || c[0]) ? NaN : c ? x : 1 / 0); - } - - // Initial estimate. - s = Math.sqrt(+valueOf(x)); - - // Math.sqrt underflow/overflow? - // Pass x to Math.sqrt as integer, then adjust the exponent of the result. - if (s == 0 || s == 1 / 0) { - n = coeffToString(c); - if ((n.length + e) % 2 == 0) n += '0'; - s = Math.sqrt(+n); - e = bitFloor((e + 1) / 2) - (e < 0 || e % 2); - - if (s == 1 / 0) { - n = '5e' + e; - } else { - n = s.toExponential(); - n = n.slice(0, n.indexOf('e') + 1) + e; - } - - r = new BigNumber(n); - } else { - r = new BigNumber(s + ''); - } - - // Check for zero. - // r could be zero if MIN_EXP is changed after the this value was created. - // This would cause a division by zero (x/t) and hence Infinity below, which would cause - // coeffToString to throw. - if (r.c[0]) { - e = r.e; - s = e + dp; - if (s < 3) s = 0; - - // Newton-Raphson iteration. - for (; ;) { - t = r; - r = half.times(t.plus(div(x, t, dp, 1))); - - if (coeffToString(t.c).slice(0, s) === (n = coeffToString(r.c)).slice(0, s)) { - - // The exponent of r may here be one less than the final result exponent, - // e.g 0.0009999 (e-4) --> 0.001 (e-3), so adjust s so the rounding digits - // are indexed correctly. - if (r.e < e) --s; - n = n.slice(s - 3, s + 1); - - // The 4th rounding digit may be in error by -1 so if the 4 rounding digits - // are 9999 or 4999 (i.e. approaching a rounding boundary) continue the - // iteration. - if (n == '9999' || !rep && n == '4999') { - - // On the first iteration only, check to see if rounding up gives the - // exact result as the nines may infinitely repeat. - if (!rep) { - round(t, t.e + DECIMAL_PLACES + 2, 0); - - if (t.times(t).eq(x)) { - r = t; - break; - } - } - - dp += 4; - s += 4; - rep = 1; - } else { - - // If rounding digits are null, 0{0,4} or 50{0,3}, check for exact - // result. If not, then there are further digits and m will be truthy. - if (!+n || !+n.slice(1) && n.charAt(0) == '5') { - - // Truncate to the first rounding digit. - round(r, r.e + DECIMAL_PLACES + 2, 1); - m = !r.times(r).eq(x); - } - - break; - } - } - } - } - - return round(r, r.e + DECIMAL_PLACES + 1, ROUNDING_MODE, m); - }; - - - /* - * Return a string representing the value of this BigNumber in exponential notation and - * rounded using ROUNDING_MODE to dp fixed decimal places. - * - * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. - * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' - */ - P.toExponential = function (dp, rm) { - if (dp != null) { - intCheck(dp, 0, MAX); - dp++; - } - return format(this, dp, rm, 1); - }; - - - /* - * Return a string representing the value of this BigNumber in fixed-point notation rounding - * to dp fixed decimal places using rounding mode rm, or ROUNDING_MODE if rm is omitted. - * - * Note: as with JavaScript's number type, (-0).toFixed(0) is '0', - * but e.g. (-0.00001).toFixed(0) is '-0'. - * - * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. - * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' - */ - P.toFixed = function (dp, rm) { - if (dp != null) { - intCheck(dp, 0, MAX); - dp = dp + this.e + 1; - } - return format(this, dp, rm); - }; - - - /* - * Return a string representing the value of this BigNumber in fixed-point notation rounded - * using rm or ROUNDING_MODE to dp decimal places, and formatted according to the properties - * of the format or FORMAT object (see BigNumber.set). - * - * The formatting object may contain some or all of the properties shown below. - * - * FORMAT = { - * prefix: '', - * groupSize: 3, - * secondaryGroupSize: 0, - * groupSeparator: ',', - * decimalSeparator: '.', - * fractionGroupSize: 0, - * fractionGroupSeparator: '\xA0', // non-breaking space - * suffix: '' - * }; - * - * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. - * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. - * [format] {object} Formatting options. See FORMAT pbject above. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' - * '[BigNumber Error] Argument not an object: {format}' - */ - P.toFormat = function (dp, rm, format) { - var str, - x = this; - - if (format == null) { - if (dp != null && rm && typeof rm == 'object') { - format = rm; - rm = null; - } else if (dp && typeof dp == 'object') { - format = dp; - dp = rm = null; - } else { - format = FORMAT; - } - } else if (typeof format != 'object') { - throw Error - (bignumberError + 'Argument not an object: ' + format); - } - - str = x.toFixed(dp, rm); - - if (x.c) { - var i, - arr = str.split('.'), - g1 = +format.groupSize, - g2 = +format.secondaryGroupSize, - groupSeparator = format.groupSeparator || '', - intPart = arr[0], - fractionPart = arr[1], - isNeg = x.s < 0, - intDigits = isNeg ? intPart.slice(1) : intPart, - len = intDigits.length; - - if (g2) { - i = g1; - g1 = g2; - g2 = i; - len -= i; - } - - if (g1 > 0 && len > 0) { - i = len % g1 || g1; - intPart = intDigits.substr(0, i); - for (; i < len; i += g1) intPart += groupSeparator + intDigits.substr(i, g1); - if (g2 > 0) intPart += groupSeparator + intDigits.slice(i); - if (isNeg) intPart = '-' + intPart; - } - - str = fractionPart - ? intPart + (format.decimalSeparator || '') + ((g2 = +format.fractionGroupSize) - ? fractionPart.replace(new RegExp('\\d{' + g2 + '}\\B', 'g'), - '$&' + (format.fractionGroupSeparator || '')) - : fractionPart) - : intPart; - } - - return (format.prefix || '') + str + (format.suffix || ''); - }; - - - /* - * Return an array of two BigNumbers representing the value of this BigNumber as a simple - * fraction with an integer numerator and an integer denominator. - * The denominator will be a positive non-zero value less than or equal to the specified - * maximum denominator. If a maximum denominator is not specified, the denominator will be - * the lowest value necessary to represent the number exactly. - * - * [md] {number|string|BigNumber} Integer >= 1, or Infinity. The maximum denominator. - * - * '[BigNumber Error] Argument {not an integer|out of range} : {md}' - */ - P.toFraction = function (md) { - var d, d0, d1, d2, e, exp, n, n0, n1, q, r, s, - x = this, - xc = x.c; - - if (md != null) { - n = new BigNumber(md); - - // Throw if md is less than one or is not an integer, unless it is Infinity. - if (!n.isInteger() && (n.c || n.s !== 1) || n.lt(ONE)) { - throw Error - (bignumberError + 'Argument ' + - (n.isInteger() ? 'out of range: ' : 'not an integer: ') + valueOf(n)); - } - } - - if (!xc) return new BigNumber(x); - - d = new BigNumber(ONE); - n1 = d0 = new BigNumber(ONE); - d1 = n0 = new BigNumber(ONE); - s = coeffToString(xc); - - // Determine initial denominator. - // d is a power of 10 and the minimum max denominator that specifies the value exactly. - e = d.e = s.length - x.e - 1; - d.c[0] = POWS_TEN[(exp = e % LOG_BASE) < 0 ? LOG_BASE + exp : exp]; - md = !md || n.comparedTo(d) > 0 ? (e > 0 ? d : n1) : n; - - exp = MAX_EXP; - MAX_EXP = 1 / 0; - n = new BigNumber(s); - - // n0 = d1 = 0 - n0.c[0] = 0; - - for (; ;) { - q = div(n, d, 0, 1); - d2 = d0.plus(q.times(d1)); - if (d2.comparedTo(md) == 1) break; - d0 = d1; - d1 = d2; - n1 = n0.plus(q.times(d2 = n1)); - n0 = d2; - d = n.minus(q.times(d2 = d)); - n = d2; - } - - d2 = div(md.minus(d0), d1, 0, 1); - n0 = n0.plus(d2.times(n1)); - d0 = d0.plus(d2.times(d1)); - n0.s = n1.s = x.s; - e = e * 2; - - // Determine which fraction is closer to x, n0/d0 or n1/d1 - r = div(n1, d1, e, ROUNDING_MODE).minus(x).abs().comparedTo( - div(n0, d0, e, ROUNDING_MODE).minus(x).abs()) < 1 ? [n1, d1] : [n0, d0]; - - MAX_EXP = exp; - - return r; - }; - - - /* - * Return the value of this BigNumber converted to a number primitive. - */ - P.toNumber = function () { - return +valueOf(this); - }; - - - /* - * Return a string representing the value of this BigNumber rounded to sd significant digits - * using rounding mode rm or ROUNDING_MODE. If sd is less than the number of digits - * necessary to represent the integer part of the value in fixed-point notation, then use - * exponential notation. - * - * [sd] {number} Significant digits. Integer, 1 to MAX inclusive. - * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. - * - * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}' - */ - P.toPrecision = function (sd, rm) { - if (sd != null) intCheck(sd, 1, MAX); - return format(this, sd, rm, 2); - }; - - - /* - * Return a string representing the value of this BigNumber in base b, or base 10 if b is - * omitted. If a base is specified, including base 10, round according to DECIMAL_PLACES and - * ROUNDING_MODE. If a base is not specified, and this BigNumber has a positive exponent - * that is equal to or greater than TO_EXP_POS, or a negative exponent equal to or less than - * TO_EXP_NEG, return exponential notation. - * - * [b] {number} Integer, 2 to ALPHABET.length inclusive. - * - * '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}' - */ - P.toString = function (b) { - var str, - n = this, - s = n.s, - e = n.e; - - // Infinity or NaN? - if (e === null) { - if (s) { - str = 'Infinity'; - if (s < 0) str = '-' + str; - } else { - str = 'NaN'; - } - } else { - if (b == null) { - str = e <= TO_EXP_NEG || e >= TO_EXP_POS - ? toExponential(coeffToString(n.c), e) - : toFixedPoint(coeffToString(n.c), e, '0'); - } else if (b === 10 && alphabetHasNormalDecimalDigits) { - n = round(new BigNumber(n), DECIMAL_PLACES + e + 1, ROUNDING_MODE); - str = toFixedPoint(coeffToString(n.c), n.e, '0'); - } else { - intCheck(b, 2, ALPHABET.length, 'Base'); - str = convertBase(toFixedPoint(coeffToString(n.c), e, '0'), 10, b, s, true); - } - - if (s < 0 && n.c[0]) str = '-' + str; - } - - return str; - }; - - - /* - * Return as toString, but do not accept a base argument, and include the minus sign for - * negative zero. - */ - P.valueOf = P.toJSON = function () { - return valueOf(this); - }; - - - P._isBigNumber = true; - - if (configObject != null) BigNumber.set(configObject); - - return BigNumber; - } - - - // PRIVATE HELPER FUNCTIONS - - // These functions don't need access to variables, - // e.g. DECIMAL_PLACES, in the scope of the `clone` function above. - - - function bitFloor(n) { - var i = n | 0; - return n > 0 || n === i ? i : i - 1; - } - - - // Return a coefficient array as a string of base 10 digits. - function coeffToString(a) { - var s, z, - i = 1, - j = a.length, - r = a[0] + ''; - - for (; i < j;) { - s = a[i++] + ''; - z = LOG_BASE - s.length; - for (; z--; s = '0' + s); - r += s; - } - - // Determine trailing zeros. - for (j = r.length; r.charCodeAt(--j) === 48;); - - return r.slice(0, j + 1 || 1); - } - - - // Compare the value of BigNumbers x and y. - function compare(x, y) { - var a, b, - xc = x.c, - yc = y.c, - i = x.s, - j = y.s, - k = x.e, - l = y.e; - - // Either NaN? - if (!i || !j) return null; - - a = xc && !xc[0]; - b = yc && !yc[0]; - - // Either zero? - if (a || b) return a ? b ? 0 : -j : i; - - // Signs differ? - if (i != j) return i; - - a = i < 0; - b = k == l; - - // Either Infinity? - if (!xc || !yc) return b ? 0 : !xc ^ a ? 1 : -1; - - // Compare exponents. - if (!b) return k > l ^ a ? 1 : -1; - - j = (k = xc.length) < (l = yc.length) ? k : l; - - // Compare digit by digit. - for (i = 0; i < j; i++) if (xc[i] != yc[i]) return xc[i] > yc[i] ^ a ? 1 : -1; - - // Compare lengths. - return k == l ? 0 : k > l ^ a ? 1 : -1; - } - - - /* - * Check that n is a primitive number, an integer, and in range, otherwise throw. - */ - function intCheck(n, min, max, name) { - if (n < min || n > max || n !== mathfloor(n)) { - throw Error - (bignumberError + (name || 'Argument') + (typeof n == 'number' - ? n < min || n > max ? ' out of range: ' : ' not an integer: ' - : ' not a primitive number: ') + String(n)); - } - } - - - // Assumes finite n. - function isOdd(n) { - var k = n.c.length - 1; - return bitFloor(n.e / LOG_BASE) == k && n.c[k] % 2 != 0; - } - - - function toExponential(str, e) { - return (str.length > 1 ? str.charAt(0) + '.' + str.slice(1) : str) + - (e < 0 ? 'e' : 'e+') + e; - } - - - function toFixedPoint(str, e, z) { - var len, zs; - - // Negative exponent? - if (e < 0) { - - // Prepend zeros. - for (zs = z + '.'; ++e; zs += z); - str = zs + str; - - // Positive exponent - } else { - len = str.length; - - // Append zeros. - if (++e > len) { - for (zs = z, e -= len; --e; zs += z); - str += zs; - } else if (e < len) { - str = str.slice(0, e) + '.' + str.slice(e); - } - } - - return str; - } - - - // EXPORT - - - BigNumber = clone(); - BigNumber['default'] = BigNumber.BigNumber = BigNumber; - - // AMD. - if (typeof define == 'function' && define.amd) { - define(function () { return BigNumber; }); - - // Node.js and other environments that support module.exports. - } else if ( true && module.exports) { - module.exports = BigNumber; - - // Browser. - } else { - if (!globalObject) { - globalObject = typeof self != 'undefined' && self ? self : window; - } - - globalObject.BigNumber = BigNumber; - } -})(this); +;(function (globalObject) { + 'use strict'; + +/* + * bignumber.js v9.1.2 + * A JavaScript library for arbitrary-precision arithmetic. + * https://github.com/MikeMcl/bignumber.js + * Copyright (c) 2022 Michael Mclaughlin + * MIT Licensed. + * + * BigNumber.prototype methods | BigNumber methods + * | + * absoluteValue abs | clone + * comparedTo | config set + * decimalPlaces dp | DECIMAL_PLACES + * dividedBy div | ROUNDING_MODE + * dividedToIntegerBy idiv | EXPONENTIAL_AT + * exponentiatedBy pow | RANGE + * integerValue | CRYPTO + * isEqualTo eq | MODULO_MODE + * isFinite | POW_PRECISION + * isGreaterThan gt | FORMAT + * isGreaterThanOrEqualTo gte | ALPHABET + * isInteger | isBigNumber + * isLessThan lt | maximum max + * isLessThanOrEqualTo lte | minimum min + * isNaN | random + * isNegative | sum + * isPositive | + * isZero | + * minus | + * modulo mod | + * multipliedBy times | + * negated | + * plus | + * precision sd | + * shiftedBy | + * squareRoot sqrt | + * toExponential | + * toFixed | + * toFormat | + * toFraction | + * toJSON | + * toNumber | + * toPrecision | + * toString | + * valueOf | + * + */ + + + var BigNumber, + isNumeric = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i, + mathceil = Math.ceil, + mathfloor = Math.floor, + + bignumberError = '[BigNumber Error] ', + tooManyDigits = bignumberError + 'Number primitive has more than 15 significant digits: ', + + BASE = 1e14, + LOG_BASE = 14, + MAX_SAFE_INTEGER = 0x1fffffffffffff, // 2^53 - 1 + // MAX_INT32 = 0x7fffffff, // 2^31 - 1 + POWS_TEN = [1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13], + SQRT_BASE = 1e7, + + // EDITABLE + // The limit on the value of DECIMAL_PLACES, TO_EXP_NEG, TO_EXP_POS, MIN_EXP, MAX_EXP, and + // the arguments to toExponential, toFixed, toFormat, and toPrecision. + MAX = 1E9; // 0 to MAX_INT32 + + + /* + * Create and return a BigNumber constructor. + */ + function clone(configObject) { + var div, convertBase, parseNumeric, + P = BigNumber.prototype = { constructor: BigNumber, toString: null, valueOf: null }, + ONE = new BigNumber(1), + + + //----------------------------- EDITABLE CONFIG DEFAULTS ------------------------------- + + + // The default values below must be integers within the inclusive ranges stated. + // The values can also be changed at run-time using BigNumber.set. + + // The maximum number of decimal places for operations involving division. + DECIMAL_PLACES = 20, // 0 to MAX + + // The rounding mode used when rounding to the above decimal places, and when using + // toExponential, toFixed, toFormat and toPrecision, and round (default value). + // UP 0 Away from zero. + // DOWN 1 Towards zero. + // CEIL 2 Towards +Infinity. + // FLOOR 3 Towards -Infinity. + // HALF_UP 4 Towards nearest neighbour. If equidistant, up. + // HALF_DOWN 5 Towards nearest neighbour. If equidistant, down. + // HALF_EVEN 6 Towards nearest neighbour. If equidistant, towards even neighbour. + // HALF_CEIL 7 Towards nearest neighbour. If equidistant, towards +Infinity. + // HALF_FLOOR 8 Towards nearest neighbour. If equidistant, towards -Infinity. + ROUNDING_MODE = 4, // 0 to 8 + + // EXPONENTIAL_AT : [TO_EXP_NEG , TO_EXP_POS] + + // The exponent value at and beneath which toString returns exponential notation. + // Number type: -7 + TO_EXP_NEG = -7, // 0 to -MAX + + // The exponent value at and above which toString returns exponential notation. + // Number type: 21 + TO_EXP_POS = 21, // 0 to MAX + + // RANGE : [MIN_EXP, MAX_EXP] + + // The minimum exponent value, beneath which underflow to zero occurs. + // Number type: -324 (5e-324) + MIN_EXP = -1e7, // -1 to -MAX + + // The maximum exponent value, above which overflow to Infinity occurs. + // Number type: 308 (1.7976931348623157e+308) + // For MAX_EXP > 1e7, e.g. new BigNumber('1e100000000').plus(1) may be slow. + MAX_EXP = 1e7, // 1 to MAX + + // Whether to use cryptographically-secure random number generation, if available. + CRYPTO = false, // true or false + + // The modulo mode used when calculating the modulus: a mod n. + // The quotient (q = a / n) is calculated according to the corresponding rounding mode. + // The remainder (r) is calculated as: r = a - n * q. + // + // UP 0 The remainder is positive if the dividend is negative, else is negative. + // DOWN 1 The remainder has the same sign as the dividend. + // This modulo mode is commonly known as 'truncated division' and is + // equivalent to (a % n) in JavaScript. + // FLOOR 3 The remainder has the same sign as the divisor (Python %). + // HALF_EVEN 6 This modulo mode implements the IEEE 754 remainder function. + // EUCLID 9 Euclidian division. q = sign(n) * floor(a / abs(n)). + // The remainder is always positive. + // + // The truncated division, floored division, Euclidian division and IEEE 754 remainder + // modes are commonly used for the modulus operation. + // Although the other rounding modes can also be used, they may not give useful results. + MODULO_MODE = 1, // 0 to 9 + + // The maximum number of significant digits of the result of the exponentiatedBy operation. + // If POW_PRECISION is 0, there will be unlimited significant digits. + POW_PRECISION = 0, // 0 to MAX + + // The format specification used by the BigNumber.prototype.toFormat method. + FORMAT = { + prefix: '', + groupSize: 3, + secondaryGroupSize: 0, + groupSeparator: ',', + decimalSeparator: '.', + fractionGroupSize: 0, + fractionGroupSeparator: '\xA0', // non-breaking space + suffix: '' + }, + + // The alphabet used for base conversion. It must be at least 2 characters long, with no '+', + // '-', '.', whitespace, or repeated character. + // '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_' + ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz', + alphabetHasNormalDecimalDigits = true; + + + //------------------------------------------------------------------------------------------ + + + // CONSTRUCTOR + + + /* + * The BigNumber constructor and exported function. + * Create and return a new instance of a BigNumber object. + * + * v {number|string|BigNumber} A numeric value. + * [b] {number} The base of v. Integer, 2 to ALPHABET.length inclusive. + */ + function BigNumber(v, b) { + var alphabet, c, caseChanged, e, i, isNum, len, str, + x = this; + + // Enable constructor call without `new`. + if (!(x instanceof BigNumber)) return new BigNumber(v, b); + + if (b == null) { + + if (v && v._isBigNumber === true) { + x.s = v.s; + + if (!v.c || v.e > MAX_EXP) { + x.c = x.e = null; + } else if (v.e < MIN_EXP) { + x.c = [x.e = 0]; + } else { + x.e = v.e; + x.c = v.c.slice(); + } + + return; + } + + if ((isNum = typeof v == 'number') && v * 0 == 0) { + + // Use `1 / n` to handle minus zero also. + x.s = 1 / v < 0 ? (v = -v, -1) : 1; + + // Fast path for integers, where n < 2147483648 (2**31). + if (v === ~~v) { + for (e = 0, i = v; i >= 10; i /= 10, e++); + + if (e > MAX_EXP) { + x.c = x.e = null; + } else { + x.e = e; + x.c = [v]; + } + + return; + } + + str = String(v); + } else { + + if (!isNumeric.test(str = String(v))) return parseNumeric(x, str, isNum); + + x.s = str.charCodeAt(0) == 45 ? (str = str.slice(1), -1) : 1; + } + + // Decimal point? + if ((e = str.indexOf('.')) > -1) str = str.replace('.', ''); + + // Exponential form? + if ((i = str.search(/e/i)) > 0) { + + // Determine exponent. + if (e < 0) e = i; + e += +str.slice(i + 1); + str = str.substring(0, i); + } else if (e < 0) { + + // Integer. + e = str.length; + } + + } else { + + // '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}' + intCheck(b, 2, ALPHABET.length, 'Base'); + + // Allow exponential notation to be used with base 10 argument, while + // also rounding to DECIMAL_PLACES as with other bases. + if (b == 10 && alphabetHasNormalDecimalDigits) { + x = new BigNumber(v); + return round(x, DECIMAL_PLACES + x.e + 1, ROUNDING_MODE); + } + + str = String(v); + + if (isNum = typeof v == 'number') { + + // Avoid potential interpretation of Infinity and NaN as base 44+ values. + if (v * 0 != 0) return parseNumeric(x, str, isNum, b); + + x.s = 1 / v < 0 ? (str = str.slice(1), -1) : 1; + + // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}' + if (BigNumber.DEBUG && str.replace(/^0\.0*|\./, '').length > 15) { + throw Error + (tooManyDigits + v); + } + } else { + x.s = str.charCodeAt(0) === 45 ? (str = str.slice(1), -1) : 1; + } + + alphabet = ALPHABET.slice(0, b); + e = i = 0; + + // Check that str is a valid base b number. + // Don't use RegExp, so alphabet can contain special characters. + for (len = str.length; i < len; i++) { + if (alphabet.indexOf(c = str.charAt(i)) < 0) { + if (c == '.') { + + // If '.' is not the first character and it has not be found before. + if (i > e) { + e = len; + continue; + } + } else if (!caseChanged) { + + // Allow e.g. hexadecimal 'FF' as well as 'ff'. + if (str == str.toUpperCase() && (str = str.toLowerCase()) || + str == str.toLowerCase() && (str = str.toUpperCase())) { + caseChanged = true; + i = -1; + e = 0; + continue; + } + } + + return parseNumeric(x, String(v), isNum, b); + } + } + + // Prevent later check for length on converted number. + isNum = false; + str = convertBase(str, b, 10, x.s); + + // Decimal point? + if ((e = str.indexOf('.')) > -1) str = str.replace('.', ''); + else e = str.length; + } + + // Determine leading zeros. + for (i = 0; str.charCodeAt(i) === 48; i++); + + // Determine trailing zeros. + for (len = str.length; str.charCodeAt(--len) === 48;); + + if (str = str.slice(i, ++len)) { + len -= i; + + // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}' + if (isNum && BigNumber.DEBUG && + len > 15 && (v > MAX_SAFE_INTEGER || v !== mathfloor(v))) { + throw Error + (tooManyDigits + (x.s * v)); + } + + // Overflow? + if ((e = e - i - 1) > MAX_EXP) { + + // Infinity. + x.c = x.e = null; + + // Underflow? + } else if (e < MIN_EXP) { + + // Zero. + x.c = [x.e = 0]; + } else { + x.e = e; + x.c = []; + + // Transform base + + // e is the base 10 exponent. + // i is where to slice str to get the first element of the coefficient array. + i = (e + 1) % LOG_BASE; + if (e < 0) i += LOG_BASE; // i < 1 + + if (i < len) { + if (i) x.c.push(+str.slice(0, i)); + + for (len -= LOG_BASE; i < len;) { + x.c.push(+str.slice(i, i += LOG_BASE)); + } + + i = LOG_BASE - (str = str.slice(i)).length; + } else { + i -= len; + } + + for (; i--; str += '0'); + x.c.push(+str); + } + } else { + + // Zero. + x.c = [x.e = 0]; + } + } + + + // CONSTRUCTOR PROPERTIES + + + BigNumber.clone = clone; + + BigNumber.ROUND_UP = 0; + BigNumber.ROUND_DOWN = 1; + BigNumber.ROUND_CEIL = 2; + BigNumber.ROUND_FLOOR = 3; + BigNumber.ROUND_HALF_UP = 4; + BigNumber.ROUND_HALF_DOWN = 5; + BigNumber.ROUND_HALF_EVEN = 6; + BigNumber.ROUND_HALF_CEIL = 7; + BigNumber.ROUND_HALF_FLOOR = 8; + BigNumber.EUCLID = 9; + + + /* + * Configure infrequently-changing library-wide settings. + * + * Accept an object with the following optional properties (if the value of a property is + * a number, it must be an integer within the inclusive range stated): + * + * DECIMAL_PLACES {number} 0 to MAX + * ROUNDING_MODE {number} 0 to 8 + * EXPONENTIAL_AT {number|number[]} -MAX to MAX or [-MAX to 0, 0 to MAX] + * RANGE {number|number[]} -MAX to MAX (not zero) or [-MAX to -1, 1 to MAX] + * CRYPTO {boolean} true or false + * MODULO_MODE {number} 0 to 9 + * POW_PRECISION {number} 0 to MAX + * ALPHABET {string} A string of two or more unique characters which does + * not contain '.'. + * FORMAT {object} An object with some of the following properties: + * prefix {string} + * groupSize {number} + * secondaryGroupSize {number} + * groupSeparator {string} + * decimalSeparator {string} + * fractionGroupSize {number} + * fractionGroupSeparator {string} + * suffix {string} + * + * (The values assigned to the above FORMAT object properties are not checked for validity.) + * + * E.g. + * BigNumber.config({ DECIMAL_PLACES : 20, ROUNDING_MODE : 4 }) + * + * Ignore properties/parameters set to null or undefined, except for ALPHABET. + * + * Return an object with the properties current values. + */ + BigNumber.config = BigNumber.set = function (obj) { + var p, v; + + if (obj != null) { + + if (typeof obj == 'object') { + + // DECIMAL_PLACES {number} Integer, 0 to MAX inclusive. + // '[BigNumber Error] DECIMAL_PLACES {not a primitive number|not an integer|out of range}: {v}' + if (obj.hasOwnProperty(p = 'DECIMAL_PLACES')) { + v = obj[p]; + intCheck(v, 0, MAX, p); + DECIMAL_PLACES = v; + } + + // ROUNDING_MODE {number} Integer, 0 to 8 inclusive. + // '[BigNumber Error] ROUNDING_MODE {not a primitive number|not an integer|out of range}: {v}' + if (obj.hasOwnProperty(p = 'ROUNDING_MODE')) { + v = obj[p]; + intCheck(v, 0, 8, p); + ROUNDING_MODE = v; + } + + // EXPONENTIAL_AT {number|number[]} + // Integer, -MAX to MAX inclusive or + // [integer -MAX to 0 inclusive, 0 to MAX inclusive]. + // '[BigNumber Error] EXPONENTIAL_AT {not a primitive number|not an integer|out of range}: {v}' + if (obj.hasOwnProperty(p = 'EXPONENTIAL_AT')) { + v = obj[p]; + if (v && v.pop) { + intCheck(v[0], -MAX, 0, p); + intCheck(v[1], 0, MAX, p); + TO_EXP_NEG = v[0]; + TO_EXP_POS = v[1]; + } else { + intCheck(v, -MAX, MAX, p); + TO_EXP_NEG = -(TO_EXP_POS = v < 0 ? -v : v); + } + } + + // RANGE {number|number[]} Non-zero integer, -MAX to MAX inclusive or + // [integer -MAX to -1 inclusive, integer 1 to MAX inclusive]. + // '[BigNumber Error] RANGE {not a primitive number|not an integer|out of range|cannot be zero}: {v}' + if (obj.hasOwnProperty(p = 'RANGE')) { + v = obj[p]; + if (v && v.pop) { + intCheck(v[0], -MAX, -1, p); + intCheck(v[1], 1, MAX, p); + MIN_EXP = v[0]; + MAX_EXP = v[1]; + } else { + intCheck(v, -MAX, MAX, p); + if (v) { + MIN_EXP = -(MAX_EXP = v < 0 ? -v : v); + } else { + throw Error + (bignumberError + p + ' cannot be zero: ' + v); + } + } + } + + // CRYPTO {boolean} true or false. + // '[BigNumber Error] CRYPTO not true or false: {v}' + // '[BigNumber Error] crypto unavailable' + if (obj.hasOwnProperty(p = 'CRYPTO')) { + v = obj[p]; + if (v === !!v) { + if (v) { + if (typeof crypto != 'undefined' && crypto && + (crypto.getRandomValues || crypto.randomBytes)) { + CRYPTO = v; + } else { + CRYPTO = !v; + throw Error + (bignumberError + 'crypto unavailable'); + } + } else { + CRYPTO = v; + } + } else { + throw Error + (bignumberError + p + ' not true or false: ' + v); + } + } + + // MODULO_MODE {number} Integer, 0 to 9 inclusive. + // '[BigNumber Error] MODULO_MODE {not a primitive number|not an integer|out of range}: {v}' + if (obj.hasOwnProperty(p = 'MODULO_MODE')) { + v = obj[p]; + intCheck(v, 0, 9, p); + MODULO_MODE = v; + } + + // POW_PRECISION {number} Integer, 0 to MAX inclusive. + // '[BigNumber Error] POW_PRECISION {not a primitive number|not an integer|out of range}: {v}' + if (obj.hasOwnProperty(p = 'POW_PRECISION')) { + v = obj[p]; + intCheck(v, 0, MAX, p); + POW_PRECISION = v; + } + + // FORMAT {object} + // '[BigNumber Error] FORMAT not an object: {v}' + if (obj.hasOwnProperty(p = 'FORMAT')) { + v = obj[p]; + if (typeof v == 'object') FORMAT = v; + else throw Error + (bignumberError + p + ' not an object: ' + v); + } + + // ALPHABET {string} + // '[BigNumber Error] ALPHABET invalid: {v}' + if (obj.hasOwnProperty(p = 'ALPHABET')) { + v = obj[p]; + + // Disallow if less than two characters, + // or if it contains '+', '-', '.', whitespace, or a repeated character. + if (typeof v == 'string' && !/^.?$|[+\-.\s]|(.).*\1/.test(v)) { + alphabetHasNormalDecimalDigits = v.slice(0, 10) == '0123456789'; + ALPHABET = v; + } else { + throw Error + (bignumberError + p + ' invalid: ' + v); + } + } + + } else { + + // '[BigNumber Error] Object expected: {v}' + throw Error + (bignumberError + 'Object expected: ' + obj); + } + } + + return { + DECIMAL_PLACES: DECIMAL_PLACES, + ROUNDING_MODE: ROUNDING_MODE, + EXPONENTIAL_AT: [TO_EXP_NEG, TO_EXP_POS], + RANGE: [MIN_EXP, MAX_EXP], + CRYPTO: CRYPTO, + MODULO_MODE: MODULO_MODE, + POW_PRECISION: POW_PRECISION, + FORMAT: FORMAT, + ALPHABET: ALPHABET + }; + }; + + + /* + * Return true if v is a BigNumber instance, otherwise return false. + * + * If BigNumber.DEBUG is true, throw if a BigNumber instance is not well-formed. + * + * v {any} + * + * '[BigNumber Error] Invalid BigNumber: {v}' + */ + BigNumber.isBigNumber = function (v) { + if (!v || v._isBigNumber !== true) return false; + if (!BigNumber.DEBUG) return true; + + var i, n, + c = v.c, + e = v.e, + s = v.s; + + out: if ({}.toString.call(c) == '[object Array]') { + + if ((s === 1 || s === -1) && e >= -MAX && e <= MAX && e === mathfloor(e)) { + + // If the first element is zero, the BigNumber value must be zero. + if (c[0] === 0) { + if (e === 0 && c.length === 1) return true; + break out; + } + + // Calculate number of digits that c[0] should have, based on the exponent. + i = (e + 1) % LOG_BASE; + if (i < 1) i += LOG_BASE; + + // Calculate number of digits of c[0]. + //if (Math.ceil(Math.log(c[0] + 1) / Math.LN10) == i) { + if (String(c[0]).length == i) { + + for (i = 0; i < c.length; i++) { + n = c[i]; + if (n < 0 || n >= BASE || n !== mathfloor(n)) break out; + } + + // Last element cannot be zero, unless it is the only element. + if (n !== 0) return true; + } + } + + // Infinity/NaN + } else if (c === null && e === null && (s === null || s === 1 || s === -1)) { + return true; + } + + throw Error + (bignumberError + 'Invalid BigNumber: ' + v); + }; + + + /* + * Return a new BigNumber whose value is the maximum of the arguments. + * + * arguments {number|string|BigNumber} + */ + BigNumber.maximum = BigNumber.max = function () { + return maxOrMin(arguments, -1); + }; + + + /* + * Return a new BigNumber whose value is the minimum of the arguments. + * + * arguments {number|string|BigNumber} + */ + BigNumber.minimum = BigNumber.min = function () { + return maxOrMin(arguments, 1); + }; + + + /* + * Return a new BigNumber with a random value equal to or greater than 0 and less than 1, + * and with dp, or DECIMAL_PLACES if dp is omitted, decimal places (or less if trailing + * zeros are produced). + * + * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp}' + * '[BigNumber Error] crypto unavailable' + */ + BigNumber.random = (function () { + var pow2_53 = 0x20000000000000; + + // Return a 53 bit integer n, where 0 <= n < 9007199254740992. + // Check if Math.random() produces more than 32 bits of randomness. + // If it does, assume at least 53 bits are produced, otherwise assume at least 30 bits. + // 0x40000000 is 2^30, 0x800000 is 2^23, 0x1fffff is 2^21 - 1. + var random53bitInt = (Math.random() * pow2_53) & 0x1fffff + ? function () { return mathfloor(Math.random() * pow2_53); } + : function () { return ((Math.random() * 0x40000000 | 0) * 0x800000) + + (Math.random() * 0x800000 | 0); }; + + return function (dp) { + var a, b, e, k, v, + i = 0, + c = [], + rand = new BigNumber(ONE); + + if (dp == null) dp = DECIMAL_PLACES; + else intCheck(dp, 0, MAX); + + k = mathceil(dp / LOG_BASE); + + if (CRYPTO) { + + // Browsers supporting crypto.getRandomValues. + if (crypto.getRandomValues) { + + a = crypto.getRandomValues(new Uint32Array(k *= 2)); + + for (; i < k;) { + + // 53 bits: + // ((Math.pow(2, 32) - 1) * Math.pow(2, 21)).toString(2) + // 11111 11111111 11111111 11111111 11100000 00000000 00000000 + // ((Math.pow(2, 32) - 1) >>> 11).toString(2) + // 11111 11111111 11111111 + // 0x20000 is 2^21. + v = a[i] * 0x20000 + (a[i + 1] >>> 11); + + // Rejection sampling: + // 0 <= v < 9007199254740992 + // Probability that v >= 9e15, is + // 7199254740992 / 9007199254740992 ~= 0.0008, i.e. 1 in 1251 + if (v >= 9e15) { + b = crypto.getRandomValues(new Uint32Array(2)); + a[i] = b[0]; + a[i + 1] = b[1]; + } else { + + // 0 <= v <= 8999999999999999 + // 0 <= (v % 1e14) <= 99999999999999 + c.push(v % 1e14); + i += 2; + } + } + i = k / 2; + + // Node.js supporting crypto.randomBytes. + } else if (crypto.randomBytes) { + + // buffer + a = crypto.randomBytes(k *= 7); + + for (; i < k;) { + + // 0x1000000000000 is 2^48, 0x10000000000 is 2^40 + // 0x100000000 is 2^32, 0x1000000 is 2^24 + // 11111 11111111 11111111 11111111 11111111 11111111 11111111 + // 0 <= v < 9007199254740992 + v = ((a[i] & 31) * 0x1000000000000) + (a[i + 1] * 0x10000000000) + + (a[i + 2] * 0x100000000) + (a[i + 3] * 0x1000000) + + (a[i + 4] << 16) + (a[i + 5] << 8) + a[i + 6]; + + if (v >= 9e15) { + crypto.randomBytes(7).copy(a, i); + } else { + + // 0 <= (v % 1e14) <= 99999999999999 + c.push(v % 1e14); + i += 7; + } + } + i = k / 7; + } else { + CRYPTO = false; + throw Error + (bignumberError + 'crypto unavailable'); + } + } + + // Use Math.random. + if (!CRYPTO) { + + for (; i < k;) { + v = random53bitInt(); + if (v < 9e15) c[i++] = v % 1e14; + } + } + + k = c[--i]; + dp %= LOG_BASE; + + // Convert trailing digits to zeros according to dp. + if (k && dp) { + v = POWS_TEN[LOG_BASE - dp]; + c[i] = mathfloor(k / v) * v; + } + + // Remove trailing elements which are zero. + for (; c[i] === 0; c.pop(), i--); + + // Zero? + if (i < 0) { + c = [e = 0]; + } else { + + // Remove leading elements which are zero and adjust exponent accordingly. + for (e = -1 ; c[0] === 0; c.splice(0, 1), e -= LOG_BASE); + + // Count the digits of the first element of c to determine leading zeros, and... + for (i = 1, v = c[0]; v >= 10; v /= 10, i++); + + // adjust the exponent accordingly. + if (i < LOG_BASE) e -= LOG_BASE - i; + } + + rand.e = e; + rand.c = c; + return rand; + }; + })(); + + + /* + * Return a BigNumber whose value is the sum of the arguments. + * + * arguments {number|string|BigNumber} + */ + BigNumber.sum = function () { + var i = 1, + args = arguments, + sum = new BigNumber(args[0]); + for (; i < args.length;) sum = sum.plus(args[i++]); + return sum; + }; + + + // PRIVATE FUNCTIONS + + + // Called by BigNumber and BigNumber.prototype.toString. + convertBase = (function () { + var decimal = '0123456789'; + + /* + * Convert string of baseIn to an array of numbers of baseOut. + * Eg. toBaseOut('255', 10, 16) returns [15, 15]. + * Eg. toBaseOut('ff', 16, 10) returns [2, 5, 5]. + */ + function toBaseOut(str, baseIn, baseOut, alphabet) { + var j, + arr = [0], + arrL, + i = 0, + len = str.length; + + for (; i < len;) { + for (arrL = arr.length; arrL--; arr[arrL] *= baseIn); + + arr[0] += alphabet.indexOf(str.charAt(i++)); + + for (j = 0; j < arr.length; j++) { + + if (arr[j] > baseOut - 1) { + if (arr[j + 1] == null) arr[j + 1] = 0; + arr[j + 1] += arr[j] / baseOut | 0; + arr[j] %= baseOut; + } + } + } + + return arr.reverse(); + } + + // Convert a numeric string of baseIn to a numeric string of baseOut. + // If the caller is toString, we are converting from base 10 to baseOut. + // If the caller is BigNumber, we are converting from baseIn to base 10. + return function (str, baseIn, baseOut, sign, callerIsToString) { + var alphabet, d, e, k, r, x, xc, y, + i = str.indexOf('.'), + dp = DECIMAL_PLACES, + rm = ROUNDING_MODE; + + // Non-integer. + if (i >= 0) { + k = POW_PRECISION; + + // Unlimited precision. + POW_PRECISION = 0; + str = str.replace('.', ''); + y = new BigNumber(baseIn); + x = y.pow(str.length - i); + POW_PRECISION = k; + + // Convert str as if an integer, then restore the fraction part by dividing the + // result by its base raised to a power. + + y.c = toBaseOut(toFixedPoint(coeffToString(x.c), x.e, '0'), + 10, baseOut, decimal); + y.e = y.c.length; + } + + // Convert the number as integer. + + xc = toBaseOut(str, baseIn, baseOut, callerIsToString + ? (alphabet = ALPHABET, decimal) + : (alphabet = decimal, ALPHABET)); + + // xc now represents str as an integer and converted to baseOut. e is the exponent. + e = k = xc.length; + + // Remove trailing zeros. + for (; xc[--k] == 0; xc.pop()); + + // Zero? + if (!xc[0]) return alphabet.charAt(0); + + // Does str represent an integer? If so, no need for the division. + if (i < 0) { + --e; + } else { + x.c = xc; + x.e = e; + + // The sign is needed for correct rounding. + x.s = sign; + x = div(x, y, dp, rm, baseOut); + xc = x.c; + r = x.r; + e = x.e; + } + + // xc now represents str converted to baseOut. + + // THe index of the rounding digit. + d = e + dp + 1; + + // The rounding digit: the digit to the right of the digit that may be rounded up. + i = xc[d]; + + // Look at the rounding digits and mode to determine whether to round up. + + k = baseOut / 2; + r = r || d < 0 || xc[d + 1] != null; + + r = rm < 4 ? (i != null || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2)) + : i > k || i == k &&(rm == 4 || r || rm == 6 && xc[d - 1] & 1 || + rm == (x.s < 0 ? 8 : 7)); + + // If the index of the rounding digit is not greater than zero, or xc represents + // zero, then the result of the base conversion is zero or, if rounding up, a value + // such as 0.00001. + if (d < 1 || !xc[0]) { + + // 1^-dp or 0 + str = r ? toFixedPoint(alphabet.charAt(1), -dp, alphabet.charAt(0)) : alphabet.charAt(0); + } else { + + // Truncate xc to the required number of decimal places. + xc.length = d; + + // Round up? + if (r) { + + // Rounding up may mean the previous digit has to be rounded up and so on. + for (--baseOut; ++xc[--d] > baseOut;) { + xc[d] = 0; + + if (!d) { + ++e; + xc = [1].concat(xc); + } + } + } + + // Determine trailing zeros. + for (k = xc.length; !xc[--k];); + + // E.g. [4, 11, 15] becomes 4bf. + for (i = 0, str = ''; i <= k; str += alphabet.charAt(xc[i++])); + + // Add leading zeros, decimal point and trailing zeros as required. + str = toFixedPoint(str, e, alphabet.charAt(0)); + } + + // The caller will add the sign. + return str; + }; + })(); + + + // Perform division in the specified base. Called by div and convertBase. + div = (function () { + + // Assume non-zero x and k. + function multiply(x, k, base) { + var m, temp, xlo, xhi, + carry = 0, + i = x.length, + klo = k % SQRT_BASE, + khi = k / SQRT_BASE | 0; + + for (x = x.slice(); i--;) { + xlo = x[i] % SQRT_BASE; + xhi = x[i] / SQRT_BASE | 0; + m = khi * xlo + xhi * klo; + temp = klo * xlo + ((m % SQRT_BASE) * SQRT_BASE) + carry; + carry = (temp / base | 0) + (m / SQRT_BASE | 0) + khi * xhi; + x[i] = temp % base; + } + + if (carry) x = [carry].concat(x); + + return x; + } + + function compare(a, b, aL, bL) { + var i, cmp; + + if (aL != bL) { + cmp = aL > bL ? 1 : -1; + } else { + + for (i = cmp = 0; i < aL; i++) { + + if (a[i] != b[i]) { + cmp = a[i] > b[i] ? 1 : -1; + break; + } + } + } + + return cmp; + } + + function subtract(a, b, aL, base) { + var i = 0; + + // Subtract b from a. + for (; aL--;) { + a[aL] -= i; + i = a[aL] < b[aL] ? 1 : 0; + a[aL] = i * base + a[aL] - b[aL]; + } + + // Remove leading zeros. + for (; !a[0] && a.length > 1; a.splice(0, 1)); + } + + // x: dividend, y: divisor. + return function (x, y, dp, rm, base) { + var cmp, e, i, more, n, prod, prodL, q, qc, rem, remL, rem0, xi, xL, yc0, + yL, yz, + s = x.s == y.s ? 1 : -1, + xc = x.c, + yc = y.c; + + // Either NaN, Infinity or 0? + if (!xc || !xc[0] || !yc || !yc[0]) { + + return new BigNumber( + + // Return NaN if either NaN, or both Infinity or 0. + !x.s || !y.s || (xc ? yc && xc[0] == yc[0] : !yc) ? NaN : + + // Return ±0 if x is ±0 or y is ±Infinity, or return ±Infinity as y is ±0. + xc && xc[0] == 0 || !yc ? s * 0 : s / 0 + ); + } + + q = new BigNumber(s); + qc = q.c = []; + e = x.e - y.e; + s = dp + e + 1; + + if (!base) { + base = BASE; + e = bitFloor(x.e / LOG_BASE) - bitFloor(y.e / LOG_BASE); + s = s / LOG_BASE | 0; + } + + // Result exponent may be one less then the current value of e. + // The coefficients of the BigNumbers from convertBase may have trailing zeros. + for (i = 0; yc[i] == (xc[i] || 0); i++); + + if (yc[i] > (xc[i] || 0)) e--; + + if (s < 0) { + qc.push(1); + more = true; + } else { + xL = xc.length; + yL = yc.length; + i = 0; + s += 2; + + // Normalise xc and yc so highest order digit of yc is >= base / 2. + + n = mathfloor(base / (yc[0] + 1)); + + // Not necessary, but to handle odd bases where yc[0] == (base / 2) - 1. + // if (n > 1 || n++ == 1 && yc[0] < base / 2) { + if (n > 1) { + yc = multiply(yc, n, base); + xc = multiply(xc, n, base); + yL = yc.length; + xL = xc.length; + } + + xi = yL; + rem = xc.slice(0, yL); + remL = rem.length; + + // Add zeros to make remainder as long as divisor. + for (; remL < yL; rem[remL++] = 0); + yz = yc.slice(); + yz = [0].concat(yz); + yc0 = yc[0]; + if (yc[1] >= base / 2) yc0++; + // Not necessary, but to prevent trial digit n > base, when using base 3. + // else if (base == 3 && yc0 == 1) yc0 = 1 + 1e-15; + + do { + n = 0; + + // Compare divisor and remainder. + cmp = compare(yc, rem, yL, remL); + + // If divisor < remainder. + if (cmp < 0) { + + // Calculate trial digit, n. + + rem0 = rem[0]; + if (yL != remL) rem0 = rem0 * base + (rem[1] || 0); + + // n is how many times the divisor goes into the current remainder. + n = mathfloor(rem0 / yc0); + + // Algorithm: + // product = divisor multiplied by trial digit (n). + // Compare product and remainder. + // If product is greater than remainder: + // Subtract divisor from product, decrement trial digit. + // Subtract product from remainder. + // If product was less than remainder at the last compare: + // Compare new remainder and divisor. + // If remainder is greater than divisor: + // Subtract divisor from remainder, increment trial digit. + + if (n > 1) { + + // n may be > base only when base is 3. + if (n >= base) n = base - 1; + + // product = divisor * trial digit. + prod = multiply(yc, n, base); + prodL = prod.length; + remL = rem.length; + + // Compare product and remainder. + // If product > remainder then trial digit n too high. + // n is 1 too high about 5% of the time, and is not known to have + // ever been more than 1 too high. + while (compare(prod, rem, prodL, remL) == 1) { + n--; + + // Subtract divisor from product. + subtract(prod, yL < prodL ? yz : yc, prodL, base); + prodL = prod.length; + cmp = 1; + } + } else { + + // n is 0 or 1, cmp is -1. + // If n is 0, there is no need to compare yc and rem again below, + // so change cmp to 1 to avoid it. + // If n is 1, leave cmp as -1, so yc and rem are compared again. + if (n == 0) { + + // divisor < remainder, so n must be at least 1. + cmp = n = 1; + } + + // product = divisor + prod = yc.slice(); + prodL = prod.length; + } + + if (prodL < remL) prod = [0].concat(prod); + + // Subtract product from remainder. + subtract(rem, prod, remL, base); + remL = rem.length; + + // If product was < remainder. + if (cmp == -1) { + + // Compare divisor and new remainder. + // If divisor < new remainder, subtract divisor from remainder. + // Trial digit n too low. + // n is 1 too low about 5% of the time, and very rarely 2 too low. + while (compare(yc, rem, yL, remL) < 1) { + n++; + + // Subtract divisor from remainder. + subtract(rem, yL < remL ? yz : yc, remL, base); + remL = rem.length; + } + } + } else if (cmp === 0) { + n++; + rem = [0]; + } // else cmp === 1 and n will be 0 + + // Add the next digit, n, to the result array. + qc[i++] = n; + + // Update the remainder. + if (rem[0]) { + rem[remL++] = xc[xi] || 0; + } else { + rem = [xc[xi]]; + remL = 1; + } + } while ((xi++ < xL || rem[0] != null) && s--); + + more = rem[0] != null; + + // Leading zero? + if (!qc[0]) qc.splice(0, 1); + } + + if (base == BASE) { + + // To calculate q.e, first get the number of digits of qc[0]. + for (i = 1, s = qc[0]; s >= 10; s /= 10, i++); + + round(q, dp + (q.e = i + e * LOG_BASE - 1) + 1, rm, more); + + // Caller is convertBase. + } else { + q.e = e; + q.r = +more; + } + + return q; + }; + })(); + + + /* + * Return a string representing the value of BigNumber n in fixed-point or exponential + * notation rounded to the specified decimal places or significant digits. + * + * n: a BigNumber. + * i: the index of the last digit required (i.e. the digit that may be rounded up). + * rm: the rounding mode. + * id: 1 (toExponential) or 2 (toPrecision). + */ + function format(n, i, rm, id) { + var c0, e, ne, len, str; + + if (rm == null) rm = ROUNDING_MODE; + else intCheck(rm, 0, 8); + + if (!n.c) return n.toString(); + + c0 = n.c[0]; + ne = n.e; + + if (i == null) { + str = coeffToString(n.c); + str = id == 1 || id == 2 && (ne <= TO_EXP_NEG || ne >= TO_EXP_POS) + ? toExponential(str, ne) + : toFixedPoint(str, ne, '0'); + } else { + n = round(new BigNumber(n), i, rm); + + // n.e may have changed if the value was rounded up. + e = n.e; + + str = coeffToString(n.c); + len = str.length; + + // toPrecision returns exponential notation if the number of significant digits + // specified is less than the number of digits necessary to represent the integer + // part of the value in fixed-point notation. + + // Exponential notation. + if (id == 1 || id == 2 && (i <= e || e <= TO_EXP_NEG)) { + + // Append zeros? + for (; len < i; str += '0', len++); + str = toExponential(str, e); + + // Fixed-point notation. + } else { + i -= ne; + str = toFixedPoint(str, e, '0'); + + // Append zeros? + if (e + 1 > len) { + if (--i > 0) for (str += '.'; i--; str += '0'); + } else { + i += e - len; + if (i > 0) { + if (e + 1 == len) str += '.'; + for (; i--; str += '0'); + } + } + } + } + + return n.s < 0 && c0 ? '-' + str : str; + } + + + // Handle BigNumber.max and BigNumber.min. + // If any number is NaN, return NaN. + function maxOrMin(args, n) { + var k, y, + i = 1, + x = new BigNumber(args[0]); + + for (; i < args.length; i++) { + y = new BigNumber(args[i]); + if (!y.s || (k = compare(x, y)) === n || k === 0 && x.s === n) { + x = y; + } + } + + return x; + } + + + /* + * Strip trailing zeros, calculate base 10 exponent and check against MIN_EXP and MAX_EXP. + * Called by minus, plus and times. + */ + function normalise(n, c, e) { + var i = 1, + j = c.length; + + // Remove trailing zeros. + for (; !c[--j]; c.pop()); + + // Calculate the base 10 exponent. First get the number of digits of c[0]. + for (j = c[0]; j >= 10; j /= 10, i++); + + // Overflow? + if ((e = i + e * LOG_BASE - 1) > MAX_EXP) { + + // Infinity. + n.c = n.e = null; + + // Underflow? + } else if (e < MIN_EXP) { + + // Zero. + n.c = [n.e = 0]; + } else { + n.e = e; + n.c = c; + } + + return n; + } + + + // Handle values that fail the validity test in BigNumber. + parseNumeric = (function () { + var basePrefix = /^(-?)0([xbo])(?=\w[\w.]*$)/i, + dotAfter = /^([^.]+)\.$/, + dotBefore = /^\.([^.]+)$/, + isInfinityOrNaN = /^-?(Infinity|NaN)$/, + whitespaceOrPlus = /^\s*\+(?=[\w.])|^\s+|\s+$/g; + + return function (x, str, isNum, b) { + var base, + s = isNum ? str : str.replace(whitespaceOrPlus, ''); + + // No exception on ±Infinity or NaN. + if (isInfinityOrNaN.test(s)) { + x.s = isNaN(s) ? null : s < 0 ? -1 : 1; + } else { + if (!isNum) { + + // basePrefix = /^(-?)0([xbo])(?=\w[\w.]*$)/i + s = s.replace(basePrefix, function (m, p1, p2) { + base = (p2 = p2.toLowerCase()) == 'x' ? 16 : p2 == 'b' ? 2 : 8; + return !b || b == base ? p1 : m; + }); + + if (b) { + base = b; + + // E.g. '1.' to '1', '.1' to '0.1' + s = s.replace(dotAfter, '$1').replace(dotBefore, '0.$1'); + } + + if (str != s) return new BigNumber(s, base); + } + + // '[BigNumber Error] Not a number: {n}' + // '[BigNumber Error] Not a base {b} number: {n}' + if (BigNumber.DEBUG) { + throw Error + (bignumberError + 'Not a' + (b ? ' base ' + b : '') + ' number: ' + str); + } + + // NaN + x.s = null; + } + + x.c = x.e = null; + } + })(); + + + /* + * Round x to sd significant digits using rounding mode rm. Check for over/under-flow. + * If r is truthy, it is known that there are more digits after the rounding digit. + */ + function round(x, sd, rm, r) { + var d, i, j, k, n, ni, rd, + xc = x.c, + pows10 = POWS_TEN; + + // if x is not Infinity or NaN... + if (xc) { + + // rd is the rounding digit, i.e. the digit after the digit that may be rounded up. + // n is a base 1e14 number, the value of the element of array x.c containing rd. + // ni is the index of n within x.c. + // d is the number of digits of n. + // i is the index of rd within n including leading zeros. + // j is the actual index of rd within n (if < 0, rd is a leading zero). + out: { + + // Get the number of digits of the first element of xc. + for (d = 1, k = xc[0]; k >= 10; k /= 10, d++); + i = sd - d; + + // If the rounding digit is in the first element of xc... + if (i < 0) { + i += LOG_BASE; + j = sd; + n = xc[ni = 0]; + + // Get the rounding digit at index j of n. + rd = mathfloor(n / pows10[d - j - 1] % 10); + } else { + ni = mathceil((i + 1) / LOG_BASE); + + if (ni >= xc.length) { + + if (r) { + + // Needed by sqrt. + for (; xc.length <= ni; xc.push(0)); + n = rd = 0; + d = 1; + i %= LOG_BASE; + j = i - LOG_BASE + 1; + } else { + break out; + } + } else { + n = k = xc[ni]; + + // Get the number of digits of n. + for (d = 1; k >= 10; k /= 10, d++); + + // Get the index of rd within n. + i %= LOG_BASE; + + // Get the index of rd within n, adjusted for leading zeros. + // The number of leading zeros of n is given by LOG_BASE - d. + j = i - LOG_BASE + d; + + // Get the rounding digit at index j of n. + rd = j < 0 ? 0 : mathfloor(n / pows10[d - j - 1] % 10); + } + } + + r = r || sd < 0 || + + // Are there any non-zero digits after the rounding digit? + // The expression n % pows10[d - j - 1] returns all digits of n to the right + // of the digit at j, e.g. if n is 908714 and j is 2, the expression gives 714. + xc[ni + 1] != null || (j < 0 ? n : n % pows10[d - j - 1]); + + r = rm < 4 + ? (rd || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2)) + : rd > 5 || rd == 5 && (rm == 4 || r || rm == 6 && + + // Check whether the digit to the left of the rounding digit is odd. + ((i > 0 ? j > 0 ? n / pows10[d - j] : 0 : xc[ni - 1]) % 10) & 1 || + rm == (x.s < 0 ? 8 : 7)); + + if (sd < 1 || !xc[0]) { + xc.length = 0; + + if (r) { + + // Convert sd to decimal places. + sd -= x.e + 1; + + // 1, 0.1, 0.01, 0.001, 0.0001 etc. + xc[0] = pows10[(LOG_BASE - sd % LOG_BASE) % LOG_BASE]; + x.e = -sd || 0; + } else { + + // Zero. + xc[0] = x.e = 0; + } + + return x; + } + + // Remove excess digits. + if (i == 0) { + xc.length = ni; + k = 1; + ni--; + } else { + xc.length = ni + 1; + k = pows10[LOG_BASE - i]; + + // E.g. 56700 becomes 56000 if 7 is the rounding digit. + // j > 0 means i > number of leading zeros of n. + xc[ni] = j > 0 ? mathfloor(n / pows10[d - j] % pows10[j]) * k : 0; + } + + // Round up? + if (r) { + + for (; ;) { + + // If the digit to be rounded up is in the first element of xc... + if (ni == 0) { + + // i will be the length of xc[0] before k is added. + for (i = 1, j = xc[0]; j >= 10; j /= 10, i++); + j = xc[0] += k; + for (k = 1; j >= 10; j /= 10, k++); + + // if i != k the length has increased. + if (i != k) { + x.e++; + if (xc[0] == BASE) xc[0] = 1; + } + + break; + } else { + xc[ni] += k; + if (xc[ni] != BASE) break; + xc[ni--] = 0; + k = 1; + } + } + } + + // Remove trailing zeros. + for (i = xc.length; xc[--i] === 0; xc.pop()); + } + + // Overflow? Infinity. + if (x.e > MAX_EXP) { + x.c = x.e = null; + + // Underflow? Zero. + } else if (x.e < MIN_EXP) { + x.c = [x.e = 0]; + } + } + + return x; + } + + + function valueOf(n) { + var str, + e = n.e; + + if (e === null) return n.toString(); + + str = coeffToString(n.c); + + str = e <= TO_EXP_NEG || e >= TO_EXP_POS + ? toExponential(str, e) + : toFixedPoint(str, e, '0'); + + return n.s < 0 ? '-' + str : str; + } + + + // PROTOTYPE/INSTANCE METHODS + + + /* + * Return a new BigNumber whose value is the absolute value of this BigNumber. + */ + P.absoluteValue = P.abs = function () { + var x = new BigNumber(this); + if (x.s < 0) x.s = 1; + return x; + }; + + + /* + * Return + * 1 if the value of this BigNumber is greater than the value of BigNumber(y, b), + * -1 if the value of this BigNumber is less than the value of BigNumber(y, b), + * 0 if they have the same value, + * or null if the value of either is NaN. + */ + P.comparedTo = function (y, b) { + return compare(this, new BigNumber(y, b)); + }; + + + /* + * If dp is undefined or null or true or false, return the number of decimal places of the + * value of this BigNumber, or null if the value of this BigNumber is ±Infinity or NaN. + * + * Otherwise, if dp is a number, return a new BigNumber whose value is the value of this + * BigNumber rounded to a maximum of dp decimal places using rounding mode rm, or + * ROUNDING_MODE if rm is omitted. + * + * [dp] {number} Decimal places: integer, 0 to MAX inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' + */ + P.decimalPlaces = P.dp = function (dp, rm) { + var c, n, v, + x = this; + + if (dp != null) { + intCheck(dp, 0, MAX); + if (rm == null) rm = ROUNDING_MODE; + else intCheck(rm, 0, 8); + + return round(new BigNumber(x), dp + x.e + 1, rm); + } + + if (!(c = x.c)) return null; + n = ((v = c.length - 1) - bitFloor(this.e / LOG_BASE)) * LOG_BASE; + + // Subtract the number of trailing zeros of the last number. + if (v = c[v]) for (; v % 10 == 0; v /= 10, n--); + if (n < 0) n = 0; + + return n; + }; + + + /* + * n / 0 = I + * n / N = N + * n / I = 0 + * 0 / n = 0 + * 0 / 0 = N + * 0 / N = N + * 0 / I = 0 + * N / n = N + * N / 0 = N + * N / N = N + * N / I = N + * I / n = I + * I / 0 = I + * I / N = N + * I / I = N + * + * Return a new BigNumber whose value is the value of this BigNumber divided by the value of + * BigNumber(y, b), rounded according to DECIMAL_PLACES and ROUNDING_MODE. + */ + P.dividedBy = P.div = function (y, b) { + return div(this, new BigNumber(y, b), DECIMAL_PLACES, ROUNDING_MODE); + }; + + + /* + * Return a new BigNumber whose value is the integer part of dividing the value of this + * BigNumber by the value of BigNumber(y, b). + */ + P.dividedToIntegerBy = P.idiv = function (y, b) { + return div(this, new BigNumber(y, b), 0, 1); + }; + + + /* + * Return a BigNumber whose value is the value of this BigNumber exponentiated by n. + * + * If m is present, return the result modulo m. + * If n is negative round according to DECIMAL_PLACES and ROUNDING_MODE. + * If POW_PRECISION is non-zero and m is not present, round to POW_PRECISION using ROUNDING_MODE. + * + * The modular power operation works efficiently when x, n, and m are integers, otherwise it + * is equivalent to calculating x.exponentiatedBy(n).modulo(m) with a POW_PRECISION of 0. + * + * n {number|string|BigNumber} The exponent. An integer. + * [m] {number|string|BigNumber} The modulus. + * + * '[BigNumber Error] Exponent not an integer: {n}' + */ + P.exponentiatedBy = P.pow = function (n, m) { + var half, isModExp, i, k, more, nIsBig, nIsNeg, nIsOdd, y, + x = this; + + n = new BigNumber(n); + + // Allow NaN and ±Infinity, but not other non-integers. + if (n.c && !n.isInteger()) { + throw Error + (bignumberError + 'Exponent not an integer: ' + valueOf(n)); + } + + if (m != null) m = new BigNumber(m); + + // Exponent of MAX_SAFE_INTEGER is 15. + nIsBig = n.e > 14; + + // If x is NaN, ±Infinity, ±0 or ±1, or n is ±Infinity, NaN or ±0. + if (!x.c || !x.c[0] || x.c[0] == 1 && !x.e && x.c.length == 1 || !n.c || !n.c[0]) { + + // The sign of the result of pow when x is negative depends on the evenness of n. + // If +n overflows to ±Infinity, the evenness of n would be not be known. + y = new BigNumber(Math.pow(+valueOf(x), nIsBig ? n.s * (2 - isOdd(n)) : +valueOf(n))); + return m ? y.mod(m) : y; + } + + nIsNeg = n.s < 0; + + if (m) { + + // x % m returns NaN if abs(m) is zero, or m is NaN. + if (m.c ? !m.c[0] : !m.s) return new BigNumber(NaN); + + isModExp = !nIsNeg && x.isInteger() && m.isInteger(); + + if (isModExp) x = x.mod(m); + + // Overflow to ±Infinity: >=2**1e10 or >=1.0000024**1e15. + // Underflow to ±0: <=0.79**1e10 or <=0.9999975**1e15. + } else if (n.e > 9 && (x.e > 0 || x.e < -1 || (x.e == 0 + // [1, 240000000] + ? x.c[0] > 1 || nIsBig && x.c[1] >= 24e7 + // [80000000000000] [99999750000000] + : x.c[0] < 8e13 || nIsBig && x.c[0] <= 9999975e7))) { + + // If x is negative and n is odd, k = -0, else k = 0. + k = x.s < 0 && isOdd(n) ? -0 : 0; + + // If x >= 1, k = ±Infinity. + if (x.e > -1) k = 1 / k; + + // If n is negative return ±0, else return ±Infinity. + return new BigNumber(nIsNeg ? 1 / k : k); + + } else if (POW_PRECISION) { + + // Truncating each coefficient array to a length of k after each multiplication + // equates to truncating significant digits to POW_PRECISION + [28, 41], + // i.e. there will be a minimum of 28 guard digits retained. + k = mathceil(POW_PRECISION / LOG_BASE + 2); + } + + if (nIsBig) { + half = new BigNumber(0.5); + if (nIsNeg) n.s = 1; + nIsOdd = isOdd(n); + } else { + i = Math.abs(+valueOf(n)); + nIsOdd = i % 2; + } + + y = new BigNumber(ONE); + + // Performs 54 loop iterations for n of 9007199254740991. + for (; ;) { + + if (nIsOdd) { + y = y.times(x); + if (!y.c) break; + + if (k) { + if (y.c.length > k) y.c.length = k; + } else if (isModExp) { + y = y.mod(m); //y = y.minus(div(y, m, 0, MODULO_MODE).times(m)); + } + } + + if (i) { + i = mathfloor(i / 2); + if (i === 0) break; + nIsOdd = i % 2; + } else { + n = n.times(half); + round(n, n.e + 1, 1); + + if (n.e > 14) { + nIsOdd = isOdd(n); + } else { + i = +valueOf(n); + if (i === 0) break; + nIsOdd = i % 2; + } + } + + x = x.times(x); + + if (k) { + if (x.c && x.c.length > k) x.c.length = k; + } else if (isModExp) { + x = x.mod(m); //x = x.minus(div(x, m, 0, MODULO_MODE).times(m)); + } + } + + if (isModExp) return y; + if (nIsNeg) y = ONE.div(y); + + return m ? y.mod(m) : k ? round(y, POW_PRECISION, ROUNDING_MODE, more) : y; + }; + + + /* + * Return a new BigNumber whose value is the value of this BigNumber rounded to an integer + * using rounding mode rm, or ROUNDING_MODE if rm is omitted. + * + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {rm}' + */ + P.integerValue = function (rm) { + var n = new BigNumber(this); + if (rm == null) rm = ROUNDING_MODE; + else intCheck(rm, 0, 8); + return round(n, n.e + 1, rm); + }; + + + /* + * Return true if the value of this BigNumber is equal to the value of BigNumber(y, b), + * otherwise return false. + */ + P.isEqualTo = P.eq = function (y, b) { + return compare(this, new BigNumber(y, b)) === 0; + }; + + + /* + * Return true if the value of this BigNumber is a finite number, otherwise return false. + */ + P.isFinite = function () { + return !!this.c; + }; + + + /* + * Return true if the value of this BigNumber is greater than the value of BigNumber(y, b), + * otherwise return false. + */ + P.isGreaterThan = P.gt = function (y, b) { + return compare(this, new BigNumber(y, b)) > 0; + }; + + + /* + * Return true if the value of this BigNumber is greater than or equal to the value of + * BigNumber(y, b), otherwise return false. + */ + P.isGreaterThanOrEqualTo = P.gte = function (y, b) { + return (b = compare(this, new BigNumber(y, b))) === 1 || b === 0; + + }; + + + /* + * Return true if the value of this BigNumber is an integer, otherwise return false. + */ + P.isInteger = function () { + return !!this.c && bitFloor(this.e / LOG_BASE) > this.c.length - 2; + }; + + + /* + * Return true if the value of this BigNumber is less than the value of BigNumber(y, b), + * otherwise return false. + */ + P.isLessThan = P.lt = function (y, b) { + return compare(this, new BigNumber(y, b)) < 0; + }; + + + /* + * Return true if the value of this BigNumber is less than or equal to the value of + * BigNumber(y, b), otherwise return false. + */ + P.isLessThanOrEqualTo = P.lte = function (y, b) { + return (b = compare(this, new BigNumber(y, b))) === -1 || b === 0; + }; + + + /* + * Return true if the value of this BigNumber is NaN, otherwise return false. + */ + P.isNaN = function () { + return !this.s; + }; + + + /* + * Return true if the value of this BigNumber is negative, otherwise return false. + */ + P.isNegative = function () { + return this.s < 0; + }; + + + /* + * Return true if the value of this BigNumber is positive, otherwise return false. + */ + P.isPositive = function () { + return this.s > 0; + }; + + + /* + * Return true if the value of this BigNumber is 0 or -0, otherwise return false. + */ + P.isZero = function () { + return !!this.c && this.c[0] == 0; + }; + + + /* + * n - 0 = n + * n - N = N + * n - I = -I + * 0 - n = -n + * 0 - 0 = 0 + * 0 - N = N + * 0 - I = -I + * N - n = N + * N - 0 = N + * N - N = N + * N - I = N + * I - n = I + * I - 0 = I + * I - N = N + * I - I = N + * + * Return a new BigNumber whose value is the value of this BigNumber minus the value of + * BigNumber(y, b). + */ + P.minus = function (y, b) { + var i, j, t, xLTy, + x = this, + a = x.s; + + y = new BigNumber(y, b); + b = y.s; + + // Either NaN? + if (!a || !b) return new BigNumber(NaN); + + // Signs differ? + if (a != b) { + y.s = -b; + return x.plus(y); + } + + var xe = x.e / LOG_BASE, + ye = y.e / LOG_BASE, + xc = x.c, + yc = y.c; + + if (!xe || !ye) { + + // Either Infinity? + if (!xc || !yc) return xc ? (y.s = -b, y) : new BigNumber(yc ? x : NaN); + + // Either zero? + if (!xc[0] || !yc[0]) { + + // Return y if y is non-zero, x if x is non-zero, or zero if both are zero. + return yc[0] ? (y.s = -b, y) : new BigNumber(xc[0] ? x : + + // IEEE 754 (2008) 6.3: n - n = -0 when rounding to -Infinity + ROUNDING_MODE == 3 ? -0 : 0); + } + } + + xe = bitFloor(xe); + ye = bitFloor(ye); + xc = xc.slice(); + + // Determine which is the bigger number. + if (a = xe - ye) { + + if (xLTy = a < 0) { + a = -a; + t = xc; + } else { + ye = xe; + t = yc; + } + + t.reverse(); + + // Prepend zeros to equalise exponents. + for (b = a; b--; t.push(0)); + t.reverse(); + } else { + + // Exponents equal. Check digit by digit. + j = (xLTy = (a = xc.length) < (b = yc.length)) ? a : b; + + for (a = b = 0; b < j; b++) { + + if (xc[b] != yc[b]) { + xLTy = xc[b] < yc[b]; + break; + } + } + } + + // x < y? Point xc to the array of the bigger number. + if (xLTy) { + t = xc; + xc = yc; + yc = t; + y.s = -y.s; + } + + b = (j = yc.length) - (i = xc.length); + + // Append zeros to xc if shorter. + // No need to add zeros to yc if shorter as subtract only needs to start at yc.length. + if (b > 0) for (; b--; xc[i++] = 0); + b = BASE - 1; + + // Subtract yc from xc. + for (; j > a;) { + + if (xc[--j] < yc[j]) { + for (i = j; i && !xc[--i]; xc[i] = b); + --xc[i]; + xc[j] += BASE; + } + + xc[j] -= yc[j]; + } + + // Remove leading zeros and adjust exponent accordingly. + for (; xc[0] == 0; xc.splice(0, 1), --ye); + + // Zero? + if (!xc[0]) { + + // Following IEEE 754 (2008) 6.3, + // n - n = +0 but n - n = -0 when rounding towards -Infinity. + y.s = ROUNDING_MODE == 3 ? -1 : 1; + y.c = [y.e = 0]; + return y; + } + + // No need to check for Infinity as +x - +y != Infinity && -x - -y != Infinity + // for finite x and y. + return normalise(y, xc, ye); + }; + + + /* + * n % 0 = N + * n % N = N + * n % I = n + * 0 % n = 0 + * -0 % n = -0 + * 0 % 0 = N + * 0 % N = N + * 0 % I = 0 + * N % n = N + * N % 0 = N + * N % N = N + * N % I = N + * I % n = N + * I % 0 = N + * I % N = N + * I % I = N + * + * Return a new BigNumber whose value is the value of this BigNumber modulo the value of + * BigNumber(y, b). The result depends on the value of MODULO_MODE. + */ + P.modulo = P.mod = function (y, b) { + var q, s, + x = this; + + y = new BigNumber(y, b); + + // Return NaN if x is Infinity or NaN, or y is NaN or zero. + if (!x.c || !y.s || y.c && !y.c[0]) { + return new BigNumber(NaN); + + // Return x if y is Infinity or x is zero. + } else if (!y.c || x.c && !x.c[0]) { + return new BigNumber(x); + } + + if (MODULO_MODE == 9) { + + // Euclidian division: q = sign(y) * floor(x / abs(y)) + // r = x - qy where 0 <= r < abs(y) + s = y.s; + y.s = 1; + q = div(x, y, 0, 3); + y.s = s; + q.s *= s; + } else { + q = div(x, y, 0, MODULO_MODE); + } + + y = x.minus(q.times(y)); + + // To match JavaScript %, ensure sign of zero is sign of dividend. + if (!y.c[0] && MODULO_MODE == 1) y.s = x.s; + + return y; + }; + + + /* + * n * 0 = 0 + * n * N = N + * n * I = I + * 0 * n = 0 + * 0 * 0 = 0 + * 0 * N = N + * 0 * I = N + * N * n = N + * N * 0 = N + * N * N = N + * N * I = N + * I * n = I + * I * 0 = N + * I * N = N + * I * I = I + * + * Return a new BigNumber whose value is the value of this BigNumber multiplied by the value + * of BigNumber(y, b). + */ + P.multipliedBy = P.times = function (y, b) { + var c, e, i, j, k, m, xcL, xlo, xhi, ycL, ylo, yhi, zc, + base, sqrtBase, + x = this, + xc = x.c, + yc = (y = new BigNumber(y, b)).c; + + // Either NaN, ±Infinity or ±0? + if (!xc || !yc || !xc[0] || !yc[0]) { + + // Return NaN if either is NaN, or one is 0 and the other is Infinity. + if (!x.s || !y.s || xc && !xc[0] && !yc || yc && !yc[0] && !xc) { + y.c = y.e = y.s = null; + } else { + y.s *= x.s; + + // Return ±Infinity if either is ±Infinity. + if (!xc || !yc) { + y.c = y.e = null; + + // Return ±0 if either is ±0. + } else { + y.c = [0]; + y.e = 0; + } + } + + return y; + } + + e = bitFloor(x.e / LOG_BASE) + bitFloor(y.e / LOG_BASE); + y.s *= x.s; + xcL = xc.length; + ycL = yc.length; + + // Ensure xc points to longer array and xcL to its length. + if (xcL < ycL) { + zc = xc; + xc = yc; + yc = zc; + i = xcL; + xcL = ycL; + ycL = i; + } + + // Initialise the result array with zeros. + for (i = xcL + ycL, zc = []; i--; zc.push(0)); + + base = BASE; + sqrtBase = SQRT_BASE; + + for (i = ycL; --i >= 0;) { + c = 0; + ylo = yc[i] % sqrtBase; + yhi = yc[i] / sqrtBase | 0; + + for (k = xcL, j = i + k; j > i;) { + xlo = xc[--k] % sqrtBase; + xhi = xc[k] / sqrtBase | 0; + m = yhi * xlo + xhi * ylo; + xlo = ylo * xlo + ((m % sqrtBase) * sqrtBase) + zc[j] + c; + c = (xlo / base | 0) + (m / sqrtBase | 0) + yhi * xhi; + zc[j--] = xlo % base; + } + + zc[j] = c; + } + + if (c) { + ++e; + } else { + zc.splice(0, 1); + } + + return normalise(y, zc, e); + }; + + + /* + * Return a new BigNumber whose value is the value of this BigNumber negated, + * i.e. multiplied by -1. + */ + P.negated = function () { + var x = new BigNumber(this); + x.s = -x.s || null; + return x; + }; + + + /* + * n + 0 = n + * n + N = N + * n + I = I + * 0 + n = n + * 0 + 0 = 0 + * 0 + N = N + * 0 + I = I + * N + n = N + * N + 0 = N + * N + N = N + * N + I = N + * I + n = I + * I + 0 = I + * I + N = N + * I + I = I + * + * Return a new BigNumber whose value is the value of this BigNumber plus the value of + * BigNumber(y, b). + */ + P.plus = function (y, b) { + var t, + x = this, + a = x.s; + + y = new BigNumber(y, b); + b = y.s; + + // Either NaN? + if (!a || !b) return new BigNumber(NaN); + + // Signs differ? + if (a != b) { + y.s = -b; + return x.minus(y); + } + + var xe = x.e / LOG_BASE, + ye = y.e / LOG_BASE, + xc = x.c, + yc = y.c; + + if (!xe || !ye) { + + // Return ±Infinity if either ±Infinity. + if (!xc || !yc) return new BigNumber(a / 0); + + // Either zero? + // Return y if y is non-zero, x if x is non-zero, or zero if both are zero. + if (!xc[0] || !yc[0]) return yc[0] ? y : new BigNumber(xc[0] ? x : a * 0); + } + + xe = bitFloor(xe); + ye = bitFloor(ye); + xc = xc.slice(); + + // Prepend zeros to equalise exponents. Faster to use reverse then do unshifts. + if (a = xe - ye) { + if (a > 0) { + ye = xe; + t = yc; + } else { + a = -a; + t = xc; + } + + t.reverse(); + for (; a--; t.push(0)); + t.reverse(); + } + + a = xc.length; + b = yc.length; + + // Point xc to the longer array, and b to the shorter length. + if (a - b < 0) { + t = yc; + yc = xc; + xc = t; + b = a; + } + + // Only start adding at yc.length - 1 as the further digits of xc can be ignored. + for (a = 0; b;) { + a = (xc[--b] = xc[b] + yc[b] + a) / BASE | 0; + xc[b] = BASE === xc[b] ? 0 : xc[b] % BASE; + } + + if (a) { + xc = [a].concat(xc); + ++ye; + } + + // No need to check for zero, as +x + +y != 0 && -x + -y != 0 + // ye = MAX_EXP + 1 possible + return normalise(y, xc, ye); + }; + + + /* + * If sd is undefined or null or true or false, return the number of significant digits of + * the value of this BigNumber, or null if the value of this BigNumber is ±Infinity or NaN. + * If sd is true include integer-part trailing zeros in the count. + * + * Otherwise, if sd is a number, return a new BigNumber whose value is the value of this + * BigNumber rounded to a maximum of sd significant digits using rounding mode rm, or + * ROUNDING_MODE if rm is omitted. + * + * sd {number|boolean} number: significant digits: integer, 1 to MAX inclusive. + * boolean: whether to count integer-part trailing zeros: true or false. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}' + */ + P.precision = P.sd = function (sd, rm) { + var c, n, v, + x = this; + + if (sd != null && sd !== !!sd) { + intCheck(sd, 1, MAX); + if (rm == null) rm = ROUNDING_MODE; + else intCheck(rm, 0, 8); + + return round(new BigNumber(x), sd, rm); + } + + if (!(c = x.c)) return null; + v = c.length - 1; + n = v * LOG_BASE + 1; + + if (v = c[v]) { + + // Subtract the number of trailing zeros of the last element. + for (; v % 10 == 0; v /= 10, n--); + + // Add the number of digits of the first element. + for (v = c[0]; v >= 10; v /= 10, n++); + } + + if (sd && x.e + 1 > n) n = x.e + 1; + + return n; + }; + + + /* + * Return a new BigNumber whose value is the value of this BigNumber shifted by k places + * (powers of 10). Shift to the right if n > 0, and to the left if n < 0. + * + * k {number} Integer, -MAX_SAFE_INTEGER to MAX_SAFE_INTEGER inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {k}' + */ + P.shiftedBy = function (k) { + intCheck(k, -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER); + return this.times('1e' + k); + }; + + + /* + * sqrt(-n) = N + * sqrt(N) = N + * sqrt(-I) = N + * sqrt(I) = I + * sqrt(0) = 0 + * sqrt(-0) = -0 + * + * Return a new BigNumber whose value is the square root of the value of this BigNumber, + * rounded according to DECIMAL_PLACES and ROUNDING_MODE. + */ + P.squareRoot = P.sqrt = function () { + var m, n, r, rep, t, + x = this, + c = x.c, + s = x.s, + e = x.e, + dp = DECIMAL_PLACES + 4, + half = new BigNumber('0.5'); + + // Negative/NaN/Infinity/zero? + if (s !== 1 || !c || !c[0]) { + return new BigNumber(!s || s < 0 && (!c || c[0]) ? NaN : c ? x : 1 / 0); + } + + // Initial estimate. + s = Math.sqrt(+valueOf(x)); + + // Math.sqrt underflow/overflow? + // Pass x to Math.sqrt as integer, then adjust the exponent of the result. + if (s == 0 || s == 1 / 0) { + n = coeffToString(c); + if ((n.length + e) % 2 == 0) n += '0'; + s = Math.sqrt(+n); + e = bitFloor((e + 1) / 2) - (e < 0 || e % 2); + + if (s == 1 / 0) { + n = '5e' + e; + } else { + n = s.toExponential(); + n = n.slice(0, n.indexOf('e') + 1) + e; + } + + r = new BigNumber(n); + } else { + r = new BigNumber(s + ''); + } + + // Check for zero. + // r could be zero if MIN_EXP is changed after the this value was created. + // This would cause a division by zero (x/t) and hence Infinity below, which would cause + // coeffToString to throw. + if (r.c[0]) { + e = r.e; + s = e + dp; + if (s < 3) s = 0; + + // Newton-Raphson iteration. + for (; ;) { + t = r; + r = half.times(t.plus(div(x, t, dp, 1))); + + if (coeffToString(t.c).slice(0, s) === (n = coeffToString(r.c)).slice(0, s)) { + + // The exponent of r may here be one less than the final result exponent, + // e.g 0.0009999 (e-4) --> 0.001 (e-3), so adjust s so the rounding digits + // are indexed correctly. + if (r.e < e) --s; + n = n.slice(s - 3, s + 1); + + // The 4th rounding digit may be in error by -1 so if the 4 rounding digits + // are 9999 or 4999 (i.e. approaching a rounding boundary) continue the + // iteration. + if (n == '9999' || !rep && n == '4999') { + + // On the first iteration only, check to see if rounding up gives the + // exact result as the nines may infinitely repeat. + if (!rep) { + round(t, t.e + DECIMAL_PLACES + 2, 0); + + if (t.times(t).eq(x)) { + r = t; + break; + } + } + + dp += 4; + s += 4; + rep = 1; + } else { + + // If rounding digits are null, 0{0,4} or 50{0,3}, check for exact + // result. If not, then there are further digits and m will be truthy. + if (!+n || !+n.slice(1) && n.charAt(0) == '5') { + + // Truncate to the first rounding digit. + round(r, r.e + DECIMAL_PLACES + 2, 1); + m = !r.times(r).eq(x); + } + + break; + } + } + } + } + + return round(r, r.e + DECIMAL_PLACES + 1, ROUNDING_MODE, m); + }; + + + /* + * Return a string representing the value of this BigNumber in exponential notation and + * rounded using ROUNDING_MODE to dp fixed decimal places. + * + * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' + */ + P.toExponential = function (dp, rm) { + if (dp != null) { + intCheck(dp, 0, MAX); + dp++; + } + return format(this, dp, rm, 1); + }; + + + /* + * Return a string representing the value of this BigNumber in fixed-point notation rounding + * to dp fixed decimal places using rounding mode rm, or ROUNDING_MODE if rm is omitted. + * + * Note: as with JavaScript's number type, (-0).toFixed(0) is '0', + * but e.g. (-0.00001).toFixed(0) is '-0'. + * + * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' + */ + P.toFixed = function (dp, rm) { + if (dp != null) { + intCheck(dp, 0, MAX); + dp = dp + this.e + 1; + } + return format(this, dp, rm); + }; + + + /* + * Return a string representing the value of this BigNumber in fixed-point notation rounded + * using rm or ROUNDING_MODE to dp decimal places, and formatted according to the properties + * of the format or FORMAT object (see BigNumber.set). + * + * The formatting object may contain some or all of the properties shown below. + * + * FORMAT = { + * prefix: '', + * groupSize: 3, + * secondaryGroupSize: 0, + * groupSeparator: ',', + * decimalSeparator: '.', + * fractionGroupSize: 0, + * fractionGroupSeparator: '\xA0', // non-breaking space + * suffix: '' + * }; + * + * [dp] {number} Decimal places. Integer, 0 to MAX inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * [format] {object} Formatting options. See FORMAT pbject above. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}' + * '[BigNumber Error] Argument not an object: {format}' + */ + P.toFormat = function (dp, rm, format) { + var str, + x = this; + + if (format == null) { + if (dp != null && rm && typeof rm == 'object') { + format = rm; + rm = null; + } else if (dp && typeof dp == 'object') { + format = dp; + dp = rm = null; + } else { + format = FORMAT; + } + } else if (typeof format != 'object') { + throw Error + (bignumberError + 'Argument not an object: ' + format); + } + + str = x.toFixed(dp, rm); + + if (x.c) { + var i, + arr = str.split('.'), + g1 = +format.groupSize, + g2 = +format.secondaryGroupSize, + groupSeparator = format.groupSeparator || '', + intPart = arr[0], + fractionPart = arr[1], + isNeg = x.s < 0, + intDigits = isNeg ? intPart.slice(1) : intPart, + len = intDigits.length; + + if (g2) { + i = g1; + g1 = g2; + g2 = i; + len -= i; + } + + if (g1 > 0 && len > 0) { + i = len % g1 || g1; + intPart = intDigits.substr(0, i); + for (; i < len; i += g1) intPart += groupSeparator + intDigits.substr(i, g1); + if (g2 > 0) intPart += groupSeparator + intDigits.slice(i); + if (isNeg) intPart = '-' + intPart; + } + + str = fractionPart + ? intPart + (format.decimalSeparator || '') + ((g2 = +format.fractionGroupSize) + ? fractionPart.replace(new RegExp('\\d{' + g2 + '}\\B', 'g'), + '$&' + (format.fractionGroupSeparator || '')) + : fractionPart) + : intPart; + } + + return (format.prefix || '') + str + (format.suffix || ''); + }; + + + /* + * Return an array of two BigNumbers representing the value of this BigNumber as a simple + * fraction with an integer numerator and an integer denominator. + * The denominator will be a positive non-zero value less than or equal to the specified + * maximum denominator. If a maximum denominator is not specified, the denominator will be + * the lowest value necessary to represent the number exactly. + * + * [md] {number|string|BigNumber} Integer >= 1, or Infinity. The maximum denominator. + * + * '[BigNumber Error] Argument {not an integer|out of range} : {md}' + */ + P.toFraction = function (md) { + var d, d0, d1, d2, e, exp, n, n0, n1, q, r, s, + x = this, + xc = x.c; + + if (md != null) { + n = new BigNumber(md); + + // Throw if md is less than one or is not an integer, unless it is Infinity. + if (!n.isInteger() && (n.c || n.s !== 1) || n.lt(ONE)) { + throw Error + (bignumberError + 'Argument ' + + (n.isInteger() ? 'out of range: ' : 'not an integer: ') + valueOf(n)); + } + } + + if (!xc) return new BigNumber(x); + + d = new BigNumber(ONE); + n1 = d0 = new BigNumber(ONE); + d1 = n0 = new BigNumber(ONE); + s = coeffToString(xc); + + // Determine initial denominator. + // d is a power of 10 and the minimum max denominator that specifies the value exactly. + e = d.e = s.length - x.e - 1; + d.c[0] = POWS_TEN[(exp = e % LOG_BASE) < 0 ? LOG_BASE + exp : exp]; + md = !md || n.comparedTo(d) > 0 ? (e > 0 ? d : n1) : n; + + exp = MAX_EXP; + MAX_EXP = 1 / 0; + n = new BigNumber(s); + + // n0 = d1 = 0 + n0.c[0] = 0; + + for (; ;) { + q = div(n, d, 0, 1); + d2 = d0.plus(q.times(d1)); + if (d2.comparedTo(md) == 1) break; + d0 = d1; + d1 = d2; + n1 = n0.plus(q.times(d2 = n1)); + n0 = d2; + d = n.minus(q.times(d2 = d)); + n = d2; + } + + d2 = div(md.minus(d0), d1, 0, 1); + n0 = n0.plus(d2.times(n1)); + d0 = d0.plus(d2.times(d1)); + n0.s = n1.s = x.s; + e = e * 2; + + // Determine which fraction is closer to x, n0/d0 or n1/d1 + r = div(n1, d1, e, ROUNDING_MODE).minus(x).abs().comparedTo( + div(n0, d0, e, ROUNDING_MODE).minus(x).abs()) < 1 ? [n1, d1] : [n0, d0]; + + MAX_EXP = exp; + + return r; + }; + + + /* + * Return the value of this BigNumber converted to a number primitive. + */ + P.toNumber = function () { + return +valueOf(this); + }; + + + /* + * Return a string representing the value of this BigNumber rounded to sd significant digits + * using rounding mode rm or ROUNDING_MODE. If sd is less than the number of digits + * necessary to represent the integer part of the value in fixed-point notation, then use + * exponential notation. + * + * [sd] {number} Significant digits. Integer, 1 to MAX inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}' + */ + P.toPrecision = function (sd, rm) { + if (sd != null) intCheck(sd, 1, MAX); + return format(this, sd, rm, 2); + }; + + + /* + * Return a string representing the value of this BigNumber in base b, or base 10 if b is + * omitted. If a base is specified, including base 10, round according to DECIMAL_PLACES and + * ROUNDING_MODE. If a base is not specified, and this BigNumber has a positive exponent + * that is equal to or greater than TO_EXP_POS, or a negative exponent equal to or less than + * TO_EXP_NEG, return exponential notation. + * + * [b] {number} Integer, 2 to ALPHABET.length inclusive. + * + * '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}' + */ + P.toString = function (b) { + var str, + n = this, + s = n.s, + e = n.e; + + // Infinity or NaN? + if (e === null) { + if (s) { + str = 'Infinity'; + if (s < 0) str = '-' + str; + } else { + str = 'NaN'; + } + } else { + if (b == null) { + str = e <= TO_EXP_NEG || e >= TO_EXP_POS + ? toExponential(coeffToString(n.c), e) + : toFixedPoint(coeffToString(n.c), e, '0'); + } else if (b === 10 && alphabetHasNormalDecimalDigits) { + n = round(new BigNumber(n), DECIMAL_PLACES + e + 1, ROUNDING_MODE); + str = toFixedPoint(coeffToString(n.c), n.e, '0'); + } else { + intCheck(b, 2, ALPHABET.length, 'Base'); + str = convertBase(toFixedPoint(coeffToString(n.c), e, '0'), 10, b, s, true); + } + + if (s < 0 && n.c[0]) str = '-' + str; + } + + return str; + }; + + + /* + * Return as toString, but do not accept a base argument, and include the minus sign for + * negative zero. + */ + P.valueOf = P.toJSON = function () { + return valueOf(this); + }; + + + P._isBigNumber = true; + + if (configObject != null) BigNumber.set(configObject); + + return BigNumber; + } + + + // PRIVATE HELPER FUNCTIONS + + // These functions don't need access to variables, + // e.g. DECIMAL_PLACES, in the scope of the `clone` function above. + + + function bitFloor(n) { + var i = n | 0; + return n > 0 || n === i ? i : i - 1; + } + + + // Return a coefficient array as a string of base 10 digits. + function coeffToString(a) { + var s, z, + i = 1, + j = a.length, + r = a[0] + ''; + + for (; i < j;) { + s = a[i++] + ''; + z = LOG_BASE - s.length; + for (; z--; s = '0' + s); + r += s; + } + + // Determine trailing zeros. + for (j = r.length; r.charCodeAt(--j) === 48;); + + return r.slice(0, j + 1 || 1); + } + + + // Compare the value of BigNumbers x and y. + function compare(x, y) { + var a, b, + xc = x.c, + yc = y.c, + i = x.s, + j = y.s, + k = x.e, + l = y.e; + + // Either NaN? + if (!i || !j) return null; + + a = xc && !xc[0]; + b = yc && !yc[0]; + + // Either zero? + if (a || b) return a ? b ? 0 : -j : i; + + // Signs differ? + if (i != j) return i; + + a = i < 0; + b = k == l; + + // Either Infinity? + if (!xc || !yc) return b ? 0 : !xc ^ a ? 1 : -1; + + // Compare exponents. + if (!b) return k > l ^ a ? 1 : -1; + + j = (k = xc.length) < (l = yc.length) ? k : l; + + // Compare digit by digit. + for (i = 0; i < j; i++) if (xc[i] != yc[i]) return xc[i] > yc[i] ^ a ? 1 : -1; + + // Compare lengths. + return k == l ? 0 : k > l ^ a ? 1 : -1; + } + + + /* + * Check that n is a primitive number, an integer, and in range, otherwise throw. + */ + function intCheck(n, min, max, name) { + if (n < min || n > max || n !== mathfloor(n)) { + throw Error + (bignumberError + (name || 'Argument') + (typeof n == 'number' + ? n < min || n > max ? ' out of range: ' : ' not an integer: ' + : ' not a primitive number: ') + String(n)); + } + } + + + // Assumes finite n. + function isOdd(n) { + var k = n.c.length - 1; + return bitFloor(n.e / LOG_BASE) == k && n.c[k] % 2 != 0; + } + + + function toExponential(str, e) { + return (str.length > 1 ? str.charAt(0) + '.' + str.slice(1) : str) + + (e < 0 ? 'e' : 'e+') + e; + } + + + function toFixedPoint(str, e, z) { + var len, zs; + + // Negative exponent? + if (e < 0) { + + // Prepend zeros. + for (zs = z + '.'; ++e; zs += z); + str = zs + str; + + // Positive exponent + } else { + len = str.length; + + // Append zeros. + if (++e > len) { + for (zs = z, e -= len; --e; zs += z); + str += zs; + } else if (e < len) { + str = str.slice(0, e) + '.' + str.slice(e); + } + } + + return str; + } + + + // EXPORT + + + BigNumber = clone(); + BigNumber['default'] = BigNumber.BigNumber = BigNumber; + + // AMD. + if (typeof define == 'function' && define.amd) { + define(function () { return BigNumber; }); + + // Node.js and other environments that support module.exports. + } else if ( true && module.exports) { + module.exports = BigNumber; + + // Browser. + } else { + if (!globalObject) { + globalObject = typeof self != 'undefined' && self ? self : window; + } + + globalObject.BigNumber = BigNumber; + } +})(this); /***/ }), @@ -11868,7 +11868,7 @@ _a = Gaxios, _Gaxios_instances = new WeakSet(), _Gaxios_urlMayUseProxy = functio } } return true; -}, _Gaxios_applyRequestInterceptors = +}, _Gaxios_applyRequestInterceptors = /** * Applies the request interceptors. The request interceptors are applied after the * call to prepareRequest is completed. @@ -11885,7 +11885,7 @@ async function _Gaxios_applyRequestInterceptors(options) { } } return promiseChain; -}, _Gaxios_applyResponseInterceptors = +}, _Gaxios_applyResponseInterceptors = /** * Applies the response interceptors. The response interceptors are applied after the * call to request is made. @@ -11902,7 +11902,7 @@ async function _Gaxios_applyResponseInterceptors(response) { } } return promiseChain; -}, _Gaxios_prepareRequest = +}, _Gaxios_prepareRequest = /** * Validates the options, merges them with defaults, and prepare request. * @@ -15778,7 +15778,7 @@ async function isAvailable() { // runtime environment. We use the same promise for each of these calls // to reduce the network load. if (cachedIsAvailableResponse === undefined) { - cachedIsAvailableResponse = metadataAccessor('instance', undefined, detectGCPAvailableRetries(), + cachedIsAvailableResponse = metadataAccessor('instance', undefined, detectGCPAvailableRetries(), // If the default HOST_ADDRESS has been overridden, we should not // make an effort to try SECONDARY_HOST_ADDRESS (as we are likely in // a non-GCP environment): @@ -16795,7 +16795,7 @@ async function generateAuthenticationHeaderMap(options) { // Header keys need to be sorted alphabetically. const amzHeaders = Object.assign({ host: options.host, - }, + }, // Previously the date was not fixed with x-amz- and could be provided manually. // https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req reformattedAdditionalAmzHeaders.date ? {} : { 'x-amz-date': amzDate }, reformattedAdditionalAmzHeaders); @@ -17568,7 +17568,7 @@ class DefaultAwsSecurityCredentialsSupplier { } } exports.DefaultAwsSecurityCredentialsSupplier = DefaultAwsSecurityCredentialsSupplier; -_DefaultAwsSecurityCredentialsSupplier_instances = new WeakSet(), _DefaultAwsSecurityCredentialsSupplier_getImdsV2SessionToken = +_DefaultAwsSecurityCredentialsSupplier_instances = new WeakSet(), _DefaultAwsSecurityCredentialsSupplier_getImdsV2SessionToken = /** * @param transporter The transporter to use for requests. * @return A promise that resolves with the IMDSv2 Session Token. @@ -17583,7 +17583,7 @@ async function _DefaultAwsSecurityCredentialsSupplier_getImdsV2SessionToken(tran }; const response = await transporter.request(opts); return response.data; -}, _DefaultAwsSecurityCredentialsSupplier_getAwsRoleName = +}, _DefaultAwsSecurityCredentialsSupplier_getAwsRoleName = /** * @param headers The headers to be used in the metadata request. * @param transporter The transporter to use for requests. @@ -17604,7 +17604,7 @@ async function _DefaultAwsSecurityCredentialsSupplier_getAwsRoleName(headers, tr }; const response = await transporter.request(opts); return response.data; -}, _DefaultAwsSecurityCredentialsSupplier_retrieveAwsSecurityCredentials = +}, _DefaultAwsSecurityCredentialsSupplier_retrieveAwsSecurityCredentials = /** * Retrieves the temporary AWS credentials by calling the security-credentials * endpoint as specified in the `credential_source` object. @@ -18248,7 +18248,7 @@ class ExternalAccountAuthorizedUserHandler extends oauth2common_1.OAuthClientAut catch (error) { // Translate error to OAuthError. if (error instanceof gaxios_1.GaxiosError && error.response) { - throw (0, oauth2common_1.getErrorFromOAuthErrorResponse)(error.response.data, + throw (0, oauth2common_1.getErrorFromOAuthErrorResponse)(error.response.data, // Preserve other fields from the original error. error); } @@ -20117,7 +20117,7 @@ class JWT extends oauth2client_1.OAuth2Client { } const useScopes = this.useJWTAccessWithScope || this.universeDomain !== authclient_1.DEFAULT_UNIVERSE; - const headers = await this.access.getRequestHeaders(url !== null && url !== void 0 ? url : undefined, this.additionalClaims, + const headers = await this.access.getRequestHeaders(url !== null && url !== void 0 ? url : undefined, this.additionalClaims, // Scopes take precedent over audience for signing, // so we only provide them if `useJWTAccessWithScope` is on or // if we are in a non-default universe @@ -22022,7 +22022,7 @@ class StsCredentials extends oauth2common_1.OAuthClientAuthHandler { * @return A promise that resolves with the token exchange response containing * the requested token and its expiration time. */ - async exchangeToken(stsCredentialsOptions, additionalHeaders, + async exchangeToken(stsCredentialsOptions, additionalHeaders, // eslint-disable-next-line @typescript-eslint/no-explicit-any options) { var _a, _b, _c; @@ -22072,7 +22072,7 @@ class StsCredentials extends oauth2common_1.OAuthClientAuthHandler { catch (error) { // Translate error to OAuthError. if (error instanceof gaxios_1.GaxiosError && error.response) { - throw (0, oauth2common_1.getErrorFromOAuthErrorResponse)(error.response.data, + throw (0, oauth2common_1.getErrorFromOAuthErrorResponse)(error.response.data, // Preserve other fields from the original error. error); } @@ -22824,7 +22824,7 @@ _LRUCache_cache = new WeakMap(), _LRUCache_instances = new WeakSet(), _LRUCache_ // limitations under the License. Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getAPI = void 0; -function getAPI(api, options, +function getAPI(api, options, // eslint-disable-next-line @typescript-eslint/no-explicit-any versions, context) { let version; @@ -160439,7 +160439,7 @@ var businessprofileperformance_v1; callback = optionsOrCallback; options = {}; } - const rootUrl = options.rootUrl || 'https://businessprofilemargoogleapis.com/'; + const rootUrl = options.rootUrl || 'https://businessprofileperformance.googleapis.com/'; const parameters = { options: Object.assign({ url: (rootUrl + '/v1/{+location}:fetchMultiDailyMetricsTimeSeries').replace(/([^:]\/)\/+/g, '$1'), @@ -727886,7 +727886,7 @@ _GoogleToken_inFlightRequest = new WeakMap(), _GoogleToken_instances = new WeakS if (options.transporter) { this.transporter = options.transporter; } -}, _GoogleToken_requestToken = +}, _GoogleToken_requestToken = /** * Request the token from Google. */ @@ -735336,1303 +735336,1303 @@ exports.parseURL = __nccwpck_require__(2158).parseURL; /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; - -const punycode = __nccwpck_require__(85477); -const tr46 = __nccwpck_require__(84256); - -const specialSchemes = { - ftp: 21, - file: null, - gopher: 70, - http: 80, - https: 443, - ws: 80, - wss: 443 -}; - -const failure = Symbol("failure"); - -function countSymbols(str) { - return punycode.ucs2.decode(str).length; -} - -function at(input, idx) { - const c = input[idx]; - return isNaN(c) ? undefined : String.fromCodePoint(c); -} - -function isASCIIDigit(c) { - return c >= 0x30 && c <= 0x39; -} - -function isASCIIAlpha(c) { - return (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A); -} - -function isASCIIAlphanumeric(c) { - return isASCIIAlpha(c) || isASCIIDigit(c); -} - -function isASCIIHex(c) { - return isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66); -} - -function isSingleDot(buffer) { - return buffer === "." || buffer.toLowerCase() === "%2e"; -} - -function isDoubleDot(buffer) { - buffer = buffer.toLowerCase(); - return buffer === ".." || buffer === "%2e." || buffer === ".%2e" || buffer === "%2e%2e"; -} - -function isWindowsDriveLetterCodePoints(cp1, cp2) { - return isASCIIAlpha(cp1) && (cp2 === 58 || cp2 === 124); -} - -function isWindowsDriveLetterString(string) { - return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && (string[1] === ":" || string[1] === "|"); -} - -function isNormalizedWindowsDriveLetterString(string) { - return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && string[1] === ":"; -} - -function containsForbiddenHostCodePoint(string) { - return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|%|\/|:|\?|@|\[|\\|\]/) !== -1; -} - -function containsForbiddenHostCodePointExcludingPercent(string) { - return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|\/|:|\?|@|\[|\\|\]/) !== -1; -} - -function isSpecialScheme(scheme) { - return specialSchemes[scheme] !== undefined; -} - -function isSpecial(url) { - return isSpecialScheme(url.scheme); -} - -function defaultPort(scheme) { - return specialSchemes[scheme]; -} - -function percentEncode(c) { - let hex = c.toString(16).toUpperCase(); - if (hex.length === 1) { - hex = "0" + hex; - } - - return "%" + hex; -} - -function utf8PercentEncode(c) { - const buf = new Buffer(c); - - let str = ""; - - for (let i = 0; i < buf.length; ++i) { - str += percentEncode(buf[i]); - } - - return str; -} - -function utf8PercentDecode(str) { - const input = new Buffer(str); - const output = []; - for (let i = 0; i < input.length; ++i) { - if (input[i] !== 37) { - output.push(input[i]); - } else if (input[i] === 37 && isASCIIHex(input[i + 1]) && isASCIIHex(input[i + 2])) { - output.push(parseInt(input.slice(i + 1, i + 3).toString(), 16)); - i += 2; - } else { - output.push(input[i]); - } - } - return new Buffer(output).toString(); -} - -function isC0ControlPercentEncode(c) { - return c <= 0x1F || c > 0x7E; -} - -const extraPathPercentEncodeSet = new Set([32, 34, 35, 60, 62, 63, 96, 123, 125]); -function isPathPercentEncode(c) { - return isC0ControlPercentEncode(c) || extraPathPercentEncodeSet.has(c); -} - -const extraUserinfoPercentEncodeSet = - new Set([47, 58, 59, 61, 64, 91, 92, 93, 94, 124]); -function isUserinfoPercentEncode(c) { - return isPathPercentEncode(c) || extraUserinfoPercentEncodeSet.has(c); -} - -function percentEncodeChar(c, encodeSetPredicate) { - const cStr = String.fromCodePoint(c); - - if (encodeSetPredicate(c)) { - return utf8PercentEncode(cStr); - } - - return cStr; -} - -function parseIPv4Number(input) { - let R = 10; - - if (input.length >= 2 && input.charAt(0) === "0" && input.charAt(1).toLowerCase() === "x") { - input = input.substring(2); - R = 16; - } else if (input.length >= 2 && input.charAt(0) === "0") { - input = input.substring(1); - R = 8; - } - - if (input === "") { - return 0; - } - - const regex = R === 10 ? /[^0-9]/ : (R === 16 ? /[^0-9A-Fa-f]/ : /[^0-7]/); - if (regex.test(input)) { - return failure; - } - - return parseInt(input, R); -} - -function parseIPv4(input) { - const parts = input.split("."); - if (parts[parts.length - 1] === "") { - if (parts.length > 1) { - parts.pop(); - } - } - - if (parts.length > 4) { - return input; - } - - const numbers = []; - for (const part of parts) { - if (part === "") { - return input; - } - const n = parseIPv4Number(part); - if (n === failure) { - return input; - } - - numbers.push(n); - } - - for (let i = 0; i < numbers.length - 1; ++i) { - if (numbers[i] > 255) { - return failure; - } - } - if (numbers[numbers.length - 1] >= Math.pow(256, 5 - numbers.length)) { - return failure; - } - - let ipv4 = numbers.pop(); - let counter = 0; - - for (const n of numbers) { - ipv4 += n * Math.pow(256, 3 - counter); - ++counter; - } - - return ipv4; -} - -function serializeIPv4(address) { - let output = ""; - let n = address; - - for (let i = 1; i <= 4; ++i) { - output = String(n % 256) + output; - if (i !== 4) { - output = "." + output; - } - n = Math.floor(n / 256); - } - - return output; -} - -function parseIPv6(input) { - const address = [0, 0, 0, 0, 0, 0, 0, 0]; - let pieceIndex = 0; - let compress = null; - let pointer = 0; - - input = punycode.ucs2.decode(input); - - if (input[pointer] === 58) { - if (input[pointer + 1] !== 58) { - return failure; - } - - pointer += 2; - ++pieceIndex; - compress = pieceIndex; - } - - while (pointer < input.length) { - if (pieceIndex === 8) { - return failure; - } - - if (input[pointer] === 58) { - if (compress !== null) { - return failure; - } - ++pointer; - ++pieceIndex; - compress = pieceIndex; - continue; - } - - let value = 0; - let length = 0; - - while (length < 4 && isASCIIHex(input[pointer])) { - value = value * 0x10 + parseInt(at(input, pointer), 16); - ++pointer; - ++length; - } - - if (input[pointer] === 46) { - if (length === 0) { - return failure; - } - - pointer -= length; - - if (pieceIndex > 6) { - return failure; - } - - let numbersSeen = 0; - - while (input[pointer] !== undefined) { - let ipv4Piece = null; - - if (numbersSeen > 0) { - if (input[pointer] === 46 && numbersSeen < 4) { - ++pointer; - } else { - return failure; - } - } - - if (!isASCIIDigit(input[pointer])) { - return failure; - } - - while (isASCIIDigit(input[pointer])) { - const number = parseInt(at(input, pointer)); - if (ipv4Piece === null) { - ipv4Piece = number; - } else if (ipv4Piece === 0) { - return failure; - } else { - ipv4Piece = ipv4Piece * 10 + number; - } - if (ipv4Piece > 255) { - return failure; - } - ++pointer; - } - - address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece; - - ++numbersSeen; - - if (numbersSeen === 2 || numbersSeen === 4) { - ++pieceIndex; - } - } - - if (numbersSeen !== 4) { - return failure; - } - - break; - } else if (input[pointer] === 58) { - ++pointer; - if (input[pointer] === undefined) { - return failure; - } - } else if (input[pointer] !== undefined) { - return failure; - } - - address[pieceIndex] = value; - ++pieceIndex; - } - - if (compress !== null) { - let swaps = pieceIndex - compress; - pieceIndex = 7; - while (pieceIndex !== 0 && swaps > 0) { - const temp = address[compress + swaps - 1]; - address[compress + swaps - 1] = address[pieceIndex]; - address[pieceIndex] = temp; - --pieceIndex; - --swaps; - } - } else if (compress === null && pieceIndex !== 8) { - return failure; - } - - return address; -} - -function serializeIPv6(address) { - let output = ""; - const seqResult = findLongestZeroSequence(address); - const compress = seqResult.idx; - let ignore0 = false; - - for (let pieceIndex = 0; pieceIndex <= 7; ++pieceIndex) { - if (ignore0 && address[pieceIndex] === 0) { - continue; - } else if (ignore0) { - ignore0 = false; - } - - if (compress === pieceIndex) { - const separator = pieceIndex === 0 ? "::" : ":"; - output += separator; - ignore0 = true; - continue; - } - - output += address[pieceIndex].toString(16); - - if (pieceIndex !== 7) { - output += ":"; - } - } - - return output; -} - -function parseHost(input, isSpecialArg) { - if (input[0] === "[") { - if (input[input.length - 1] !== "]") { - return failure; - } - - return parseIPv6(input.substring(1, input.length - 1)); - } - - if (!isSpecialArg) { - return parseOpaqueHost(input); - } - - const domain = utf8PercentDecode(input); - const asciiDomain = tr46.toASCII(domain, false, tr46.PROCESSING_OPTIONS.NONTRANSITIONAL, false); - if (asciiDomain === null) { - return failure; - } - - if (containsForbiddenHostCodePoint(asciiDomain)) { - return failure; - } - - const ipv4Host = parseIPv4(asciiDomain); - if (typeof ipv4Host === "number" || ipv4Host === failure) { - return ipv4Host; - } - - return asciiDomain; -} - -function parseOpaqueHost(input) { - if (containsForbiddenHostCodePointExcludingPercent(input)) { - return failure; - } - - let output = ""; - const decoded = punycode.ucs2.decode(input); - for (let i = 0; i < decoded.length; ++i) { - output += percentEncodeChar(decoded[i], isC0ControlPercentEncode); - } - return output; -} - -function findLongestZeroSequence(arr) { - let maxIdx = null; - let maxLen = 1; // only find elements > 1 - let currStart = null; - let currLen = 0; - - for (let i = 0; i < arr.length; ++i) { - if (arr[i] !== 0) { - if (currLen > maxLen) { - maxIdx = currStart; - maxLen = currLen; - } - - currStart = null; - currLen = 0; - } else { - if (currStart === null) { - currStart = i; - } - ++currLen; - } - } - - // if trailing zeros - if (currLen > maxLen) { - maxIdx = currStart; - maxLen = currLen; - } - - return { - idx: maxIdx, - len: maxLen - }; -} - -function serializeHost(host) { - if (typeof host === "number") { - return serializeIPv4(host); - } - - // IPv6 serializer - if (host instanceof Array) { - return "[" + serializeIPv6(host) + "]"; - } - - return host; -} - -function trimControlChars(url) { - return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/g, ""); -} - -function trimTabAndNewline(url) { - return url.replace(/\u0009|\u000A|\u000D/g, ""); -} - -function shortenPath(url) { - const path = url.path; - if (path.length === 0) { - return; - } - if (url.scheme === "file" && path.length === 1 && isNormalizedWindowsDriveLetter(path[0])) { - return; - } - - path.pop(); -} - -function includesCredentials(url) { - return url.username !== "" || url.password !== ""; -} - -function cannotHaveAUsernamePasswordPort(url) { - return url.host === null || url.host === "" || url.cannotBeABaseURL || url.scheme === "file"; -} - -function isNormalizedWindowsDriveLetter(string) { - return /^[A-Za-z]:$/.test(string); -} - -function URLStateMachine(input, base, encodingOverride, url, stateOverride) { - this.pointer = 0; - this.input = input; - this.base = base || null; - this.encodingOverride = encodingOverride || "utf-8"; - this.stateOverride = stateOverride; - this.url = url; - this.failure = false; - this.parseError = false; - - if (!this.url) { - this.url = { - scheme: "", - username: "", - password: "", - host: null, - port: null, - path: [], - query: null, - fragment: null, - - cannotBeABaseURL: false - }; - - const res = trimControlChars(this.input); - if (res !== this.input) { - this.parseError = true; - } - this.input = res; - } - - const res = trimTabAndNewline(this.input); - if (res !== this.input) { - this.parseError = true; - } - this.input = res; - - this.state = stateOverride || "scheme start"; - - this.buffer = ""; - this.atFlag = false; - this.arrFlag = false; - this.passwordTokenSeenFlag = false; - - this.input = punycode.ucs2.decode(this.input); - - for (; this.pointer <= this.input.length; ++this.pointer) { - const c = this.input[this.pointer]; - const cStr = isNaN(c) ? undefined : String.fromCodePoint(c); - - // exec state machine - const ret = this["parse " + this.state](c, cStr); - if (!ret) { - break; // terminate algorithm - } else if (ret === failure) { - this.failure = true; - break; - } - } -} - -URLStateMachine.prototype["parse scheme start"] = function parseSchemeStart(c, cStr) { - if (isASCIIAlpha(c)) { - this.buffer += cStr.toLowerCase(); - this.state = "scheme"; - } else if (!this.stateOverride) { - this.state = "no scheme"; - --this.pointer; - } else { - this.parseError = true; - return failure; - } - - return true; -}; - -URLStateMachine.prototype["parse scheme"] = function parseScheme(c, cStr) { - if (isASCIIAlphanumeric(c) || c === 43 || c === 45 || c === 46) { - this.buffer += cStr.toLowerCase(); - } else if (c === 58) { - if (this.stateOverride) { - if (isSpecial(this.url) && !isSpecialScheme(this.buffer)) { - return false; - } - - if (!isSpecial(this.url) && isSpecialScheme(this.buffer)) { - return false; - } - - if ((includesCredentials(this.url) || this.url.port !== null) && this.buffer === "file") { - return false; - } - - if (this.url.scheme === "file" && (this.url.host === "" || this.url.host === null)) { - return false; - } - } - this.url.scheme = this.buffer; - this.buffer = ""; - if (this.stateOverride) { - return false; - } - if (this.url.scheme === "file") { - if (this.input[this.pointer + 1] !== 47 || this.input[this.pointer + 2] !== 47) { - this.parseError = true; - } - this.state = "file"; - } else if (isSpecial(this.url) && this.base !== null && this.base.scheme === this.url.scheme) { - this.state = "special relative or authority"; - } else if (isSpecial(this.url)) { - this.state = "special authority slashes"; - } else if (this.input[this.pointer + 1] === 47) { - this.state = "path or authority"; - ++this.pointer; - } else { - this.url.cannotBeABaseURL = true; - this.url.path.push(""); - this.state = "cannot-be-a-base-URL path"; - } - } else if (!this.stateOverride) { - this.buffer = ""; - this.state = "no scheme"; - this.pointer = -1; - } else { - this.parseError = true; - return failure; - } - - return true; -}; - -URLStateMachine.prototype["parse no scheme"] = function parseNoScheme(c) { - if (this.base === null || (this.base.cannotBeABaseURL && c !== 35)) { - return failure; - } else if (this.base.cannotBeABaseURL && c === 35) { - this.url.scheme = this.base.scheme; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - this.url.fragment = ""; - this.url.cannotBeABaseURL = true; - this.state = "fragment"; - } else if (this.base.scheme === "file") { - this.state = "file"; - --this.pointer; - } else { - this.state = "relative"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse special relative or authority"] = function parseSpecialRelativeOrAuthority(c) { - if (c === 47 && this.input[this.pointer + 1] === 47) { - this.state = "special authority ignore slashes"; - ++this.pointer; - } else { - this.parseError = true; - this.state = "relative"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse path or authority"] = function parsePathOrAuthority(c) { - if (c === 47) { - this.state = "authority"; - } else { - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse relative"] = function parseRelative(c) { - this.url.scheme = this.base.scheme; - if (isNaN(c)) { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - } else if (c === 47) { - this.state = "relative slash"; - } else if (c === 63) { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.url.path = this.base.path.slice(); - this.url.query = ""; - this.state = "query"; - } else if (c === 35) { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - this.url.fragment = ""; - this.state = "fragment"; - } else if (isSpecial(this.url) && c === 92) { - this.parseError = true; - this.state = "relative slash"; - } else { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.url.path = this.base.path.slice(0, this.base.path.length - 1); - - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse relative slash"] = function parseRelativeSlash(c) { - if (isSpecial(this.url) && (c === 47 || c === 92)) { - if (c === 92) { - this.parseError = true; - } - this.state = "special authority ignore slashes"; - } else if (c === 47) { - this.state = "authority"; - } else { - this.url.username = this.base.username; - this.url.password = this.base.password; - this.url.host = this.base.host; - this.url.port = this.base.port; - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse special authority slashes"] = function parseSpecialAuthoritySlashes(c) { - if (c === 47 && this.input[this.pointer + 1] === 47) { - this.state = "special authority ignore slashes"; - ++this.pointer; - } else { - this.parseError = true; - this.state = "special authority ignore slashes"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse special authority ignore slashes"] = function parseSpecialAuthorityIgnoreSlashes(c) { - if (c !== 47 && c !== 92) { - this.state = "authority"; - --this.pointer; - } else { - this.parseError = true; - } - - return true; -}; - -URLStateMachine.prototype["parse authority"] = function parseAuthority(c, cStr) { - if (c === 64) { - this.parseError = true; - if (this.atFlag) { - this.buffer = "%40" + this.buffer; - } - this.atFlag = true; - - // careful, this is based on buffer and has its own pointer (this.pointer != pointer) and inner chars - const len = countSymbols(this.buffer); - for (let pointer = 0; pointer < len; ++pointer) { - const codePoint = this.buffer.codePointAt(pointer); - - if (codePoint === 58 && !this.passwordTokenSeenFlag) { - this.passwordTokenSeenFlag = true; - continue; - } - const encodedCodePoints = percentEncodeChar(codePoint, isUserinfoPercentEncode); - if (this.passwordTokenSeenFlag) { - this.url.password += encodedCodePoints; - } else { - this.url.username += encodedCodePoints; - } - } - this.buffer = ""; - } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || - (isSpecial(this.url) && c === 92)) { - if (this.atFlag && this.buffer === "") { - this.parseError = true; - return failure; - } - this.pointer -= countSymbols(this.buffer) + 1; - this.buffer = ""; - this.state = "host"; - } else { - this.buffer += cStr; - } - - return true; -}; - -URLStateMachine.prototype["parse hostname"] = -URLStateMachine.prototype["parse host"] = function parseHostName(c, cStr) { - if (this.stateOverride && this.url.scheme === "file") { - --this.pointer; - this.state = "file host"; - } else if (c === 58 && !this.arrFlag) { - if (this.buffer === "") { - this.parseError = true; - return failure; - } - - const host = parseHost(this.buffer, isSpecial(this.url)); - if (host === failure) { - return failure; - } - - this.url.host = host; - this.buffer = ""; - this.state = "port"; - if (this.stateOverride === "hostname") { - return false; - } - } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || - (isSpecial(this.url) && c === 92)) { - --this.pointer; - if (isSpecial(this.url) && this.buffer === "") { - this.parseError = true; - return failure; - } else if (this.stateOverride && this.buffer === "" && - (includesCredentials(this.url) || this.url.port !== null)) { - this.parseError = true; - return false; - } - - const host = parseHost(this.buffer, isSpecial(this.url)); - if (host === failure) { - return failure; - } - - this.url.host = host; - this.buffer = ""; - this.state = "path start"; - if (this.stateOverride) { - return false; - } - } else { - if (c === 91) { - this.arrFlag = true; - } else if (c === 93) { - this.arrFlag = false; - } - this.buffer += cStr; - } - - return true; -}; - -URLStateMachine.prototype["parse port"] = function parsePort(c, cStr) { - if (isASCIIDigit(c)) { - this.buffer += cStr; - } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || - (isSpecial(this.url) && c === 92) || - this.stateOverride) { - if (this.buffer !== "") { - const port = parseInt(this.buffer); - if (port > Math.pow(2, 16) - 1) { - this.parseError = true; - return failure; - } - this.url.port = port === defaultPort(this.url.scheme) ? null : port; - this.buffer = ""; - } - if (this.stateOverride) { - return false; - } - this.state = "path start"; - --this.pointer; - } else { - this.parseError = true; - return failure; - } - - return true; -}; - -const fileOtherwiseCodePoints = new Set([47, 92, 63, 35]); - -URLStateMachine.prototype["parse file"] = function parseFile(c) { - this.url.scheme = "file"; - - if (c === 47 || c === 92) { - if (c === 92) { - this.parseError = true; - } - this.state = "file slash"; - } else if (this.base !== null && this.base.scheme === "file") { - if (isNaN(c)) { - this.url.host = this.base.host; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - } else if (c === 63) { - this.url.host = this.base.host; - this.url.path = this.base.path.slice(); - this.url.query = ""; - this.state = "query"; - } else if (c === 35) { - this.url.host = this.base.host; - this.url.path = this.base.path.slice(); - this.url.query = this.base.query; - this.url.fragment = ""; - this.state = "fragment"; - } else { - if (this.input.length - this.pointer - 1 === 0 || // remaining consists of 0 code points - !isWindowsDriveLetterCodePoints(c, this.input[this.pointer + 1]) || - (this.input.length - this.pointer - 1 >= 2 && // remaining has at least 2 code points - !fileOtherwiseCodePoints.has(this.input[this.pointer + 2]))) { - this.url.host = this.base.host; - this.url.path = this.base.path.slice(); - shortenPath(this.url); - } else { - this.parseError = true; - } - - this.state = "path"; - --this.pointer; - } - } else { - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse file slash"] = function parseFileSlash(c) { - if (c === 47 || c === 92) { - if (c === 92) { - this.parseError = true; - } - this.state = "file host"; - } else { - if (this.base !== null && this.base.scheme === "file") { - if (isNormalizedWindowsDriveLetterString(this.base.path[0])) { - this.url.path.push(this.base.path[0]); - } else { - this.url.host = this.base.host; - } - } - this.state = "path"; - --this.pointer; - } - - return true; -}; - -URLStateMachine.prototype["parse file host"] = function parseFileHost(c, cStr) { - if (isNaN(c) || c === 47 || c === 92 || c === 63 || c === 35) { - --this.pointer; - if (!this.stateOverride && isWindowsDriveLetterString(this.buffer)) { - this.parseError = true; - this.state = "path"; - } else if (this.buffer === "") { - this.url.host = ""; - if (this.stateOverride) { - return false; - } - this.state = "path start"; - } else { - let host = parseHost(this.buffer, isSpecial(this.url)); - if (host === failure) { - return failure; - } - if (host === "localhost") { - host = ""; - } - this.url.host = host; - - if (this.stateOverride) { - return false; - } - - this.buffer = ""; - this.state = "path start"; - } - } else { - this.buffer += cStr; - } - - return true; -}; - -URLStateMachine.prototype["parse path start"] = function parsePathStart(c) { - if (isSpecial(this.url)) { - if (c === 92) { - this.parseError = true; - } - this.state = "path"; - - if (c !== 47 && c !== 92) { - --this.pointer; - } - } else if (!this.stateOverride && c === 63) { - this.url.query = ""; - this.state = "query"; - } else if (!this.stateOverride && c === 35) { - this.url.fragment = ""; - this.state = "fragment"; - } else if (c !== undefined) { - this.state = "path"; - if (c !== 47) { - --this.pointer; - } - } - - return true; -}; - -URLStateMachine.prototype["parse path"] = function parsePath(c) { - if (isNaN(c) || c === 47 || (isSpecial(this.url) && c === 92) || - (!this.stateOverride && (c === 63 || c === 35))) { - if (isSpecial(this.url) && c === 92) { - this.parseError = true; - } - - if (isDoubleDot(this.buffer)) { - shortenPath(this.url); - if (c !== 47 && !(isSpecial(this.url) && c === 92)) { - this.url.path.push(""); - } - } else if (isSingleDot(this.buffer) && c !== 47 && - !(isSpecial(this.url) && c === 92)) { - this.url.path.push(""); - } else if (!isSingleDot(this.buffer)) { - if (this.url.scheme === "file" && this.url.path.length === 0 && isWindowsDriveLetterString(this.buffer)) { - if (this.url.host !== "" && this.url.host !== null) { - this.parseError = true; - this.url.host = ""; - } - this.buffer = this.buffer[0] + ":"; - } - this.url.path.push(this.buffer); - } - this.buffer = ""; - if (this.url.scheme === "file" && (c === undefined || c === 63 || c === 35)) { - while (this.url.path.length > 1 && this.url.path[0] === "") { - this.parseError = true; - this.url.path.shift(); - } - } - if (c === 63) { - this.url.query = ""; - this.state = "query"; - } - if (c === 35) { - this.url.fragment = ""; - this.state = "fragment"; - } - } else { - // TODO: If c is not a URL code point and not "%", parse error. - - if (c === 37 && - (!isASCIIHex(this.input[this.pointer + 1]) || - !isASCIIHex(this.input[this.pointer + 2]))) { - this.parseError = true; - } - - this.buffer += percentEncodeChar(c, isPathPercentEncode); - } - - return true; -}; - -URLStateMachine.prototype["parse cannot-be-a-base-URL path"] = function parseCannotBeABaseURLPath(c) { - if (c === 63) { - this.url.query = ""; - this.state = "query"; - } else if (c === 35) { - this.url.fragment = ""; - this.state = "fragment"; - } else { - // TODO: Add: not a URL code point - if (!isNaN(c) && c !== 37) { - this.parseError = true; - } - - if (c === 37 && - (!isASCIIHex(this.input[this.pointer + 1]) || - !isASCIIHex(this.input[this.pointer + 2]))) { - this.parseError = true; - } - - if (!isNaN(c)) { - this.url.path[0] = this.url.path[0] + percentEncodeChar(c, isC0ControlPercentEncode); - } - } - - return true; -}; - -URLStateMachine.prototype["parse query"] = function parseQuery(c, cStr) { - if (isNaN(c) || (!this.stateOverride && c === 35)) { - if (!isSpecial(this.url) || this.url.scheme === "ws" || this.url.scheme === "wss") { - this.encodingOverride = "utf-8"; - } - - const buffer = new Buffer(this.buffer); // TODO: Use encoding override instead - for (let i = 0; i < buffer.length; ++i) { - if (buffer[i] < 0x21 || buffer[i] > 0x7E || buffer[i] === 0x22 || buffer[i] === 0x23 || - buffer[i] === 0x3C || buffer[i] === 0x3E) { - this.url.query += percentEncode(buffer[i]); - } else { - this.url.query += String.fromCodePoint(buffer[i]); - } - } - - this.buffer = ""; - if (c === 35) { - this.url.fragment = ""; - this.state = "fragment"; - } - } else { - // TODO: If c is not a URL code point and not "%", parse error. - if (c === 37 && - (!isASCIIHex(this.input[this.pointer + 1]) || - !isASCIIHex(this.input[this.pointer + 2]))) { - this.parseError = true; - } - - this.buffer += cStr; - } - - return true; -}; - -URLStateMachine.prototype["parse fragment"] = function parseFragment(c) { - if (isNaN(c)) { // do nothing - } else if (c === 0x0) { - this.parseError = true; - } else { - // TODO: If c is not a URL code point and not "%", parse error. - if (c === 37 && - (!isASCIIHex(this.input[this.pointer + 1]) || - !isASCIIHex(this.input[this.pointer + 2]))) { - this.parseError = true; - } - - this.url.fragment += percentEncodeChar(c, isC0ControlPercentEncode); - } - - return true; -}; - -function serializeURL(url, excludeFragment) { - let output = url.scheme + ":"; - if (url.host !== null) { - output += "//"; - - if (url.username !== "" || url.password !== "") { - output += url.username; - if (url.password !== "") { - output += ":" + url.password; - } - output += "@"; - } - - output += serializeHost(url.host); - - if (url.port !== null) { - output += ":" + url.port; - } - } else if (url.host === null && url.scheme === "file") { - output += "//"; - } - - if (url.cannotBeABaseURL) { - output += url.path[0]; - } else { - for (const string of url.path) { - output += "/" + string; - } - } - - if (url.query !== null) { - output += "?" + url.query; - } - - if (!excludeFragment && url.fragment !== null) { - output += "#" + url.fragment; - } - - return output; -} - -function serializeOrigin(tuple) { - let result = tuple.scheme + "://"; - result += serializeHost(tuple.host); - - if (tuple.port !== null) { - result += ":" + tuple.port; - } - - return result; -} - -module.exports.serializeURL = serializeURL; - -module.exports.serializeURLOrigin = function (url) { - // https://url.spec.whatwg.org/#concept-url-origin - switch (url.scheme) { - case "blob": - try { - return module.exports.serializeURLOrigin(module.exports.parseURL(url.path[0])); - } catch (e) { - // serializing an opaque origin returns "null" - return "null"; - } - case "ftp": - case "gopher": - case "http": - case "https": - case "ws": - case "wss": - return serializeOrigin({ - scheme: url.scheme, - host: url.host, - port: url.port - }); - case "file": - // spec says "exercise to the reader", chrome says "file://" - return "file://"; - default: - // serializing an opaque origin returns "null" - return "null"; - } -}; - -module.exports.basicURLParse = function (input, options) { - if (options === undefined) { - options = {}; - } - - const usm = new URLStateMachine(input, options.baseURL, options.encodingOverride, options.url, options.stateOverride); - if (usm.failure) { - return "failure"; - } - - return usm.url; -}; - -module.exports.setTheUsername = function (url, username) { - url.username = ""; - const decoded = punycode.ucs2.decode(username); - for (let i = 0; i < decoded.length; ++i) { - url.username += percentEncodeChar(decoded[i], isUserinfoPercentEncode); - } -}; - -module.exports.setThePassword = function (url, password) { - url.password = ""; - const decoded = punycode.ucs2.decode(password); - for (let i = 0; i < decoded.length; ++i) { - url.password += percentEncodeChar(decoded[i], isUserinfoPercentEncode); - } -}; - -module.exports.serializeHost = serializeHost; - -module.exports.cannotHaveAUsernamePasswordPort = cannotHaveAUsernamePasswordPort; - -module.exports.serializeInteger = function (integer) { - return String(integer); -}; - -module.exports.parseURL = function (input, options) { - if (options === undefined) { - options = {}; - } - - // We don't handle blobs, so this just delegates: - return module.exports.basicURLParse(input, { baseURL: options.baseURL, encodingOverride: options.encodingOverride }); -}; + +const punycode = __nccwpck_require__(85477); +const tr46 = __nccwpck_require__(84256); + +const specialSchemes = { + ftp: 21, + file: null, + gopher: 70, + http: 80, + https: 443, + ws: 80, + wss: 443 +}; + +const failure = Symbol("failure"); + +function countSymbols(str) { + return punycode.ucs2.decode(str).length; +} + +function at(input, idx) { + const c = input[idx]; + return isNaN(c) ? undefined : String.fromCodePoint(c); +} + +function isASCIIDigit(c) { + return c >= 0x30 && c <= 0x39; +} + +function isASCIIAlpha(c) { + return (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A); +} + +function isASCIIAlphanumeric(c) { + return isASCIIAlpha(c) || isASCIIDigit(c); +} + +function isASCIIHex(c) { + return isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66); +} + +function isSingleDot(buffer) { + return buffer === "." || buffer.toLowerCase() === "%2e"; +} + +function isDoubleDot(buffer) { + buffer = buffer.toLowerCase(); + return buffer === ".." || buffer === "%2e." || buffer === ".%2e" || buffer === "%2e%2e"; +} + +function isWindowsDriveLetterCodePoints(cp1, cp2) { + return isASCIIAlpha(cp1) && (cp2 === 58 || cp2 === 124); +} + +function isWindowsDriveLetterString(string) { + return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && (string[1] === ":" || string[1] === "|"); +} + +function isNormalizedWindowsDriveLetterString(string) { + return string.length === 2 && isASCIIAlpha(string.codePointAt(0)) && string[1] === ":"; +} + +function containsForbiddenHostCodePoint(string) { + return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|%|\/|:|\?|@|\[|\\|\]/) !== -1; +} + +function containsForbiddenHostCodePointExcludingPercent(string) { + return string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|\/|:|\?|@|\[|\\|\]/) !== -1; +} + +function isSpecialScheme(scheme) { + return specialSchemes[scheme] !== undefined; +} + +function isSpecial(url) { + return isSpecialScheme(url.scheme); +} + +function defaultPort(scheme) { + return specialSchemes[scheme]; +} + +function percentEncode(c) { + let hex = c.toString(16).toUpperCase(); + if (hex.length === 1) { + hex = "0" + hex; + } + + return "%" + hex; +} + +function utf8PercentEncode(c) { + const buf = new Buffer(c); + + let str = ""; + + for (let i = 0; i < buf.length; ++i) { + str += percentEncode(buf[i]); + } + + return str; +} + +function utf8PercentDecode(str) { + const input = new Buffer(str); + const output = []; + for (let i = 0; i < input.length; ++i) { + if (input[i] !== 37) { + output.push(input[i]); + } else if (input[i] === 37 && isASCIIHex(input[i + 1]) && isASCIIHex(input[i + 2])) { + output.push(parseInt(input.slice(i + 1, i + 3).toString(), 16)); + i += 2; + } else { + output.push(input[i]); + } + } + return new Buffer(output).toString(); +} + +function isC0ControlPercentEncode(c) { + return c <= 0x1F || c > 0x7E; +} + +const extraPathPercentEncodeSet = new Set([32, 34, 35, 60, 62, 63, 96, 123, 125]); +function isPathPercentEncode(c) { + return isC0ControlPercentEncode(c) || extraPathPercentEncodeSet.has(c); +} + +const extraUserinfoPercentEncodeSet = + new Set([47, 58, 59, 61, 64, 91, 92, 93, 94, 124]); +function isUserinfoPercentEncode(c) { + return isPathPercentEncode(c) || extraUserinfoPercentEncodeSet.has(c); +} + +function percentEncodeChar(c, encodeSetPredicate) { + const cStr = String.fromCodePoint(c); + + if (encodeSetPredicate(c)) { + return utf8PercentEncode(cStr); + } + + return cStr; +} + +function parseIPv4Number(input) { + let R = 10; + + if (input.length >= 2 && input.charAt(0) === "0" && input.charAt(1).toLowerCase() === "x") { + input = input.substring(2); + R = 16; + } else if (input.length >= 2 && input.charAt(0) === "0") { + input = input.substring(1); + R = 8; + } + + if (input === "") { + return 0; + } + + const regex = R === 10 ? /[^0-9]/ : (R === 16 ? /[^0-9A-Fa-f]/ : /[^0-7]/); + if (regex.test(input)) { + return failure; + } + + return parseInt(input, R); +} + +function parseIPv4(input) { + const parts = input.split("."); + if (parts[parts.length - 1] === "") { + if (parts.length > 1) { + parts.pop(); + } + } + + if (parts.length > 4) { + return input; + } + + const numbers = []; + for (const part of parts) { + if (part === "") { + return input; + } + const n = parseIPv4Number(part); + if (n === failure) { + return input; + } + + numbers.push(n); + } + + for (let i = 0; i < numbers.length - 1; ++i) { + if (numbers[i] > 255) { + return failure; + } + } + if (numbers[numbers.length - 1] >= Math.pow(256, 5 - numbers.length)) { + return failure; + } + + let ipv4 = numbers.pop(); + let counter = 0; + + for (const n of numbers) { + ipv4 += n * Math.pow(256, 3 - counter); + ++counter; + } + + return ipv4; +} + +function serializeIPv4(address) { + let output = ""; + let n = address; + + for (let i = 1; i <= 4; ++i) { + output = String(n % 256) + output; + if (i !== 4) { + output = "." + output; + } + n = Math.floor(n / 256); + } + + return output; +} + +function parseIPv6(input) { + const address = [0, 0, 0, 0, 0, 0, 0, 0]; + let pieceIndex = 0; + let compress = null; + let pointer = 0; + + input = punycode.ucs2.decode(input); + + if (input[pointer] === 58) { + if (input[pointer + 1] !== 58) { + return failure; + } + + pointer += 2; + ++pieceIndex; + compress = pieceIndex; + } + + while (pointer < input.length) { + if (pieceIndex === 8) { + return failure; + } + + if (input[pointer] === 58) { + if (compress !== null) { + return failure; + } + ++pointer; + ++pieceIndex; + compress = pieceIndex; + continue; + } + + let value = 0; + let length = 0; + + while (length < 4 && isASCIIHex(input[pointer])) { + value = value * 0x10 + parseInt(at(input, pointer), 16); + ++pointer; + ++length; + } + + if (input[pointer] === 46) { + if (length === 0) { + return failure; + } + + pointer -= length; + + if (pieceIndex > 6) { + return failure; + } + + let numbersSeen = 0; + + while (input[pointer] !== undefined) { + let ipv4Piece = null; + + if (numbersSeen > 0) { + if (input[pointer] === 46 && numbersSeen < 4) { + ++pointer; + } else { + return failure; + } + } + + if (!isASCIIDigit(input[pointer])) { + return failure; + } + + while (isASCIIDigit(input[pointer])) { + const number = parseInt(at(input, pointer)); + if (ipv4Piece === null) { + ipv4Piece = number; + } else if (ipv4Piece === 0) { + return failure; + } else { + ipv4Piece = ipv4Piece * 10 + number; + } + if (ipv4Piece > 255) { + return failure; + } + ++pointer; + } + + address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece; + + ++numbersSeen; + + if (numbersSeen === 2 || numbersSeen === 4) { + ++pieceIndex; + } + } + + if (numbersSeen !== 4) { + return failure; + } + + break; + } else if (input[pointer] === 58) { + ++pointer; + if (input[pointer] === undefined) { + return failure; + } + } else if (input[pointer] !== undefined) { + return failure; + } + + address[pieceIndex] = value; + ++pieceIndex; + } + + if (compress !== null) { + let swaps = pieceIndex - compress; + pieceIndex = 7; + while (pieceIndex !== 0 && swaps > 0) { + const temp = address[compress + swaps - 1]; + address[compress + swaps - 1] = address[pieceIndex]; + address[pieceIndex] = temp; + --pieceIndex; + --swaps; + } + } else if (compress === null && pieceIndex !== 8) { + return failure; + } + + return address; +} + +function serializeIPv6(address) { + let output = ""; + const seqResult = findLongestZeroSequence(address); + const compress = seqResult.idx; + let ignore0 = false; + + for (let pieceIndex = 0; pieceIndex <= 7; ++pieceIndex) { + if (ignore0 && address[pieceIndex] === 0) { + continue; + } else if (ignore0) { + ignore0 = false; + } + + if (compress === pieceIndex) { + const separator = pieceIndex === 0 ? "::" : ":"; + output += separator; + ignore0 = true; + continue; + } + + output += address[pieceIndex].toString(16); + + if (pieceIndex !== 7) { + output += ":"; + } + } + + return output; +} + +function parseHost(input, isSpecialArg) { + if (input[0] === "[") { + if (input[input.length - 1] !== "]") { + return failure; + } + + return parseIPv6(input.substring(1, input.length - 1)); + } + + if (!isSpecialArg) { + return parseOpaqueHost(input); + } + + const domain = utf8PercentDecode(input); + const asciiDomain = tr46.toASCII(domain, false, tr46.PROCESSING_OPTIONS.NONTRANSITIONAL, false); + if (asciiDomain === null) { + return failure; + } + + if (containsForbiddenHostCodePoint(asciiDomain)) { + return failure; + } + + const ipv4Host = parseIPv4(asciiDomain); + if (typeof ipv4Host === "number" || ipv4Host === failure) { + return ipv4Host; + } + + return asciiDomain; +} + +function parseOpaqueHost(input) { + if (containsForbiddenHostCodePointExcludingPercent(input)) { + return failure; + } + + let output = ""; + const decoded = punycode.ucs2.decode(input); + for (let i = 0; i < decoded.length; ++i) { + output += percentEncodeChar(decoded[i], isC0ControlPercentEncode); + } + return output; +} + +function findLongestZeroSequence(arr) { + let maxIdx = null; + let maxLen = 1; // only find elements > 1 + let currStart = null; + let currLen = 0; + + for (let i = 0; i < arr.length; ++i) { + if (arr[i] !== 0) { + if (currLen > maxLen) { + maxIdx = currStart; + maxLen = currLen; + } + + currStart = null; + currLen = 0; + } else { + if (currStart === null) { + currStart = i; + } + ++currLen; + } + } + + // if trailing zeros + if (currLen > maxLen) { + maxIdx = currStart; + maxLen = currLen; + } + + return { + idx: maxIdx, + len: maxLen + }; +} + +function serializeHost(host) { + if (typeof host === "number") { + return serializeIPv4(host); + } + + // IPv6 serializer + if (host instanceof Array) { + return "[" + serializeIPv6(host) + "]"; + } + + return host; +} + +function trimControlChars(url) { + return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/g, ""); +} + +function trimTabAndNewline(url) { + return url.replace(/\u0009|\u000A|\u000D/g, ""); +} + +function shortenPath(url) { + const path = url.path; + if (path.length === 0) { + return; + } + if (url.scheme === "file" && path.length === 1 && isNormalizedWindowsDriveLetter(path[0])) { + return; + } + + path.pop(); +} + +function includesCredentials(url) { + return url.username !== "" || url.password !== ""; +} + +function cannotHaveAUsernamePasswordPort(url) { + return url.host === null || url.host === "" || url.cannotBeABaseURL || url.scheme === "file"; +} + +function isNormalizedWindowsDriveLetter(string) { + return /^[A-Za-z]:$/.test(string); +} + +function URLStateMachine(input, base, encodingOverride, url, stateOverride) { + this.pointer = 0; + this.input = input; + this.base = base || null; + this.encodingOverride = encodingOverride || "utf-8"; + this.stateOverride = stateOverride; + this.url = url; + this.failure = false; + this.parseError = false; + + if (!this.url) { + this.url = { + scheme: "", + username: "", + password: "", + host: null, + port: null, + path: [], + query: null, + fragment: null, + + cannotBeABaseURL: false + }; + + const res = trimControlChars(this.input); + if (res !== this.input) { + this.parseError = true; + } + this.input = res; + } + + const res = trimTabAndNewline(this.input); + if (res !== this.input) { + this.parseError = true; + } + this.input = res; + + this.state = stateOverride || "scheme start"; + + this.buffer = ""; + this.atFlag = false; + this.arrFlag = false; + this.passwordTokenSeenFlag = false; + + this.input = punycode.ucs2.decode(this.input); + + for (; this.pointer <= this.input.length; ++this.pointer) { + const c = this.input[this.pointer]; + const cStr = isNaN(c) ? undefined : String.fromCodePoint(c); + + // exec state machine + const ret = this["parse " + this.state](c, cStr); + if (!ret) { + break; // terminate algorithm + } else if (ret === failure) { + this.failure = true; + break; + } + } +} + +URLStateMachine.prototype["parse scheme start"] = function parseSchemeStart(c, cStr) { + if (isASCIIAlpha(c)) { + this.buffer += cStr.toLowerCase(); + this.state = "scheme"; + } else if (!this.stateOverride) { + this.state = "no scheme"; + --this.pointer; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +URLStateMachine.prototype["parse scheme"] = function parseScheme(c, cStr) { + if (isASCIIAlphanumeric(c) || c === 43 || c === 45 || c === 46) { + this.buffer += cStr.toLowerCase(); + } else if (c === 58) { + if (this.stateOverride) { + if (isSpecial(this.url) && !isSpecialScheme(this.buffer)) { + return false; + } + + if (!isSpecial(this.url) && isSpecialScheme(this.buffer)) { + return false; + } + + if ((includesCredentials(this.url) || this.url.port !== null) && this.buffer === "file") { + return false; + } + + if (this.url.scheme === "file" && (this.url.host === "" || this.url.host === null)) { + return false; + } + } + this.url.scheme = this.buffer; + this.buffer = ""; + if (this.stateOverride) { + return false; + } + if (this.url.scheme === "file") { + if (this.input[this.pointer + 1] !== 47 || this.input[this.pointer + 2] !== 47) { + this.parseError = true; + } + this.state = "file"; + } else if (isSpecial(this.url) && this.base !== null && this.base.scheme === this.url.scheme) { + this.state = "special relative or authority"; + } else if (isSpecial(this.url)) { + this.state = "special authority slashes"; + } else if (this.input[this.pointer + 1] === 47) { + this.state = "path or authority"; + ++this.pointer; + } else { + this.url.cannotBeABaseURL = true; + this.url.path.push(""); + this.state = "cannot-be-a-base-URL path"; + } + } else if (!this.stateOverride) { + this.buffer = ""; + this.state = "no scheme"; + this.pointer = -1; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +URLStateMachine.prototype["parse no scheme"] = function parseNoScheme(c) { + if (this.base === null || (this.base.cannotBeABaseURL && c !== 35)) { + return failure; + } else if (this.base.cannotBeABaseURL && c === 35) { + this.url.scheme = this.base.scheme; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.url.cannotBeABaseURL = true; + this.state = "fragment"; + } else if (this.base.scheme === "file") { + this.state = "file"; + --this.pointer; + } else { + this.state = "relative"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special relative or authority"] = function parseSpecialRelativeOrAuthority(c) { + if (c === 47 && this.input[this.pointer + 1] === 47) { + this.state = "special authority ignore slashes"; + ++this.pointer; + } else { + this.parseError = true; + this.state = "relative"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse path or authority"] = function parsePathOrAuthority(c) { + if (c === 47) { + this.state = "authority"; + } else { + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse relative"] = function parseRelative(c) { + this.url.scheme = this.base.scheme; + if (isNaN(c)) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + } else if (c === 47) { + this.state = "relative slash"; + } else if (c === 63) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.state = "fragment"; + } else if (isSpecial(this.url) && c === 92) { + this.parseError = true; + this.state = "relative slash"; + } else { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.url.path = this.base.path.slice(0, this.base.path.length - 1); + + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse relative slash"] = function parseRelativeSlash(c) { + if (isSpecial(this.url) && (c === 47 || c === 92)) { + if (c === 92) { + this.parseError = true; + } + this.state = "special authority ignore slashes"; + } else if (c === 47) { + this.state = "authority"; + } else { + this.url.username = this.base.username; + this.url.password = this.base.password; + this.url.host = this.base.host; + this.url.port = this.base.port; + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special authority slashes"] = function parseSpecialAuthoritySlashes(c) { + if (c === 47 && this.input[this.pointer + 1] === 47) { + this.state = "special authority ignore slashes"; + ++this.pointer; + } else { + this.parseError = true; + this.state = "special authority ignore slashes"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse special authority ignore slashes"] = function parseSpecialAuthorityIgnoreSlashes(c) { + if (c !== 47 && c !== 92) { + this.state = "authority"; + --this.pointer; + } else { + this.parseError = true; + } + + return true; +}; + +URLStateMachine.prototype["parse authority"] = function parseAuthority(c, cStr) { + if (c === 64) { + this.parseError = true; + if (this.atFlag) { + this.buffer = "%40" + this.buffer; + } + this.atFlag = true; + + // careful, this is based on buffer and has its own pointer (this.pointer != pointer) and inner chars + const len = countSymbols(this.buffer); + for (let pointer = 0; pointer < len; ++pointer) { + const codePoint = this.buffer.codePointAt(pointer); + + if (codePoint === 58 && !this.passwordTokenSeenFlag) { + this.passwordTokenSeenFlag = true; + continue; + } + const encodedCodePoints = percentEncodeChar(codePoint, isUserinfoPercentEncode); + if (this.passwordTokenSeenFlag) { + this.url.password += encodedCodePoints; + } else { + this.url.username += encodedCodePoints; + } + } + this.buffer = ""; + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92)) { + if (this.atFlag && this.buffer === "") { + this.parseError = true; + return failure; + } + this.pointer -= countSymbols(this.buffer) + 1; + this.buffer = ""; + this.state = "host"; + } else { + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse hostname"] = +URLStateMachine.prototype["parse host"] = function parseHostName(c, cStr) { + if (this.stateOverride && this.url.scheme === "file") { + --this.pointer; + this.state = "file host"; + } else if (c === 58 && !this.arrFlag) { + if (this.buffer === "") { + this.parseError = true; + return failure; + } + + const host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + + this.url.host = host; + this.buffer = ""; + this.state = "port"; + if (this.stateOverride === "hostname") { + return false; + } + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92)) { + --this.pointer; + if (isSpecial(this.url) && this.buffer === "") { + this.parseError = true; + return failure; + } else if (this.stateOverride && this.buffer === "" && + (includesCredentials(this.url) || this.url.port !== null)) { + this.parseError = true; + return false; + } + + const host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + + this.url.host = host; + this.buffer = ""; + this.state = "path start"; + if (this.stateOverride) { + return false; + } + } else { + if (c === 91) { + this.arrFlag = true; + } else if (c === 93) { + this.arrFlag = false; + } + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse port"] = function parsePort(c, cStr) { + if (isASCIIDigit(c)) { + this.buffer += cStr; + } else if (isNaN(c) || c === 47 || c === 63 || c === 35 || + (isSpecial(this.url) && c === 92) || + this.stateOverride) { + if (this.buffer !== "") { + const port = parseInt(this.buffer); + if (port > Math.pow(2, 16) - 1) { + this.parseError = true; + return failure; + } + this.url.port = port === defaultPort(this.url.scheme) ? null : port; + this.buffer = ""; + } + if (this.stateOverride) { + return false; + } + this.state = "path start"; + --this.pointer; + } else { + this.parseError = true; + return failure; + } + + return true; +}; + +const fileOtherwiseCodePoints = new Set([47, 92, 63, 35]); + +URLStateMachine.prototype["parse file"] = function parseFile(c) { + this.url.scheme = "file"; + + if (c === 47 || c === 92) { + if (c === 92) { + this.parseError = true; + } + this.state = "file slash"; + } else if (this.base !== null && this.base.scheme === "file") { + if (isNaN(c)) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + } else if (c === 63) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + this.url.query = this.base.query; + this.url.fragment = ""; + this.state = "fragment"; + } else { + if (this.input.length - this.pointer - 1 === 0 || // remaining consists of 0 code points + !isWindowsDriveLetterCodePoints(c, this.input[this.pointer + 1]) || + (this.input.length - this.pointer - 1 >= 2 && // remaining has at least 2 code points + !fileOtherwiseCodePoints.has(this.input[this.pointer + 2]))) { + this.url.host = this.base.host; + this.url.path = this.base.path.slice(); + shortenPath(this.url); + } else { + this.parseError = true; + } + + this.state = "path"; + --this.pointer; + } + } else { + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse file slash"] = function parseFileSlash(c) { + if (c === 47 || c === 92) { + if (c === 92) { + this.parseError = true; + } + this.state = "file host"; + } else { + if (this.base !== null && this.base.scheme === "file") { + if (isNormalizedWindowsDriveLetterString(this.base.path[0])) { + this.url.path.push(this.base.path[0]); + } else { + this.url.host = this.base.host; + } + } + this.state = "path"; + --this.pointer; + } + + return true; +}; + +URLStateMachine.prototype["parse file host"] = function parseFileHost(c, cStr) { + if (isNaN(c) || c === 47 || c === 92 || c === 63 || c === 35) { + --this.pointer; + if (!this.stateOverride && isWindowsDriveLetterString(this.buffer)) { + this.parseError = true; + this.state = "path"; + } else if (this.buffer === "") { + this.url.host = ""; + if (this.stateOverride) { + return false; + } + this.state = "path start"; + } else { + let host = parseHost(this.buffer, isSpecial(this.url)); + if (host === failure) { + return failure; + } + if (host === "localhost") { + host = ""; + } + this.url.host = host; + + if (this.stateOverride) { + return false; + } + + this.buffer = ""; + this.state = "path start"; + } + } else { + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse path start"] = function parsePathStart(c) { + if (isSpecial(this.url)) { + if (c === 92) { + this.parseError = true; + } + this.state = "path"; + + if (c !== 47 && c !== 92) { + --this.pointer; + } + } else if (!this.stateOverride && c === 63) { + this.url.query = ""; + this.state = "query"; + } else if (!this.stateOverride && c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } else if (c !== undefined) { + this.state = "path"; + if (c !== 47) { + --this.pointer; + } + } + + return true; +}; + +URLStateMachine.prototype["parse path"] = function parsePath(c) { + if (isNaN(c) || c === 47 || (isSpecial(this.url) && c === 92) || + (!this.stateOverride && (c === 63 || c === 35))) { + if (isSpecial(this.url) && c === 92) { + this.parseError = true; + } + + if (isDoubleDot(this.buffer)) { + shortenPath(this.url); + if (c !== 47 && !(isSpecial(this.url) && c === 92)) { + this.url.path.push(""); + } + } else if (isSingleDot(this.buffer) && c !== 47 && + !(isSpecial(this.url) && c === 92)) { + this.url.path.push(""); + } else if (!isSingleDot(this.buffer)) { + if (this.url.scheme === "file" && this.url.path.length === 0 && isWindowsDriveLetterString(this.buffer)) { + if (this.url.host !== "" && this.url.host !== null) { + this.parseError = true; + this.url.host = ""; + } + this.buffer = this.buffer[0] + ":"; + } + this.url.path.push(this.buffer); + } + this.buffer = ""; + if (this.url.scheme === "file" && (c === undefined || c === 63 || c === 35)) { + while (this.url.path.length > 1 && this.url.path[0] === "") { + this.parseError = true; + this.url.path.shift(); + } + } + if (c === 63) { + this.url.query = ""; + this.state = "query"; + } + if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } + } else { + // TODO: If c is not a URL code point and not "%", parse error. + + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.buffer += percentEncodeChar(c, isPathPercentEncode); + } + + return true; +}; + +URLStateMachine.prototype["parse cannot-be-a-base-URL path"] = function parseCannotBeABaseURLPath(c) { + if (c === 63) { + this.url.query = ""; + this.state = "query"; + } else if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } else { + // TODO: Add: not a URL code point + if (!isNaN(c) && c !== 37) { + this.parseError = true; + } + + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + if (!isNaN(c)) { + this.url.path[0] = this.url.path[0] + percentEncodeChar(c, isC0ControlPercentEncode); + } + } + + return true; +}; + +URLStateMachine.prototype["parse query"] = function parseQuery(c, cStr) { + if (isNaN(c) || (!this.stateOverride && c === 35)) { + if (!isSpecial(this.url) || this.url.scheme === "ws" || this.url.scheme === "wss") { + this.encodingOverride = "utf-8"; + } + + const buffer = new Buffer(this.buffer); // TODO: Use encoding override instead + for (let i = 0; i < buffer.length; ++i) { + if (buffer[i] < 0x21 || buffer[i] > 0x7E || buffer[i] === 0x22 || buffer[i] === 0x23 || + buffer[i] === 0x3C || buffer[i] === 0x3E) { + this.url.query += percentEncode(buffer[i]); + } else { + this.url.query += String.fromCodePoint(buffer[i]); + } + } + + this.buffer = ""; + if (c === 35) { + this.url.fragment = ""; + this.state = "fragment"; + } + } else { + // TODO: If c is not a URL code point and not "%", parse error. + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.buffer += cStr; + } + + return true; +}; + +URLStateMachine.prototype["parse fragment"] = function parseFragment(c) { + if (isNaN(c)) { // do nothing + } else if (c === 0x0) { + this.parseError = true; + } else { + // TODO: If c is not a URL code point and not "%", parse error. + if (c === 37 && + (!isASCIIHex(this.input[this.pointer + 1]) || + !isASCIIHex(this.input[this.pointer + 2]))) { + this.parseError = true; + } + + this.url.fragment += percentEncodeChar(c, isC0ControlPercentEncode); + } + + return true; +}; + +function serializeURL(url, excludeFragment) { + let output = url.scheme + ":"; + if (url.host !== null) { + output += "//"; + + if (url.username !== "" || url.password !== "") { + output += url.username; + if (url.password !== "") { + output += ":" + url.password; + } + output += "@"; + } + + output += serializeHost(url.host); + + if (url.port !== null) { + output += ":" + url.port; + } + } else if (url.host === null && url.scheme === "file") { + output += "//"; + } + + if (url.cannotBeABaseURL) { + output += url.path[0]; + } else { + for (const string of url.path) { + output += "/" + string; + } + } + + if (url.query !== null) { + output += "?" + url.query; + } + + if (!excludeFragment && url.fragment !== null) { + output += "#" + url.fragment; + } + + return output; +} + +function serializeOrigin(tuple) { + let result = tuple.scheme + "://"; + result += serializeHost(tuple.host); + + if (tuple.port !== null) { + result += ":" + tuple.port; + } + + return result; +} + +module.exports.serializeURL = serializeURL; + +module.exports.serializeURLOrigin = function (url) { + // https://url.spec.whatwg.org/#concept-url-origin + switch (url.scheme) { + case "blob": + try { + return module.exports.serializeURLOrigin(module.exports.parseURL(url.path[0])); + } catch (e) { + // serializing an opaque origin returns "null" + return "null"; + } + case "ftp": + case "gopher": + case "http": + case "https": + case "ws": + case "wss": + return serializeOrigin({ + scheme: url.scheme, + host: url.host, + port: url.port + }); + case "file": + // spec says "exercise to the reader", chrome says "file://" + return "file://"; + default: + // serializing an opaque origin returns "null" + return "null"; + } +}; + +module.exports.basicURLParse = function (input, options) { + if (options === undefined) { + options = {}; + } + + const usm = new URLStateMachine(input, options.baseURL, options.encodingOverride, options.url, options.stateOverride); + if (usm.failure) { + return "failure"; + } + + return usm.url; +}; + +module.exports.setTheUsername = function (url, username) { + url.username = ""; + const decoded = punycode.ucs2.decode(username); + for (let i = 0; i < decoded.length; ++i) { + url.username += percentEncodeChar(decoded[i], isUserinfoPercentEncode); + } +}; + +module.exports.setThePassword = function (url, password) { + url.password = ""; + const decoded = punycode.ucs2.decode(password); + for (let i = 0; i < decoded.length; ++i) { + url.password += percentEncodeChar(decoded[i], isUserinfoPercentEncode); + } +}; + +module.exports.serializeHost = serializeHost; + +module.exports.cannotHaveAUsernamePasswordPort = cannotHaveAUsernamePasswordPort; + +module.exports.serializeInteger = function (integer) { + return String(integer); +}; + +module.exports.parseURL = function (input, options) { + if (options === undefined) { + options = {}; + } + + // We don't handle blobs, so this just delegates: + return module.exports.basicURLParse(input, { baseURL: options.baseURL, encodingOverride: options.encodingOverride }); +}; /***/ }), @@ -737910,7 +737910,7 @@ module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"] /************************************************************************/ /******/ // The module cache /******/ var __webpack_module_cache__ = {}; -/******/ +/******/ /******/ // The require function /******/ function __nccwpck_require__(moduleId) { /******/ // Check if module is in cache @@ -737924,7 +737924,7 @@ module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"] /******/ // no module.loaded needed /******/ exports: {} /******/ }; -/******/ +/******/ /******/ // Execute the module function /******/ var threw = true; /******/ try { @@ -737933,23 +737933,23 @@ module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"] /******/ } finally { /******/ if(threw) delete __webpack_module_cache__[moduleId]; /******/ } -/******/ +/******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } -/******/ +/******/ /************************************************************************/ /******/ /* webpack/runtime/compat */ -/******/ +/******/ /******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; -/******/ +/******/ /************************************************************************/ -/******/ +/******/ /******/ // startup /******/ // Load entry module and return exports /******/ // This entry module is referenced by other modules so it can't be inlined /******/ var __webpack_exports__ = __nccwpck_require__(45555); /******/ module.exports = __webpack_exports__; -/******/ +/******/ /******/ })() ; From 1856b71b1140684e249b5b6e83fa603430bedfc6 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Tue, 5 Aug 2025 11:02:37 +0200 Subject: [PATCH 24/54] exclude partial transactions from getOldestTransactionDate --- src/libs/Formula.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index fa15bc862ac3c..0ef0fcaa2f2aa 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -6,6 +6,7 @@ import type Report from '@src/types/onyx/Report'; import {getCurrencySymbol} from './CurrencyUtils'; import {getAllReportActions} from './ReportActionsUtils'; import {getReportTransactions} from './ReportUtils'; +import {getCreated, isPartialTransaction} from './TransactionUtils'; type FormulaPart = { /** The original definition from the formula */ @@ -458,11 +459,17 @@ function getOldestTransactionDate(reportID: string): string | undefined { let oldestDate: string | undefined; transactions.forEach((transaction) => { - if (transaction?.created) { - if (!oldestDate || transaction.created < oldestDate) { - oldestDate = transaction.created; - } + const created = getCreated(transaction); + if (!created) { + return; + } + if (oldestDate && created >= oldestDate) { + return; + } + if (isPartialTransaction(transaction)) { + return; } + oldestDate = created; }); return oldestDate; From fc26fcc092a2834d8b73c80546bc10cfcd5fa48f Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Tue, 5 Aug 2025 11:20:38 +0200 Subject: [PATCH 25/54] domainName function --- src/libs/Formula.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 0ef0fcaa2f2aa..6344fe6324c47 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -291,6 +291,9 @@ function applyFunctions(value: string, functions: string[]): string { case 'substr': result = getSubstring(result, args); break; + case 'domain': + result = getDomainName(result); + break; default: // Unknown function, leave value as is break; @@ -315,6 +318,20 @@ function getFrontPart(value: string): string { return trimmed.split(' ')[0]; } +/** + * Get the domain name of an email or URL + */ +function getDomainName(value: string): string { + const trimmed = value.trim(); + + // If it's an email, return the part after @ + if (trimmed.includes('@')) { + return trimmed.split('@')[1]; + } + + return ''; +} + /** * Get substring of a value */ From 4076a3a9cfd03b4de1c90c65b695ff79b4a92baa Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Tue, 5 Aug 2025 11:51:19 +0200 Subject: [PATCH 26/54] valid report types check --- src/libs/OptimisticReportNames.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 6a4d43c76ad17..38ac1fa604b6f 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -98,7 +98,7 @@ function shouldComputeReportName(report: Report, policy: Policy | undefined): bo } // Check if the report is an expense report - if (!ReportUtils.isExpenseReport(report)) { + if (!isValidReportType(report.type)) { return false; } @@ -111,6 +111,19 @@ function shouldComputeReportName(report: Report, policy: Policy | undefined): bo // Check if the formula contains formula parts return Formula.isFormula(titleField.defaultValue); } + +function isValidReportType(reportType?: string): boolean { + if (!reportType) { + return false; + } + return ( + reportType === CONST.REPORT.TYPE.EXPENSE || + reportType === CONST.REPORT.TYPE.INVOICE || + reportType === CONST.REPORT.UNSUPPORTED_TYPE.BILL || + reportType === CONST.REPORT.UNSUPPORTED_TYPE.PAYCHECK || + reportType === 'trip' + ); +} /** * Compute a report name for a new report being created From ecc5a1e41ce16a7899afe916909ea759ef5be504 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Tue, 5 Aug 2025 13:55:01 +0200 Subject: [PATCH 27/54] Small improvements, beta name correction --- src/CONST/index.ts | 2 +- src/libs/OptimisticReportNames.ts | 26 ++++++++++++------------- src/libs/Permissions.ts | 2 +- tests/unit/OptimisticReportNamesTest.ts | 4 ++-- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 86036ae7ab8fb..10f2492a4e163 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -656,7 +656,7 @@ const CONST = { BETAS: { ALL: 'all', ASAP_SUBMIT: 'asapSubmit', - USE_CUSTOM_REPORT_NAMES: 'useCustomReportNamesNewExpensify', + AUTH_AUTO_REPORT_TITLE: 'authAutoReportTitle', DEFAULT_ROOMS: 'defaultRooms', P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', SPOTNANA_TRAVEL: 'spotnanaTravel', diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 38ac1fa604b6f..e76758241b5bd 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -111,7 +111,7 @@ function shouldComputeReportName(report: Report, policy: Policy | undefined): bo // Check if the formula contains formula parts return Formula.isFormula(titleField.defaultValue); } - + function isValidReportType(reportType?: string): boolean { if (!reportType) { return false; @@ -129,7 +129,7 @@ function isValidReportType(reportType?: string): boolean { * Compute a report name for a new report being created * This handles the case where the report doesn't exist in context yet */ -function computeNameForNewReport(update: OnyxUpdate, context: UpdateContext): {reportName: string} | null { +function computeNameForNewReport(update: OnyxUpdate, context: UpdateContext): string | null { Performance.markStart(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); Timing.start(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); @@ -180,7 +180,7 @@ function computeNameForNewReport(update: OnyxUpdate, context: UpdateContext): {r Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); - return {reportName: newName}; + return newName; } Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); @@ -191,13 +191,7 @@ function computeNameForNewReport(update: OnyxUpdate, context: UpdateContext): {r /** * Compute a new report name if needed based on an optimistic update */ -function computeReportNameIfNeeded( - report: Report, - incomingUpdate: OnyxUpdate, - context: UpdateContext, -): { - reportName: string; -} | null { +function computeReportNameIfNeeded(report: Report, incomingUpdate: OnyxUpdate, context: UpdateContext): string | null { Performance.markStart(CONST.TIMING.COMPUTE_REPORT_NAME); Timing.start(CONST.TIMING.COMPUTE_REPORT_NAME); @@ -225,7 +219,7 @@ function computeReportNameIfNeeded( // Check if any formula part might be affected by this update const isAffected = formulaParts.some((part) => { if (part.type === Formula.FORMULA_PART_TYPES.REPORT) { - return updateType === 'report' || updateType === 'transaction'; + return updateType === 'report' || updateType === 'transaction' || updateType === 'policy'; } if (part.type === Formula.FORMULA_PART_TYPES.FIELD) { return updateType === 'report'; @@ -264,7 +258,7 @@ function computeReportNameIfNeeded( Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); - return {reportName: newName}; + return newName; } Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); @@ -319,7 +313,9 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: additionalUpdates.push({ key: getReportKey(reportID), onyxMethod: Onyx.METHOD.MERGE, - value: reportNameUpdate, + value: { + reportName: reportNameUpdate, + }, }); } continue; // Skip the normal processing for this update @@ -357,7 +353,9 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: additionalUpdates.push({ key: getReportKey(report.reportID), onyxMethod: Onyx.METHOD.MERGE, - value: reportNameUpdate, + value: { + reportName: reportNameUpdate, + }, }); } } diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 6a18963d68a6a..cc36adff7ec93 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -9,7 +9,7 @@ function canUseAllBetas(betas: OnyxEntry): boolean { // eslint-disable-next-line rulesdir/no-beta-handler function canUseCustomReportNames(betas: OnyxEntry): boolean { - return isBetaEnabled(CONST.BETAS.USE_CUSTOM_REPORT_NAMES, betas); + return isBetaEnabled(CONST.BETAS.AUTH_AUTO_REPORT_TITLE, betas); } // eslint-disable-next-line rulesdir/no-beta-handler diff --git a/tests/unit/OptimisticReportNamesTest.ts b/tests/unit/OptimisticReportNamesTest.ts index ed296f2183139..73bf3c8cd3c87 100644 --- a/tests/unit/OptimisticReportNamesTest.ts +++ b/tests/unit/OptimisticReportNamesTest.ts @@ -96,7 +96,7 @@ describe('OptimisticReportNames', () => { }; const result = computeNameForNewReport(update, mockContext); - expect(result).toEqual({reportName: 'Expense Report - USD 100.00'}); + expect(result).toEqual('Expense Report - USD 100.00'); }); test('should return null for report without policy', () => { @@ -149,7 +149,7 @@ describe('OptimisticReportNames', () => { }; const result = computeReportNameIfNeeded(mockReport as any, update, mockContext); - expect(result).toEqual({reportName: 'Expense Report - USD 200.00'}); + expect(result).toEqual('Expense Report - USD 200.00'); }); test('should return null when name would not change', () => { From 0285d67600a89f9fb5b6b55b5ff6100ade3c337a Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Tue, 5 Aug 2025 15:12:42 +0200 Subject: [PATCH 28/54] report filtering for policyID --- src/libs/OptimisticReportNames.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index e76758241b5bd..53d3bcebca2a3 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -68,7 +68,30 @@ function getPolicyByID(policyID: string, allPolicies: Record): P * Get all reports associated with a policy ID */ function getReportsByPolicyID(policyID: string, allReports: Record): Report[] { - return Object.values(allReports).filter((report) => report?.policyID === policyID); + return Object.values(allReports).filter((report) => { + // 1. Filter by policy ID + if (report?.policyID !== policyID) { + return false; + } + + // 2. Filter by type - only reports that support custom names + if (!isValidReportType(report.type)) { + return false; + } + + // 3. Filter by state - exclude reports in high states (like approved or higher) + const stateThreshold = CONST.REPORT.STATE_NUM.APPROVED; + if (report.stateNum && report.stateNum > stateThreshold) { + return false; + } + + // 4. Filter by isArchived - exclude archived reports + if (ReportUtils.isArchivedReport(ReportUtils.getReportNameValuePairs(report?.reportID))) { + return false; + } + + return true; + }); } /** From 776e7ad1706443c61f3ead2e90ddca652dd7f7f9 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Wed, 6 Aug 2025 12:28:49 +0200 Subject: [PATCH 29/54] Make single path for old and new reports name computation --- src/libs/Formula.ts | 10 ++--- src/libs/OptimisticReportNames.ts | 62 +++++++++++++++---------------- 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 6344fe6324c47..3792156990cd6 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -262,7 +262,7 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { /** * Compute the value of a field formula part */ -function computeFieldPart(part: FormulaPart, context: FormulaContext): string { +function computeFieldPart(part: FormulaPart): string { // Field computation will be implemented later return part.definition; } @@ -270,7 +270,7 @@ function computeFieldPart(part: FormulaPart, context: FormulaContext): string { /** * Compute the value of a user formula part */ -function computeUserPart(part: FormulaPart, context: FormulaContext): string { +function computeUserPart(part: FormulaPart): string { // User computation will be implemented later return part.definition; } @@ -311,11 +311,11 @@ function getFrontPart(value: string): string { // If it's an email, return the part before @ if (trimmed.includes('@')) { - return trimmed.split('@')[0]; + return trimmed.split('@').at(0) ?? ''; } // Otherwise, return the first word - return trimmed.split(' ')[0]; + return trimmed.split(' ').at(0) ?? ''; } /** @@ -326,7 +326,7 @@ function getDomainName(value: string): string { // If it's an email, return the part after @ if (trimmed.includes('@')) { - return trimmed.split('@')[1]; + return trimmed.split('@').at(1) ?? ''; } return ''; diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 53d3bcebca2a3..00d94e62e5ecd 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -214,14 +214,23 @@ function computeNameForNewReport(update: OnyxUpdate, context: UpdateContext): st /** * Compute a new report name if needed based on an optimistic update */ -function computeReportNameIfNeeded(report: Report, incomingUpdate: OnyxUpdate, context: UpdateContext): string | null { +function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: OnyxUpdate, context: UpdateContext): string | null { Performance.markStart(CONST.TIMING.COMPUTE_REPORT_NAME); Timing.start(CONST.TIMING.COMPUTE_REPORT_NAME); const {allPolicies} = context; - const policy = getPolicyByID(report.policyID ?? '', allPolicies); - if (!shouldComputeReportName(report, policy)) { + // If no report is provided, extract it from the update (for new reports) + const targetReport = report ?? (incomingUpdate.value as Report); + + if (!targetReport) { + Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); + Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); + return null; + } + + const policy = getPolicyByID(targetReport.policyID ?? '', allPolicies); + if (!shouldComputeReportName(targetReport, policy)) { Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); return null; @@ -257,9 +266,9 @@ function computeReportNameIfNeeded(report: Report, incomingUpdate: OnyxUpdate, c } // Build context with the updated data - const updatedReport = updateType === 'report' && report.reportID === getReportIDFromKey(incomingUpdate.key) ? {...report, ...incomingUpdate.value} : report; + const updatedReport = updateType === 'report' && targetReport.reportID === getReportIDFromKey(incomingUpdate.key) ? {...targetReport, ...incomingUpdate.value} : targetReport; - const updatedPolicy = updateType === 'policy' && report.policyID === getPolicyIDFromKey(incomingUpdate.key) ? {...policy, ...incomingUpdate.value} : policy; + const updatedPolicy = updateType === 'policy' && targetReport.policyID === getPolicyIDFromKey(incomingUpdate.key) ? {...policy, ...incomingUpdate.value} : policy; // Compute the new name const formulaContext: Formula.FormulaContext = { @@ -270,13 +279,14 @@ function computeReportNameIfNeeded(report: Report, incomingUpdate: OnyxUpdate, c const newName = Formula.compute(formula, formulaContext); // Only return an update if the name actually changed - if (newName && newName !== report.reportName) { - Log.info('[OptimisticReportNames] Report name computed for existing report', false, { - reportID: report.reportID, - oldName: report.reportName, + if (newName && newName !== targetReport.reportName) { + Log.info('[OptimisticReportNames] Report name computed', false, { + reportID: targetReport.reportID, + oldName: targetReport.reportName, newName, formula, updateType, + isNewReport: !report, }); Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); @@ -304,9 +314,8 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: }); const {betas, allReports} = context; - console.log('morwa Permissions.canUseCustomReportNames(betas)', Permissions.canUseCustomReportNames(betas)); + // Check if the feature is enabled - // TODO: change this condition later (implemented only for testing purposes) if (!Permissions.canUseCustomReportNames(betas)) { Performance.markEnd(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); Timing.end(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); @@ -324,28 +333,17 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: const reportID = getReportIDFromKey(update.key); const report = getReportByID(reportID, allReports); - // Special handling for new reports (SET method means new report creation) - if (!report && update.onyxMethod === Onyx.METHOD.SET) { - Log.info('[OptimisticReportNames] Detected new report creation', false, { - reportID, - updateKey: update.key, - }); - const reportNameUpdate = computeNameForNewReport(update, context); - - if (reportNameUpdate) { - additionalUpdates.push({ - key: getReportKey(reportID), - onyxMethod: Onyx.METHOD.MERGE, - value: { - reportName: reportNameUpdate, - }, - }); - } - continue; // Skip the normal processing for this update - } + // Handle both existing and new reports with the same function + const reportNameUpdate = computeReportNameIfNeeded(report, update, context); - if (report) { - affectedReports = [report]; + if (reportNameUpdate) { + additionalUpdates.push({ + key: getReportKey(reportID), + onyxMethod: Onyx.METHOD.MERGE, + value: { + reportName: reportNameUpdate, + }, + }); } break; } From 5c92ef236a1a585bd9c451c46a14b94d45f428d0 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Wed, 6 Aug 2025 12:55:00 +0200 Subject: [PATCH 30/54] filter by __FAKE__ policyId --- src/libs/OptimisticReportNames.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 00d94e62e5ecd..144f4b8c76324 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -68,24 +68,26 @@ function getPolicyByID(policyID: string, allPolicies: Record): P * Get all reports associated with a policy ID */ function getReportsByPolicyID(policyID: string, allReports: Record): Report[] { + if (policyID === CONST.POLICY.ID_FAKE) { + return []; + } return Object.values(allReports).filter((report) => { - // 1. Filter by policy ID if (report?.policyID !== policyID) { return false; } - // 2. Filter by type - only reports that support custom names + // Filter by type - only reports that support custom names if (!isValidReportType(report.type)) { return false; } - // 3. Filter by state - exclude reports in high states (like approved or higher) + // Filter by state - exclude reports in high states (like approved or higher) const stateThreshold = CONST.REPORT.STATE_NUM.APPROVED; if (report.stateNum && report.stateNum > stateThreshold) { return false; } - // 4. Filter by isArchived - exclude archived reports + // Filter by isArchived - exclude archived reports if (ReportUtils.isArchivedReport(ReportUtils.getReportNameValuePairs(report?.reportID))) { return false; } From 0a68994ead70006bf4b38f37a56c17917c30ee04 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Wed, 6 Aug 2025 12:59:50 +0200 Subject: [PATCH 31/54] Explained usage of Onyx.connectWithoutView in connection mangager --- src/libs/OptimisticReportNamesConnectionManager.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/libs/OptimisticReportNamesConnectionManager.ts b/src/libs/OptimisticReportNamesConnectionManager.ts index d59c721e246a6..d1eb2dd92582d 100644 --- a/src/libs/OptimisticReportNamesConnectionManager.ts +++ b/src/libs/OptimisticReportNamesConnectionManager.ts @@ -19,6 +19,12 @@ let isInitialized = false; /** * Initialize persistent connections to Onyx data needed for OptimisticReportNames * This is called lazily when OptimisticReportNames functionality is first used + * + * We use Onyx.connectWithoutView because we do not use this in React components and this logic is not tied to the UI. + * This is a centralized system that needs access to all objects of several types, so that when any updates affect + * the computed report names, we can compute the new names according to the formula and add the necessary updates. + * It wouldn't be possible to do this without connecting to all the data. + * */ function initialize(): void { if (isInitialized) { @@ -26,7 +32,6 @@ function initialize(): void { } // Connect to BETAS - // eslint-disable-next-line react-compiler/react-compiler -- This is not a React component and needs to access Onyx data for OptimisticReportNames processing without being tied to a UI view Onyx.connectWithoutView({ key: ONYXKEYS.BETAS, callback: (val) => { @@ -35,7 +40,6 @@ function initialize(): void { }); // Connect to all REPORTS - // eslint-disable-next-line react-compiler/react-compiler -- This is not a React component and needs to access Onyx data for OptimisticReportNames processing without being tied to a UI view Onyx.connectWithoutView({ key: ONYXKEYS.COLLECTION.REPORT, waitForCollectionCallback: true, @@ -45,7 +49,6 @@ function initialize(): void { }); // Connect to all POLICIES - // eslint-disable-next-line react-compiler/react-compiler -- This is not a React component and needs to access Onyx data for OptimisticReportNames processing without being tied to a UI view Onyx.connectWithoutView({ key: ONYXKEYS.COLLECTION.POLICY, waitForCollectionCallback: true, From 2bd87e81cb16831cb548c9f386f7b03d5bab4d06 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Wed, 6 Aug 2025 13:31:12 +0200 Subject: [PATCH 32/54] Connection manager is initialized in async manner --- .../OptimisticReportNamesConnectionManager.ts | 92 ++++++++++--------- 1 file changed, 51 insertions(+), 41 deletions(-) diff --git a/src/libs/OptimisticReportNamesConnectionManager.ts b/src/libs/OptimisticReportNamesConnectionManager.ts index d1eb2dd92582d..29e494ec6e038 100644 --- a/src/libs/OptimisticReportNamesConnectionManager.ts +++ b/src/libs/OptimisticReportNamesConnectionManager.ts @@ -15,10 +15,14 @@ let betas: OnyxEntry; let allReports: Record; let allPolicies: Record; let isInitialized = false; +let connectionsInitializedCount = 0; +const totalConnections = 3; +let initializationPromise: Promise | null = null; /** * Initialize persistent connections to Onyx data needed for OptimisticReportNames * This is called lazily when OptimisticReportNames functionality is first used + * Returns a Promise that resolves when all connections have received their initial data * * We use Onyx.connectWithoutView because we do not use this in React components and this logic is not tied to the UI. * This is a centralized system that needs access to all objects of several types, so that when any updates affect @@ -26,62 +30,68 @@ let isInitialized = false; * It wouldn't be possible to do this without connecting to all the data. * */ -function initialize(): void { +function initialize(): Promise { if (isInitialized) { - return; + return Promise.resolve(); } - // Connect to BETAS - Onyx.connectWithoutView({ - key: ONYXKEYS.BETAS, - callback: (val) => { - betas = val; - }, - }); + if (initializationPromise) { + return initializationPromise; + } - // Connect to all REPORTS - Onyx.connectWithoutView({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (val) => { - allReports = (val as Record) ?? {}; - }, - }); + initializationPromise = new Promise((resolve) => { + const checkAndMarkInitialized = () => { + connectionsInitializedCount++; + if (connectionsInitializedCount === totalConnections) { + isInitialized = true; + resolve(); + } + }; + + // Connect to BETAS + Onyx.connectWithoutView({ + key: ONYXKEYS.BETAS, + callback: (val) => { + betas = val; + checkAndMarkInitialized(); + }, + }); - // Connect to all POLICIES - Onyx.connectWithoutView({ - key: ONYXKEYS.COLLECTION.POLICY, - waitForCollectionCallback: true, - callback: (val) => { - allPolicies = (val as Record) ?? {}; - }, + // Connect to all REPORTS + Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (val) => { + allReports = (val as Record) ?? {}; + checkAndMarkInitialized(); + }, + }); + + // Connect to all POLICIES + Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.POLICY, + waitForCollectionCallback: true, + callback: (val) => { + allPolicies = (val as Record) ?? {}; + checkAndMarkInitialized(); + }, + }); }); - isInitialized = true; + return initializationPromise; } /** - * Get the current update context for OptimisticReportNames - * This provides access to the cached Onyx data without creating new connections + * Get the current update context as a promise for backward compatibility * Initializes connections lazily on first use */ -function getUpdateContext(): UpdateContext { - initialize(); - return { +function getUpdateContextAsync(): Promise { + return initialize().then(() => ({ betas, allReports: allReports ?? {}, allPolicies: allPolicies ?? {}, - }; -} - -/** - * Get the current update context as a promise for backward compatibility - * Initializes connections lazily on first use - */ -function getUpdateContextAsync(): Promise { - initialize(); - return Promise.resolve(getUpdateContext()); + })); } -export {getUpdateContext, getUpdateContextAsync}; +export {getUpdateContextAsync}; export type {UpdateContext}; From 3ba4506104840b23bf100120e1f446774c09eae1 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Wed, 6 Aug 2025 14:37:13 +0200 Subject: [PATCH 33/54] PR fixes to for testing --- src/libs/Formula.ts | 4 +- src/libs/OptimisticReportNames.ts | 71 +---------- tests/perf-test/Formula.perf-test.ts | 2 +- .../OptimisticReportNames.perf-test.ts | 20 +-- tests/unit/FormulaTest.ts | 2 +- tests/unit/OptimisticReportNamesTest.ts | 119 +----------------- 6 files changed, 13 insertions(+), 205 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 3792156990cd6..703706909d27b 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -208,10 +208,10 @@ function compute(formula: string, context: FormulaContext): string { value = computeReportPart(part, context); break; case FORMULA_PART_TYPES.FIELD: - value = computeFieldPart(part, context); + value = computeFieldPart(part); break; case FORMULA_PART_TYPES.USER: - value = computeUserPart(part, context); + value = computeUserPart(part); break; case FORMULA_PART_TYPES.FREETEXT: value = part.definition; diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 144f4b8c76324..87c26b82e5047 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -132,9 +132,7 @@ function shouldComputeReportName(report: Report, policy: Policy | undefined): bo if (!titleField?.defaultValue) { return false; } - - // Check if the formula contains formula parts - return Formula.isFormula(titleField.defaultValue); + return true; } function isValidReportType(reportType?: string): boolean { @@ -150,69 +148,6 @@ function isValidReportType(reportType?: string): boolean { ); } -/** - * Compute a report name for a new report being created - * This handles the case where the report doesn't exist in context yet - */ -function computeNameForNewReport(update: OnyxUpdate, context: UpdateContext): string | null { - Performance.markStart(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); - Timing.start(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); - - Log.info('[OptimisticReportNames] Computing name for new report', false, { - updateKey: update.key, - reportID: (update.value as Report)?.reportID, - }); - - const {allPolicies} = context; - - // Extract the new report data from the update - const newReport = update.value as Report; - if (!newReport?.policyID) { - Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); - Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); - return null; - } - - const policy = getPolicyByID(newReport.policyID, allPolicies); - if (!shouldComputeReportName(newReport, policy)) { - Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); - Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); - return null; - } - - const titleField = ReportUtils.getTitleReportField(policy?.fieldList ?? {}); - if (!titleField?.defaultValue) { - Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); - Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); - return null; - } - - // Build context for formula computation - const formulaContext: Formula.FormulaContext = { - report: newReport, - policy, - }; - - const newName = Formula.compute(titleField.defaultValue, formulaContext); - - if (newName && newName !== newReport.reportName) { - Log.info('[OptimisticReportNames] New report name computed successfully', false, { - reportID: newReport.reportID, - oldName: newReport.reportName, - newName, - formula: titleField.defaultValue, - }); - - Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); - Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); - return newName; - } - - Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); - Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME_FOR_NEW_REPORT); - return null; -} - /** * Compute a new report name if needed based on an optimistic update */ @@ -233,10 +168,12 @@ function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: O const policy = getPolicyByID(targetReport.policyID ?? '', allPolicies); if (!shouldComputeReportName(targetReport, policy)) { + console.log('morwa cancel'); Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); return null; } + console.log('morwa continue'); const titleField = ReportUtils.getTitleReportField(policy?.fieldList ?? {}); if (!titleField?.defaultValue) { @@ -403,4 +340,4 @@ function createUpdateContext(): Promise { return getUpdateContextAsync(); } -export {updateOptimisticReportNamesFromUpdates, computeReportNameIfNeeded, computeNameForNewReport, createUpdateContext, shouldComputeReportName}; +export {updateOptimisticReportNamesFromUpdates, computeReportNameIfNeeded, createUpdateContext, shouldComputeReportName}; diff --git a/tests/perf-test/Formula.perf-test.ts b/tests/perf-test/Formula.perf-test.ts index 6379c053034c1..7acc0e1a4dc9f 100644 --- a/tests/perf-test/Formula.perf-test.ts +++ b/tests/perf-test/Formula.perf-test.ts @@ -34,7 +34,7 @@ describe('[CustomFormula] Performance Tests', () => { }); test('[CustomFormula] extract() with nested braces', async () => { - const formula = '{report:created:yyyy-MM-dd} - {field:custom_field|substr:0:10} - {user:email|frontPart}'; + const formula = '{report:{report:submit:from:firstName|substr:2}}'; await measureFunction(() => extract(formula)); }); diff --git a/tests/perf-test/OptimisticReportNames.perf-test.ts b/tests/perf-test/OptimisticReportNames.perf-test.ts index f26184a00f553..17dc02b07f389 100644 --- a/tests/perf-test/OptimisticReportNames.perf-test.ts +++ b/tests/perf-test/OptimisticReportNames.perf-test.ts @@ -1,6 +1,6 @@ import Onyx from 'react-native-onyx'; import {measureFunction} from 'reassure'; -import {computeNameForNewReport, computeReportNameIfNeeded, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames'; +import {computeReportNameIfNeeded, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames'; import type {UpdateContext} from '@libs/OptimisticReportNames'; import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -80,22 +80,6 @@ describe('[OptimisticReportNames] Performance Tests', () => { }); describe('Single Report Name Computation', () => { - test('[OptimisticReportNames] computeNameForNewReport() single report', async () => { - const update = { - key: 'report_123', - onyxMethod: Onyx.METHOD.SET, - value: { - reportID: '123', - policyID: 'policy1', - total: -10000, - currency: 'USD', - lastVisibleActionCreated: new Date().toISOString(), - }, - }; - - await measureFunction(() => computeNameForNewReport(update, mockContext)); - }); - test('[OptimisticReportNames] computeReportNameIfNeeded() single report', async () => { const report = Object.values(mockReports)[0]; const update = { @@ -188,7 +172,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { describe('Large Dataset Performance', () => { test('[OptimisticReportNames] processing with large context (1000 reports)', async () => { - const updates = Array.from({length: 20}, (_, i) => ({ + const updates = Array.from({length: 1000}, (_, i) => ({ key: `report_large${i}`, onyxMethod: Onyx.METHOD.SET, value: { diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index 8d9cc86a66192..b79d4c0a0ce0d 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -22,7 +22,7 @@ describe('CustomFormula', () => { }); test('should handle nested braces', () => { - expect(extract('{report:created:yyyy-MM-dd}')).toEqual(['{report:created:yyyy-MM-dd}']); + expect(extract('{report:{report:submit:from:firstName|substr:2}}')).toEqual(['{report:{report:submit:from:firstName|substr:2}}']); }); test('should handle escaped braces', () => { diff --git a/tests/unit/OptimisticReportNamesTest.ts b/tests/unit/OptimisticReportNamesTest.ts index 73bf3c8cd3c87..f4e56091e0cd1 100644 --- a/tests/unit/OptimisticReportNamesTest.ts +++ b/tests/unit/OptimisticReportNamesTest.ts @@ -1,8 +1,7 @@ import Onyx from 'react-native-onyx'; -import {computeNameForNewReport, computeReportNameIfNeeded, shouldComputeReportName, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames'; +import {computeReportNameIfNeeded, shouldComputeReportName, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames'; import type {UpdateContext} from '@libs/OptimisticReportNames'; import * as ReportUtils from '@libs/ReportUtils'; -import CONST from '@src/CONST'; // Mock dependencies jest.mock('@libs/ReportUtils'); @@ -68,7 +67,7 @@ describe('OptimisticReportNames', () => { expect(result).toBe(false); }); - test('should return false when title field has no formula', () => { + test('should return true when title field has no formula', () => { const policyWithoutFormula = { ...mockPolicy, fieldList: { @@ -77,66 +76,7 @@ describe('OptimisticReportNames', () => { }; mockReportUtils.getTitleReportField.mockReturnValue(policyWithoutFormula.fieldList.text_title); const result = shouldComputeReportName(mockReport as any, policyWithoutFormula as any); - expect(result).toBe(false); - }); - }); - - describe('computeNameForNewReport()', () => { - test('should compute name for new report with formula', () => { - const update = { - key: 'report_123', - onyxMethod: Onyx.METHOD.SET, - value: { - reportID: '123', - policyID: 'policy1', - total: -10000, - currency: 'USD', - lastVisibleActionCreated: '2025-01-15T10:30:00Z', - }, - }; - - const result = computeNameForNewReport(update, mockContext); - expect(result).toEqual('Expense Report - USD 100.00'); - }); - - test('should return null for report without policy', () => { - const update = { - key: 'report_123', - onyxMethod: Onyx.METHOD.SET, - value: {reportID: '123'}, - }; - - const result = computeNameForNewReport(update, mockContext); - expect(result).toBeNull(); - }); - - test('should return null when shouldComputeReportName returns false', () => { - mockReportUtils.isExpenseReport.mockReturnValue(false); - const update = { - key: 'report_123', - onyxMethod: Onyx.METHOD.SET, - value: {reportID: '123', policyID: 'policy1'}, - }; - - const result = computeNameForNewReport(update, mockContext); - expect(result).toBeNull(); - }); - - test('should return null when computed name is same as existing', () => { - const update = { - key: 'report_123', - onyxMethod: Onyx.METHOD.SET, - value: { - reportID: '123', - policyID: 'policy1', - reportName: 'Expense Report - USD 100.00', - total: -10000, - currency: 'USD', - }, - }; - - const result = computeNameForNewReport(update, mockContext); - expect(result).toBeNull(); + expect(result).toBe(true); }); }); @@ -202,19 +142,6 @@ describe('OptimisticReportNames', () => { expect(result[1].value).toEqual({reportName: 'Expense Report - USD 250.00'}); }); - test('should skip processing when no changes needed', () => { - const updates = [ - { - key: 'report_999', - onyxMethod: Onyx.METHOD.MERGE, - value: {description: 'No formula impact'}, - }, - ]; - - const result = updateOptimisticReportNamesFromUpdates(updates, mockContext); - expect(result).toEqual(updates); // Unchanged - }); - test('should handle policy updates affecting multiple reports', () => { const contextWithMultipleReports = { ...mockContext, @@ -261,45 +188,5 @@ describe('OptimisticReportNames', () => { const result = computeReportNameIfNeeded(null as any, update, mockContext); expect(result).toBeNull(); }); - - test('should handle missing policy gracefully', () => { - const contextWithoutPolicy = { - ...mockContext, - allPolicies: {}, - }; - - const result = computeNameForNewReport( - { - key: 'report_123', - onyxMethod: Onyx.METHOD.SET, - value: {reportID: '123', policyID: 'missing'}, - }, - contextWithoutPolicy, - ); - expect(result).toBeNull(); - }); - - test('should handle malformed formula gracefully', () => { - const policyWithBadFormula = { - ...mockPolicy, - fieldList: { - text_title: {defaultValue: '{invalid:formula}'}, - }, - }; - mockReportUtils.getTitleReportField.mockReturnValue(policyWithBadFormula.fieldList.text_title); - - const update = { - key: 'report_123', - onyxMethod: Onyx.METHOD.SET, - value: {reportID: '123', policyID: 'policy1'}, - }; - - const result = computeNameForNewReport(update, { - ...mockContext, - allPolicies: {policy_policy1: policyWithBadFormula}, - }); - // Should still return a result with the invalid formula as-is - expect(result).toBeDefined(); - }); }); }); From d3bca5b69ef42e017fdd637a5a4ef533bd3bec34 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Thu, 7 Aug 2025 12:02:47 +0200 Subject: [PATCH 34/54] Removed dependency on TransactionDetails. Added comment for potentail optimization --- src/libs/Formula.ts | 47 ++++++++++++++++++++++++++++++- src/libs/OptimisticReportNames.ts | 3 ++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 703706909d27b..167a33685a232 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -1,12 +1,13 @@ +import lodashHas from 'lodash/has'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; +import type Transaction from '@src/types/onyx/Transaction'; import {getCurrencySymbol} from './CurrencyUtils'; import {getAllReportActions} from './ReportActionsUtils'; import {getReportTransactions} from './ReportUtils'; -import {getCreated, isPartialTransaction} from './TransactionUtils'; type FormulaPart = { /** The original definition from the formula */ @@ -492,6 +493,50 @@ function getOldestTransactionDate(reportID: string): string | undefined { return oldestDate; } +/** + * Replacement to getCreated from `TransactionUtils`. Inlined because of circular dependency. + * @param transaction + * @returns + */ +function getCreated(transaction: Transaction): string | undefined { + return transaction?.modifiedCreated ? transaction.modifiedCreated : transaction?.created || ''; +} + +/** + * Replacement to isPartialTransaction from `TransactionUtils`. Inlined because of circular dependency. + * @param transaction + * @returns + */ +function isPartialTransaction(transaction: Transaction): boolean { + const merchant = transaction?.modifiedMerchant ? transaction.modifiedMerchant : (transaction?.merchant ?? ''); + + if (!merchant || merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT) { + return true; + } + if (transaction?.amount === 0 && (!transaction.modifiedAmount || transaction.modifiedAmount === 0)) { + return true; + } + + if (isScanRequest(transaction)) { + return true; + } + return false; +} + +/** + * Replacement to isScanRequest from `TransactionUtils`. Inlined because of circular dependency. + * @param transaction + * @returns + */ +function isScanRequest(transaction: OnyxEntry | Partial): boolean { + // This is used during the expense creation flow before the transaction has been saved to the server + if (lodashHas(transaction, 'iouRequestType')) { + return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.SCAN; + } + + return !!transaction?.receipt?.source && transaction?.amount === 0; +} + /** * Check if a string contains formula parts */ diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 87c26b82e5047..a11bac30151df 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -190,6 +190,9 @@ function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: O // Check if any formula part might be affected by this update const isAffected = formulaParts.some((part) => { if (part.type === Formula.FORMULA_PART_TYPES.REPORT) { + // Checking if the formula part is affected in this manner works, but it could certainly be more precise. + // For example, a policy update only affects the part if the formula in the policy changed, or if the report part references a field on the policy. + // However, if we run into performance problems, this would be a good place to optimize. return updateType === 'report' || updateType === 'transaction' || updateType === 'policy'; } if (part.type === Formula.FORMULA_PART_TYPES.FIELD) { From 6aa78f79344c0d39670f68b9430f24363648f512 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Thu, 7 Aug 2025 12:32:10 +0200 Subject: [PATCH 35/54] Fixed Formula tests --- tests/unit/FormulaTest.ts | 83 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index b79d4c0a0ce0d..432c5bc70d767 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -1,7 +1,8 @@ -import {compute, extract, FORMULA_PART_TYPES, isFormula, parse} from '@libs/Formula'; +import {compute, extract, isFormula, parse} from '@libs/Formula'; import type {FormulaContext} from '@libs/Formula'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import type {Policy, Report} from '@src/types/onyx'; // Mock ReportActionsUtils and ReportUtils jest.mock('@libs/ReportActionsUtils', () => ({ @@ -95,14 +96,15 @@ describe('CustomFormula', () => { report: { reportID: '123', reportName: '', + type: 'expense', total: -10000, // -$100.00 currency: 'USD', lastVisibleActionCreated: '2025-01-15T10:30:00Z', policyID: 'policy1', - } as any, + } as Report, policy: { name: 'Test Policy', - }, + } as Policy, }; beforeEach(() => { @@ -133,16 +135,19 @@ describe('CustomFormula', () => { transactionID: 'trans1', created: '2025-01-08T12:00:00Z', // Oldest transaction amount: 5000, + merchant: 'ACME Ltd.', }, { transactionID: 'trans2', created: '2025-01-14T16:45:00Z', // Later transaction amount: 3000, + merchant: 'ACME Ltd.', }, { transactionID: 'trans3', created: '2025-01-11T09:15:00Z', // Middle transaction amount: 2000, + merchant: 'ACME Ltd.', }, ]; @@ -269,9 +274,13 @@ describe('CustomFormula', () => { report: {reportID: '123'} as any, policy: null, }; - + const expected = new Date().toLocaleDateString('en-US', { + month: '2-digit', + day: '2-digit', + year: 'numeric', + }); const result = compute('{report:startdate}', context); - expect(result).toBe(''); + expect(result).toBe(expected); }); test('should call getReportTransactions with correct reportID for startdate', () => { @@ -293,5 +302,69 @@ describe('CustomFormula', () => { compute('{report:created}', context); expect(mockReportActionsUtils.getAllReportActions).toHaveBeenCalledWith('test-report-456'); }); + + test('should skip partial transactions (empty merchant)', () => { + const mockTransactions = [ + { + transactionID: 'trans1', + created: '2025-01-15T12:00:00Z', + amount: 5000, + merchant: 'ACME Ltd.', + }, + { + transactionID: 'trans2', + created: '2025-01-08T16:45:00Z', // Older but partial + amount: 3000, + merchant: '', // Empty merchant = partial + }, + { + transactionID: 'trans3', + created: '2025-01-12T09:15:00Z', // Should be oldest valid + amount: 2000, + merchant: 'Gamma Inc.', + }, + ]; + + mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions as any); + const context: FormulaContext = { + report: {reportID: 'test-report-123'} as any, + policy: null, + }; + + const result = compute('{report:startdate}', context); + expect(result).toBe('01/12/2025'); // Should skip partial transaction + }); + + test('should skip partial transactions (zero amount)', () => { + const mockTransactions = [ + { + transactionID: 'trans1', + created: '2025-01-15T12:00:00Z', + amount: 5000, + merchant: 'ACME Ltd.', + }, + { + transactionID: 'trans2', + created: '2025-01-08T16:45:00Z', // Older but partial + amount: 0, // Zero amount = partial + merchant: 'Beta Corp.', + }, + { + transactionID: 'trans3', + created: '2025-01-12T09:15:00Z', // Should be oldest valid + amount: 2000, + merchant: 'Gamma Inc.', + }, + ]; + + mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions as any); + const context: FormulaContext = { + report: {reportID: 'test-report-123'} as any, + policy: null, + }; + + const result = compute('{report:startdate}', context); + expect(result).toBe('01/12/2025'); // Should skip zero amount transaction + }); }); }); From 474568074e3a4efa11a373f3f2c725a2bab38061 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Thu, 7 Aug 2025 13:05:14 +0200 Subject: [PATCH 36/54] add mock for getCurrencySymbol --- tests/unit/FormulaTest.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index 432c5bc70d767..e49c81d3c981a 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -1,3 +1,4 @@ +import * as CurrencyUtils from '@libs/CurrencyUtils'; import {compute, extract, isFormula, parse} from '@libs/Formula'; import type {FormulaContext} from '@libs/Formula'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; @@ -13,8 +14,13 @@ jest.mock('@libs/ReportUtils', () => ({ getReportTransactions: jest.fn(), })); +jest.mock('@libs/CurrencyUtils', () => ({ + getCurrencySymbol: jest.fn(), +})); + const mockReportActionsUtils = ReportActionsUtils as jest.Mocked; const mockReportUtils = ReportUtils as jest.Mocked; +const mockCurrencyUtils = CurrencyUtils as jest.Mocked; describe('CustomFormula', () => { describe('extract()', () => { @@ -110,6 +116,14 @@ describe('CustomFormula', () => { beforeEach(() => { jest.clearAllMocks(); + // Mock getCurrencySymbol to return $ for USD + mockCurrencyUtils.getCurrencySymbol.mockImplementation((currency: string) => { + if (currency === 'USD') { + return '$'; + } + return currency; + }); + // Mock report actions - test the iteration logic for finding oldest date (for 'created' field) const mockReportActions = { '1': { @@ -157,7 +171,7 @@ describe('CustomFormula', () => { test('should compute basic report formula', () => { const result = compute('{report:type} {report:total}', mockContext); - expect(result).toBe('Expense ReportUSD100.00'); // No space between parts + expect(result).toBe('Expense Report$100.00'); // No space between parts }); test('should compute startdate formula using transactions', () => { @@ -211,7 +225,7 @@ describe('CustomFormula', () => { test('should preserve free text', () => { const result = compute('Expense Report - {report:total}', mockContext); - expect(result).toBe('Expense Report - USD100.00'); + expect(result).toBe('Expense Report - $100.00'); }); test('should preserve exact spacing around formula parts', () => { From ddc1bf73a7f9e09e00a51bb88cdb9f8b74a12a7d Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Thu, 7 Aug 2025 13:24:29 +0200 Subject: [PATCH 37/54] Proper handle of white spaces --- src/libs/Formula.ts | 5 +++-- tests/unit/FormulaTest.ts | 21 ++++++--------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 167a33685a232..f840c4368152e 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -111,7 +111,7 @@ function parse(formula: string): FormulaPart[] { // Add any free text before this formula part if (partIndex > lastIndex) { const freeText = remainingFormula.substring(lastIndex, partIndex); - if (freeText.trim()) { + if (freeText) { parts.push({ definition: freeText, type: FORMULA_PART_TYPES.FREETEXT, @@ -129,7 +129,7 @@ function parse(formula: string): FormulaPart[] { // Add any remaining free text after the last formula part if (lastIndex < remainingFormula.length) { const freeText = remainingFormula.substring(lastIndex); - if (freeText.trim()) { + if (freeText) { parts.push({ definition: freeText, type: FORMULA_PART_TYPES.FREETEXT, @@ -207,6 +207,7 @@ function compute(formula: string, context: FormulaContext): string { switch (part.type) { case FORMULA_PART_TYPES.REPORT: value = computeReportPart(part, context); + value = value === '' ? part.definition : value; break; case FORMULA_PART_TYPES.FIELD: value = computeFieldPart(part); diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index e49c81d3c981a..00f7af6e77de3 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -50,14 +50,14 @@ describe('CustomFormula', () => { describe('parse()', () => { test('should parse report formula parts', () => { const parts = parse('{report:type} {report:startdate}'); - expect(parts).toHaveLength(2); // report:type, report:startdate (space is trimmed) + expect(parts).toHaveLength(3); // report:type, report:startdate (space is trimmed) expect(parts[0]).toEqual({ definition: '{report:type}', type: 'report', fieldPath: ['type'], functions: [], }); - expect(parts[1]).toEqual({ + expect(parts[2]).toEqual({ definition: '{report:startdate}', type: 'report', fieldPath: ['startdate'], @@ -171,7 +171,7 @@ describe('CustomFormula', () => { test('should compute basic report formula', () => { const result = compute('{report:type} {report:total}', mockContext); - expect(result).toBe('Expense Report$100.00'); // No space between parts + expect(result).toBe('Expense Report $100.00'); // No space between parts }); test('should compute startdate formula using transactions', () => { @@ -220,7 +220,7 @@ describe('CustomFormula', () => { policy: null, }; const result = compute('{report:total} {report:policyname}', contextWithMissingData); - expect(result).toBe(''); // Empty strings concatenated = empty string + expect(result).toBe('{report:total} {report:policyname}'); // Empty data is replaced with definition }); test('should preserve free text', () => { @@ -253,22 +253,13 @@ describe('CustomFormula', () => { expect(parts[0].type).toBe('freetext'); }); - test('should handle invalid date', () => { - const context: FormulaContext = { - report: {lastVisibleActionCreated: 'invalid-date'} as any, - policy: null, - }; - const result = compute('{report:startdate}', context); - expect(result).toBe(''); - }); - test('should handle undefined amounts', () => { const context: FormulaContext = { report: {total: undefined} as any, policy: null, }; const result = compute('{report:total}', context); - expect(result).toBe(''); + expect(result).toBe('{report:total}'); }); test('should handle missing report actions for created', () => { @@ -279,7 +270,7 @@ describe('CustomFormula', () => { }; const result = compute('{report:created}', context); - expect(result).toBe(''); + expect(result).toBe('{report:created}'); }); test('should handle missing transactions for startdate', () => { From 032270f1cbc1ea769f968af0c41367303a92523a Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Thu, 7 Aug 2025 14:16:14 +0200 Subject: [PATCH 38/54] Tests fixes --- src/libs/Formula.ts | 45 +---------------------- src/libs/OptimisticReportNames.ts | 2 -- tests/unit/FormulaTest.ts | 3 ++ tests/unit/OptimisticReportNamesTest.ts | 48 +++++++++++++++++++------ 4 files changed, 42 insertions(+), 56 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index f840c4368152e..b2c6286cd6df0 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -8,6 +8,7 @@ import type Transaction from '@src/types/onyx/Transaction'; import {getCurrencySymbol} from './CurrencyUtils'; import {getAllReportActions} from './ReportActionsUtils'; import {getReportTransactions} from './ReportUtils'; +import {getCreated, isPartialTransaction} from './TransactionUtils'; type FormulaPart = { /** The original definition from the formula */ @@ -494,50 +495,6 @@ function getOldestTransactionDate(reportID: string): string | undefined { return oldestDate; } -/** - * Replacement to getCreated from `TransactionUtils`. Inlined because of circular dependency. - * @param transaction - * @returns - */ -function getCreated(transaction: Transaction): string | undefined { - return transaction?.modifiedCreated ? transaction.modifiedCreated : transaction?.created || ''; -} - -/** - * Replacement to isPartialTransaction from `TransactionUtils`. Inlined because of circular dependency. - * @param transaction - * @returns - */ -function isPartialTransaction(transaction: Transaction): boolean { - const merchant = transaction?.modifiedMerchant ? transaction.modifiedMerchant : (transaction?.merchant ?? ''); - - if (!merchant || merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT) { - return true; - } - if (transaction?.amount === 0 && (!transaction.modifiedAmount || transaction.modifiedAmount === 0)) { - return true; - } - - if (isScanRequest(transaction)) { - return true; - } - return false; -} - -/** - * Replacement to isScanRequest from `TransactionUtils`. Inlined because of circular dependency. - * @param transaction - * @returns - */ -function isScanRequest(transaction: OnyxEntry | Partial): boolean { - // This is used during the expense creation flow before the transaction has been saved to the server - if (lodashHas(transaction, 'iouRequestType')) { - return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.SCAN; - } - - return !!transaction?.receipt?.source && transaction?.amount === 0; -} - /** * Check if a string contains formula parts */ diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index a11bac30151df..811864b5c1040 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -168,12 +168,10 @@ function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: O const policy = getPolicyByID(targetReport.policyID ?? '', allPolicies); if (!shouldComputeReportName(targetReport, policy)) { - console.log('morwa cancel'); Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); return null; } - console.log('morwa continue'); const titleField = ReportUtils.getTitleReportField(policy?.fieldList ?? {}); if (!titleField?.defaultValue) { diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index 00f7af6e77de3..0e9d82b916c17 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -3,6 +3,7 @@ import {compute, extract, isFormula, parse} from '@libs/Formula'; import type {FormulaContext} from '@libs/Formula'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import CONST from '@src/CONST'; import type {Policy, Report} from '@src/types/onyx'; // Mock ReportActionsUtils and ReportUtils @@ -11,6 +12,7 @@ jest.mock('@libs/ReportActionsUtils', () => ({ })); jest.mock('@libs/ReportUtils', () => ({ + ...jest.requireActual('@libs/ReportUtils'), getReportTransactions: jest.fn(), })); @@ -353,6 +355,7 @@ describe('CustomFormula', () => { created: '2025-01-08T16:45:00Z', // Older but partial amount: 0, // Zero amount = partial merchant: 'Beta Corp.', + iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, }, { transactionID: 'trans3', diff --git a/tests/unit/OptimisticReportNamesTest.ts b/tests/unit/OptimisticReportNamesTest.ts index f4e56091e0cd1..d57428e1dd824 100644 --- a/tests/unit/OptimisticReportNamesTest.ts +++ b/tests/unit/OptimisticReportNamesTest.ts @@ -2,10 +2,21 @@ import Onyx from 'react-native-onyx'; import {computeReportNameIfNeeded, shouldComputeReportName, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames'; import type {UpdateContext} from '@libs/OptimisticReportNames'; import * as ReportUtils from '@libs/ReportUtils'; +import {Policy, Report} from '@src/types/onyx'; // Mock dependencies -jest.mock('@libs/ReportUtils'); -jest.mock('@libs/Permissions'); +jest.mock('@libs/ReportUtils', () => ({ + ...jest.requireActual('@libs/ReportUtils'), + isExpenseReport: jest.fn(), + getTitleReportField: jest.fn(), +})); +jest.mock('@libs/Permissions', () => ({ + canUseCustomReportNames: jest.fn().mockReturnValue(true), +})); + +jest.mock('@libs/CurrencyUtils', () => ({ + getCurrencySymbol: jest.fn().mockReturnValue('$'), +})); const mockReportUtils = ReportUtils as jest.Mocked; @@ -17,7 +28,7 @@ describe('OptimisticReportNames', () => { defaultValue: '{report:type} - {report:total}', }, }, - }; + } as any as Policy; const mockReport = { reportID: '123', @@ -26,7 +37,8 @@ describe('OptimisticReportNames', () => { total: -10000, currency: 'USD', lastVisibleActionCreated: '2025-01-15T10:30:00Z', - }; + type: 'expense', + } as Report; const mockContext: UpdateContext = { betas: ['authAutoReportTitles'], @@ -50,9 +62,16 @@ describe('OptimisticReportNames', () => { expect(result).toBe(true); }); - test('should return false for non-expense reports', () => { + test('should return false for reports with unsupported type', () => { mockReportUtils.isExpenseReport.mockReturnValue(false); - const result = shouldComputeReportName(mockReport as any, mockPolicy as any); + + const result = shouldComputeReportName( + { + ...mockReport, + type: 'iou', + } as Report, + mockPolicy, + ); expect(result).toBe(false); }); @@ -89,7 +108,7 @@ describe('OptimisticReportNames', () => { }; const result = computeReportNameIfNeeded(mockReport as any, update, mockContext); - expect(result).toEqual('Expense Report - USD 200.00'); + expect(result).toEqual('Expense Report - $200.00'); }); test('should return null when name would not change', () => { @@ -99,7 +118,14 @@ describe('OptimisticReportNames', () => { value: {description: 'Updated description'}, }; - const result = computeReportNameIfNeeded(mockReport as any, update, mockContext); + const result = computeReportNameIfNeeded( + { + ...mockReport, + reportName: 'Expense Report - $100.00', + }, + update, + mockContext, + ); expect(result).toBeNull(); }); }); @@ -115,6 +141,7 @@ describe('OptimisticReportNames', () => { policyID: 'policy1', total: -15000, currency: 'USD', + type: 'expense', }, }, ]; @@ -124,7 +151,7 @@ describe('OptimisticReportNames', () => { expect(result[1]).toEqual({ key: 'report_456', onyxMethod: Onyx.METHOD.MERGE, - value: {reportName: 'Expense Report - USD 150.00'}, + value: {reportName: 'Expense Report - $150.00'}, }); }); @@ -139,7 +166,7 @@ describe('OptimisticReportNames', () => { const result = updateOptimisticReportNamesFromUpdates(updates, mockContext); expect(result).toHaveLength(2); // Original + name update - expect(result[1].value).toEqual({reportName: 'Expense Report - USD 250.00'}); + expect(result[1].value).toEqual({reportName: 'Expense Report - $250.00'}); }); test('should handle policy updates affecting multiple reports', () => { @@ -148,6 +175,7 @@ describe('OptimisticReportNames', () => { allReports: { report_123: {...mockReport, reportID: '123'}, report_456: {...mockReport, reportID: '456'}, + report_456: {...mockReport, reportID: '789'}, }, }; From 3b8e79b0474dc20a00ecc8b1b037cd43579d722b Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 8 Aug 2025 12:08:22 +0200 Subject: [PATCH 39/54] Linter fixes in test files --- src/libs/OptimisticReportNames.ts | 1 + tests/perf-test/Formula.perf-test.ts | 13 ++-- .../OptimisticReportNames.perf-test.ts | 47 +++++------ tests/unit/FormulaTest.ts | 78 ++++++++++--------- tests/unit/OptimisticReportNamesTest.ts | 58 +++++++------- 5 files changed, 105 insertions(+), 92 deletions(-) diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 811864b5c1040..59d2282e2890b 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -342,3 +342,4 @@ function createUpdateContext(): Promise { } export {updateOptimisticReportNamesFromUpdates, computeReportNameIfNeeded, createUpdateContext, shouldComputeReportName}; +export type {UpdateContext}; diff --git a/tests/perf-test/Formula.perf-test.ts b/tests/perf-test/Formula.perf-test.ts index 7acc0e1a4dc9f..dfe574172d326 100644 --- a/tests/perf-test/Formula.perf-test.ts +++ b/tests/perf-test/Formula.perf-test.ts @@ -1,24 +1,25 @@ import {measureFunction} from 'reassure'; import {compute, extract, parse} from '@libs/Formula'; import type {FormulaContext} from '@libs/Formula'; +import type {Policy, Report} from '@src/types/onyx'; describe('[CustomFormula] Performance Tests', () => { const mockReport = { reportID: '123', reportName: 'Test Report', - total: -10000, // -$100.00 + total: -10000, currency: 'USD', lastVisibleActionCreated: '2025-01-15T10:30:00Z', policyID: 'policy1', - }; + } as Report; const mockPolicy = { name: 'Test Policy', id: 'policy1', - }; + } as Policy; const mockContext: FormulaContext = { - report: mockReport as any, + report: mockReport, policy: mockPolicy, }; @@ -73,8 +74,8 @@ describe('[CustomFormula] Performance Tests', () => { test('[CustomFormula] compute() with missing data context', async () => { const formula = '{report:type} - {report:total} - {report:unknown} - {report:policyname}'; const contextWithMissingData: FormulaContext = { - report: {} as any, - policy: null, + report: {} as Report, + policy: null as unknown as Policy, }; await measureFunction(() => compute(formula, contextWithMissingData)); }); diff --git a/tests/perf-test/OptimisticReportNames.perf-test.ts b/tests/perf-test/OptimisticReportNames.perf-test.ts index 17dc02b07f389..80dd319855403 100644 --- a/tests/perf-test/OptimisticReportNames.perf-test.ts +++ b/tests/perf-test/OptimisticReportNames.perf-test.ts @@ -2,16 +2,17 @@ import Onyx from 'react-native-onyx'; import {measureFunction} from 'reassure'; import {computeReportNameIfNeeded, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames'; import type {UpdateContext} from '@libs/OptimisticReportNames'; +// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type Report from '@src/types/onyx/Report'; +import type {OnyxKey} from '@src/ONYXKEYS'; +import type {Policy, Report} from '@src/types/onyx'; import createCollection from '../utils/collections/createCollection'; import {createRandomReport} from '../utils/collections/reports'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; // Mock dependencies jest.mock('@libs/ReportUtils'); -jest.mock('@libs/Permissions'); jest.mock('@libs/Performance', () => ({ markStart: jest.fn(), markEnd: jest.fn(), @@ -34,13 +35,14 @@ describe('[OptimisticReportNames] Performance Tests', () => { id: 'policy1', name: 'Test Policy', fieldList: { + // eslint-disable-next-line @typescript-eslint/naming-convention text_title: { defaultValue: '{report:type} - {report:startdate} - {report:total} {report:currency}', }, }, - }; + } as unknown as Policy; - const mockPolicies = createCollection( + const mockPolicies = createCollection( (item) => `policy_${item.id}`, (index) => ({ ...mockPolicy, @@ -63,15 +65,15 @@ describe('[OptimisticReportNames] Performance Tests', () => { ); const mockContext: UpdateContext = { - betas: ['authAutoReportTitles'], + betas: ['authAutoReportTitle'], allReports: mockReports, allPolicies: mockPolicies, }; - beforeAll(async () => { - await Onyx.init({keys: ONYXKEYS}); + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); mockReportUtils.isExpenseReport.mockReturnValue(true); - mockReportUtils.getTitleReportField.mockReturnValue(mockPolicy.fieldList.text_title); + mockReportUtils.getTitleReportField.mockReturnValue(mockPolicy.fieldList?.text_title); await waitForBatchedUpdates(); }); @@ -81,9 +83,9 @@ describe('[OptimisticReportNames] Performance Tests', () => { describe('Single Report Name Computation', () => { test('[OptimisticReportNames] computeReportNameIfNeeded() single report', async () => { - const report = Object.values(mockReports)[0]; + const report = Object.values(mockReports).at(0); const update = { - key: `report_${report.reportID}`, + key: `report_${report?.reportID}` as OnyxKey, onyxMethod: Onyx.METHOD.MERGE, value: {total: -20000}, }; @@ -95,7 +97,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { describe('Batch Processing Performance', () => { test('[OptimisticReportNames] updateOptimisticReportNamesFromUpdates() with 10 new reports', async () => { const updates = Array.from({length: 10}, (_, i) => ({ - key: `report_new${i}`, + key: `report_new${i}` as OnyxKey, onyxMethod: Onyx.METHOD.SET, value: { reportID: `new${i}`, @@ -110,8 +112,8 @@ describe('[OptimisticReportNames] Performance Tests', () => { }); test('[OptimisticReportNames] updateOptimisticReportNamesFromUpdates() with 50 existing report updates', async () => { - const reportKeys = Object.keys(mockReports).slice(0, 50); - const updates = reportKeys.map((key, i) => ({ + const reportKeys = Object.keys(mockReports).slice(0, 50) as OnyxKey[]; + const updates = reportKeys.map((key) => ({ key, onyxMethod: Onyx.METHOD.MERGE, value: {total: -(Math.random() * 100000)}, @@ -122,7 +124,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { test('[OptimisticReportNames] updateOptimisticReportNamesFromUpdates() with 100 mixed updates', async () => { const newReportUpdates = Array.from({length: 50}, (_, i) => ({ - key: `report_batch${i}`, + key: `report_batch${i}` as OnyxKey, onyxMethod: Onyx.METHOD.SET, value: { reportID: `batch${i}`, @@ -136,7 +138,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { const existingReportUpdates = Object.keys(mockReports) .slice(0, 50) .map((key) => ({ - key, + key: key as OnyxKey, onyxMethod: Onyx.METHOD.MERGE, value: {total: -(Math.random() * 125000)}, })); @@ -150,7 +152,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { describe('Policy Update Impact Performance', () => { test('[OptimisticReportNames] policy update affecting multiple reports', async () => { const policyUpdate = { - key: 'policy_policy1', + key: 'policy_policy1' as OnyxKey, onyxMethod: Onyx.METHOD.MERGE, value: {name: 'Updated Policy Name'}, }; @@ -161,7 +163,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { test('[OptimisticReportNames] multiple policy updates', async () => { const policyUpdates = Array.from({length: 10}, (_, i) => ({ - key: `policy_policy${i}`, + key: `policy_policy${i}` as OnyxKey, onyxMethod: Onyx.METHOD.MERGE, value: {name: `Bulk Updated Policy ${i}`}, })); @@ -173,7 +175,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { describe('Large Dataset Performance', () => { test('[OptimisticReportNames] processing with large context (1000 reports)', async () => { const updates = Array.from({length: 1000}, (_, i) => ({ - key: `report_large${i}`, + key: `report_large${i}` as OnyxKey, onyxMethod: Onyx.METHOD.SET, value: { reportID: `large${i}`, @@ -190,7 +192,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { test('[OptimisticReportNames] worst case: many irrelevant updates', async () => { // Create updates that won't trigger name computation to test filtering performance const irrelevantUpdates = Array.from({length: 100}, (_, i) => ({ - key: `transaction_${i}`, + key: `transaction_${i}` as OnyxKey, onyxMethod: Onyx.METHOD.MERGE, value: {description: `Updated transaction ${i}`}, })); @@ -210,6 +212,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { id: `policy${index}`, name: `Policy ${index}`, fieldList: { + // eslint-disable-next-line @typescript-eslint/naming-convention text_title: {defaultValue: 'Static Title'}, // No formula }, }), @@ -218,7 +221,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { }; const updates = Array.from({length: 20}, (_, i) => ({ - key: `report_static${i}`, + key: `report_static${i}` as OnyxKey, onyxMethod: Onyx.METHOD.SET, value: { reportID: `static${i}`, @@ -233,13 +236,13 @@ describe('[OptimisticReportNames] Performance Tests', () => { test('[OptimisticReportNames] missing policies and reports', async () => { const contextWithMissingData: UpdateContext = { - betas: ['authAutoReportTitles'], + betas: ['authAutoReportTitle'], allReports: {}, allPolicies: {}, }; const updates = Array.from({length: 10}, (_, i) => ({ - key: `report_missing${i}`, + key: `report_missing${i}` as OnyxKey, onyxMethod: Onyx.METHOD.SET, value: { reportID: `missing${i}`, diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index 0e9d82b916c17..f04e0731429e1 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -1,10 +1,13 @@ +// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock import * as CurrencyUtils from '@libs/CurrencyUtils'; -import {compute, extract, isFormula, parse} from '@libs/Formula'; import type {FormulaContext} from '@libs/Formula'; +import {compute, extract, isFormula, parse} from '@libs/Formula'; +// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; -import type {Policy, Report} from '@src/types/onyx'; +import type {Policy, Report, ReportActions, Transaction} from '@src/types/onyx'; // Mock ReportActionsUtils and ReportUtils jest.mock('@libs/ReportActionsUtils', () => ({ @@ -40,8 +43,6 @@ describe('CustomFormula', () => { test('should handle empty formula', () => { expect(extract('')).toEqual([]); - expect(extract(null as any)).toEqual([]); - expect(extract(undefined as any)).toEqual([]); }); test('should handle formula without braces', () => { @@ -53,13 +54,13 @@ describe('CustomFormula', () => { test('should parse report formula parts', () => { const parts = parse('{report:type} {report:startdate}'); expect(parts).toHaveLength(3); // report:type, report:startdate (space is trimmed) - expect(parts[0]).toEqual({ + expect(parts.at(0)).toEqual({ definition: '{report:type}', type: 'report', fieldPath: ['type'], functions: [], }); - expect(parts[2]).toEqual({ + expect(parts.at(2)).toEqual({ definition: '{report:startdate}', type: 'report', fieldPath: ['startdate'], @@ -69,7 +70,7 @@ describe('CustomFormula', () => { test('should parse field formula parts', () => { const parts = parse('{field:custom_field}'); - expect(parts[0]).toEqual({ + expect(parts.at(0)).toEqual({ definition: '{field:custom_field}', type: 'field', fieldPath: ['custom_field'], @@ -79,7 +80,7 @@ describe('CustomFormula', () => { test('should parse user formula parts with functions', () => { const parts = parse('{user:email|frontPart}'); - expect(parts[0]).toEqual({ + expect(parts.at(0)).toEqual({ definition: '{user:email|frontPart}', type: 'user', fieldPath: ['email'], @@ -89,13 +90,12 @@ describe('CustomFormula', () => { test('should handle empty formula', () => { expect(parse('')).toEqual([]); - expect(parse(null as any)).toEqual([]); }); test('should treat formula without braces as free text', () => { const parts = parse('no braces here'); expect(parts).toHaveLength(1); - expect(parts[0].type).toBe('freetext'); + expect(parts.at(0)?.type).toBe('freetext'); }); }); @@ -128,22 +128,25 @@ describe('CustomFormula', () => { // Mock report actions - test the iteration logic for finding oldest date (for 'created' field) const mockReportActions = { + // eslint-disable-next-line @typescript-eslint/naming-convention '1': { reportActionID: '1', created: '2025-01-10T08:00:00Z', // Oldest action actionName: 'CREATED', }, + // eslint-disable-next-line @typescript-eslint/naming-convention '2': { reportActionID: '2', created: '2025-01-15T10:30:00Z', // Later action actionName: 'IOU', }, + // eslint-disable-next-line @typescript-eslint/naming-convention '3': { reportActionID: '3', created: '2025-01-12T14:20:00Z', // Middle action actionName: 'COMMENT', }, - }; + } as unknown as ReportActions; // Mock transactions - test the iteration logic for finding oldest transaction date (for 'startdate' field) const mockTransactions = [ @@ -165,10 +168,10 @@ describe('CustomFormula', () => { amount: 2000, merchant: 'ACME Ltd.', }, - ]; + ] as Transaction[]; - mockReportActionsUtils.getAllReportActions.mockReturnValue(mockReportActions as any); - mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions as any); + mockReportActionsUtils.getAllReportActions.mockReturnValue(mockReportActions); + mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions); }); test('should compute basic report formula', () => { @@ -208,7 +211,6 @@ describe('CustomFormula', () => { test('should handle empty formula', () => { expect(compute('', mockContext)).toBe(''); - expect(compute(null as any, mockContext)).toBe(''); }); test('should handle unknown formula parts', () => { @@ -218,8 +220,8 @@ describe('CustomFormula', () => { test('should handle missing report data gracefully', () => { const contextWithMissingData: FormulaContext = { - report: {} as any, - policy: null, + report: {} as unknown as Report, + policy: null as unknown as Policy, }; const result = compute('{report:total} {report:policyname}', contextWithMissingData); expect(result).toBe('{report:total} {report:policyname}'); // Empty data is replaced with definition @@ -252,13 +254,13 @@ describe('CustomFormula', () => { describe('Edge Cases', () => { test('should handle malformed braces', () => { const parts = parse('{incomplete'); - expect(parts[0].type).toBe('freetext'); + expect(parts.at(0)?.type).toBe('freetext'); }); test('should handle undefined amounts', () => { const context: FormulaContext = { - report: {total: undefined} as any, - policy: null, + report: {total: undefined} as Report, + policy: null as unknown as Policy, }; const result = compute('{report:total}', context); expect(result).toBe('{report:total}'); @@ -267,8 +269,8 @@ describe('CustomFormula', () => { test('should handle missing report actions for created', () => { mockReportActionsUtils.getAllReportActions.mockReturnValue({}); const context: FormulaContext = { - report: {reportID: '123'} as any, - policy: null, + report: {reportID: '123'} as Report, + policy: null as unknown as Policy, }; const result = compute('{report:created}', context); @@ -278,8 +280,8 @@ describe('CustomFormula', () => { test('should handle missing transactions for startdate', () => { mockReportUtils.getReportTransactions.mockReturnValue([]); const context: FormulaContext = { - report: {reportID: '123'} as any, - policy: null, + report: {reportID: '123'} as Report, + policy: null as unknown as Policy, }; const expected = new Date().toLocaleDateString('en-US', { month: '2-digit', @@ -292,8 +294,8 @@ describe('CustomFormula', () => { test('should call getReportTransactions with correct reportID for startdate', () => { const context: FormulaContext = { - report: {reportID: 'test-report-123'} as any, - policy: null, + report: {reportID: 'test-report-123'} as Report, + policy: null as unknown as Policy, }; compute('{report:startdate}', context); @@ -302,8 +304,8 @@ describe('CustomFormula', () => { test('should call getAllReportActions with correct reportID for created', () => { const context: FormulaContext = { - report: {reportID: 'test-report-456'} as any, - policy: null, + report: {reportID: 'test-report-456'} as Report, + policy: null as unknown as Policy, }; compute('{report:created}', context); @@ -330,16 +332,16 @@ describe('CustomFormula', () => { amount: 2000, merchant: 'Gamma Inc.', }, - ]; + ] as Transaction[]; - mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions as any); + mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions); const context: FormulaContext = { - report: {reportID: 'test-report-123'} as any, - policy: null, + report: {reportID: 'test-report-123'} as Report, + policy: null as unknown as Policy, }; const result = compute('{report:startdate}', context); - expect(result).toBe('01/12/2025'); // Should skip partial transaction + expect(result).toBe('01/12/2025'); }); test('should skip partial transactions (zero amount)', () => { @@ -363,16 +365,16 @@ describe('CustomFormula', () => { amount: 2000, merchant: 'Gamma Inc.', }, - ]; + ] as Transaction[]; - mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions as any); + mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions); const context: FormulaContext = { - report: {reportID: 'test-report-123'} as any, - policy: null, + report: {reportID: 'test-report-123'} as Report, + policy: null as unknown as Policy, }; const result = compute('{report:startdate}', context); - expect(result).toBe('01/12/2025'); // Should skip zero amount transaction + expect(result).toBe('01/12/2025'); }); }); }); diff --git a/tests/unit/OptimisticReportNamesTest.ts b/tests/unit/OptimisticReportNamesTest.ts index d57428e1dd824..b67d68fc6aa75 100644 --- a/tests/unit/OptimisticReportNamesTest.ts +++ b/tests/unit/OptimisticReportNamesTest.ts @@ -1,8 +1,10 @@ import Onyx from 'react-native-onyx'; -import {computeReportNameIfNeeded, shouldComputeReportName, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames'; import type {UpdateContext} from '@libs/OptimisticReportNames'; +import {computeReportNameIfNeeded, shouldComputeReportName, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames'; +// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock import * as ReportUtils from '@libs/ReportUtils'; -import {Policy, Report} from '@src/types/onyx'; +import type {OnyxKey} from '@src/ONYXKEYS'; +import type {Policy, Report} from '@src/types/onyx'; // Mock dependencies jest.mock('@libs/ReportUtils', () => ({ @@ -10,9 +12,6 @@ jest.mock('@libs/ReportUtils', () => ({ isExpenseReport: jest.fn(), getTitleReportField: jest.fn(), })); -jest.mock('@libs/Permissions', () => ({ - canUseCustomReportNames: jest.fn().mockReturnValue(true), -})); jest.mock('@libs/CurrencyUtils', () => ({ getCurrencySymbol: jest.fn().mockReturnValue('$'), @@ -24,11 +23,12 @@ describe('OptimisticReportNames', () => { const mockPolicy = { id: 'policy1', fieldList: { + // eslint-disable-next-line @typescript-eslint/naming-convention text_title: { defaultValue: '{report:type} - {report:total}', }, }, - } as any as Policy; + } as unknown as Policy; const mockReport = { reportID: '123', @@ -41,11 +41,13 @@ describe('OptimisticReportNames', () => { } as Report; const mockContext: UpdateContext = { - betas: ['authAutoReportTitles'], + betas: ['authAutoReportTitle'], allReports: { + // eslint-disable-next-line @typescript-eslint/naming-convention report_123: mockReport, }, allPolicies: { + // eslint-disable-next-line @typescript-eslint/naming-convention policy_policy1: mockPolicy, }, }; @@ -53,12 +55,12 @@ describe('OptimisticReportNames', () => { beforeEach(() => { jest.clearAllMocks(); mockReportUtils.isExpenseReport.mockReturnValue(true); - mockReportUtils.getTitleReportField.mockReturnValue(mockPolicy.fieldList.text_title); + mockReportUtils.getTitleReportField.mockReturnValue(mockPolicy.fieldList?.text_title); }); describe('shouldComputeReportName()', () => { test('should return true for expense report with title field formula', () => { - const result = shouldComputeReportName(mockReport as any, mockPolicy as any); + const result = shouldComputeReportName(mockReport, mockPolicy); expect(result).toBe(true); }); @@ -76,13 +78,13 @@ describe('OptimisticReportNames', () => { }); test('should return false when no policy', () => { - const result = shouldComputeReportName(mockReport as any, null); + const result = shouldComputeReportName(mockReport, undefined); expect(result).toBe(false); }); test('should return false when no title field', () => { mockReportUtils.getTitleReportField.mockReturnValue(undefined); - const result = shouldComputeReportName(mockReport as any, mockPolicy as any); + const result = shouldComputeReportName(mockReport, mockPolicy); expect(result).toBe(false); }); @@ -90,11 +92,12 @@ describe('OptimisticReportNames', () => { const policyWithoutFormula = { ...mockPolicy, fieldList: { + // eslint-disable-next-line @typescript-eslint/naming-convention text_title: {defaultValue: 'Static Title'}, }, - }; - mockReportUtils.getTitleReportField.mockReturnValue(policyWithoutFormula.fieldList.text_title); - const result = shouldComputeReportName(mockReport as any, policyWithoutFormula as any); + } as unknown as Policy; + mockReportUtils.getTitleReportField.mockReturnValue(policyWithoutFormula.fieldList?.text_title); + const result = shouldComputeReportName(mockReport, policyWithoutFormula); expect(result).toBe(true); }); }); @@ -102,18 +105,18 @@ describe('OptimisticReportNames', () => { describe('computeReportNameIfNeeded()', () => { test('should compute name when report data changes', () => { const update = { - key: 'report_123', + key: 'report_123' as OnyxKey, onyxMethod: Onyx.METHOD.MERGE, value: {total: -20000}, }; - const result = computeReportNameIfNeeded(mockReport as any, update, mockContext); + const result = computeReportNameIfNeeded(mockReport, update, mockContext); expect(result).toEqual('Expense Report - $200.00'); }); test('should return null when name would not change', () => { const update = { - key: 'report_456', + key: 'report_456' as OnyxKey, onyxMethod: Onyx.METHOD.MERGE, value: {description: 'Updated description'}, }; @@ -134,7 +137,7 @@ describe('OptimisticReportNames', () => { test('should detect new report creation and add name update', () => { const updates = [ { - key: 'report_456', + key: 'report_456' as OnyxKey, onyxMethod: Onyx.METHOD.SET, value: { reportID: '456', @@ -148,7 +151,7 @@ describe('OptimisticReportNames', () => { const result = updateOptimisticReportNamesFromUpdates(updates, mockContext); expect(result).toHaveLength(2); // Original + name update - expect(result[1]).toEqual({ + expect(result.at(1)).toEqual({ key: 'report_456', onyxMethod: Onyx.METHOD.MERGE, value: {reportName: 'Expense Report - $150.00'}, @@ -158,7 +161,7 @@ describe('OptimisticReportNames', () => { test('should handle existing report updates', () => { const updates = [ { - key: 'report_123', + key: 'report_123' as OnyxKey, onyxMethod: Onyx.METHOD.MERGE, value: {total: -25000}, }, @@ -166,22 +169,25 @@ describe('OptimisticReportNames', () => { const result = updateOptimisticReportNamesFromUpdates(updates, mockContext); expect(result).toHaveLength(2); // Original + name update - expect(result[1].value).toEqual({reportName: 'Expense Report - $250.00'}); + expect(result.at(1).value).toEqual({reportName: 'Expense Report - $250.00'}); }); test('should handle policy updates affecting multiple reports', () => { const contextWithMultipleReports = { ...mockContext, allReports: { + // eslint-disable-next-line @typescript-eslint/naming-convention report_123: {...mockReport, reportID: '123'}, + // eslint-disable-next-line @typescript-eslint/naming-convention report_456: {...mockReport, reportID: '456'}, - report_456: {...mockReport, reportID: '789'}, + // eslint-disable-next-line @typescript-eslint/naming-convention + report_789: {...mockReport, reportID: '789'}, }, }; const updates = [ { - key: 'policy_policy1', + key: 'policy_policy1' as OnyxKey, onyxMethod: Onyx.METHOD.MERGE, value: {name: 'Updated Policy Name'}, }, @@ -194,7 +200,7 @@ describe('OptimisticReportNames', () => { test('should handle unknown object types gracefully', () => { const updates = [ { - key: 'unknown_123', + key: 'unknown_123' as OnyxKey, onyxMethod: Onyx.METHOD.MERGE, value: {someData: 'value'}, }, @@ -208,12 +214,12 @@ describe('OptimisticReportNames', () => { describe('Edge Cases', () => { test('should handle missing report gracefully', () => { const update = { - key: 'report_999', + key: 'report_999' as OnyxKey, onyxMethod: Onyx.METHOD.MERGE, value: {total: -10000}, }; - const result = computeReportNameIfNeeded(null as any, update, mockContext); + const result = computeReportNameIfNeeded(undefined, update, mockContext); expect(result).toBeNull(); }); }); From 4c91fe6881f49b9e42d1ec5a60026edc4f32b84e Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 8 Aug 2025 12:49:15 +0200 Subject: [PATCH 40/54] Eslint errors --- src/libs/Formula.ts | 55 +++++++++---------- src/libs/OptimisticReportNames.ts | 39 +++++++------ .../OptimisticReportNamesConnectionManager.ts | 16 +++++- .../OptimisticReportNames.perf-test.ts | 3 + tests/unit/OptimisticReportNamesTest.ts | 14 +++++ 5 files changed, 80 insertions(+), 47 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index b2c6286cd6df0..34c73a03ffa9f 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -1,10 +1,8 @@ -import lodashHas from 'lodash/has'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; -import type Transaction from '@src/types/onyx/Transaction'; import {getCurrencySymbol} from './CurrencyUtils'; import {getAllReportActions} from './ReportActionsUtils'; import {getReportTransactions} from './ReportUtils'; @@ -52,13 +50,13 @@ function extract(formula: string, opener = '{', closer = '}'): string[] { for (let i = 0; i < letters.length; i++) { // Found an escape character, skip the next character - if (letters[i] === '\\') { + if (letters.at(i) === '\\') { i++; continue; } // Found an opener, save the spot - if (letters[i] === opener) { + if (letters.at(i) === opener) { if (nesting === 0) { start = i; } @@ -66,7 +64,7 @@ function extract(formula: string, opener = '{', closer = '}'): string[] { } // Found a closer, decrement the nesting and possibly extract it - if (letters[i] === closer && nesting > 0) { + if (letters.at(i) === closer && nesting > 0) { nesting--; if (nesting === 0) { sections.push(formula.substring(start, i + 1)); @@ -103,15 +101,14 @@ function parse(formula: string): FormulaPart[] { } // Process the formula by splitting on formula parts to preserve free text - let remainingFormula = formula; let lastIndex = 0; formulaParts.forEach((part) => { - const partIndex = remainingFormula.indexOf(part, lastIndex); + const partIndex = formula.indexOf(part, lastIndex); // Add any free text before this formula part if (partIndex > lastIndex) { - const freeText = remainingFormula.substring(lastIndex, partIndex); + const freeText = formula.substring(lastIndex, partIndex); if (freeText) { parts.push({ definition: freeText, @@ -128,8 +125,8 @@ function parse(formula: string): FormulaPart[] { }); // Add any remaining free text after the last formula part - if (lastIndex < remainingFormula.length) { - const freeText = remainingFormula.substring(lastIndex); + if (lastIndex < formula.length) { + const freeText = formula.substring(lastIndex); if (freeText) { parts.push({ definition: freeText, @@ -168,12 +165,12 @@ function parsePart(definition: string): FormulaPart { // Split on | to separate functions const segments = cleanDefinition.split('|'); - const fieldSegment = segments[0]; + const fieldSegment = segments.at(0); const functions = segments.slice(1); // Split the field segment on : to get the field path - const fieldPath = fieldSegment.split(':'); - const type = fieldPath[0]?.toLowerCase(); + const fieldPath = fieldSegment?.split(':'); + const type = fieldPath?.at(0)?.toLowerCase(); // Determine the formula part type if (type === 'report') { @@ -185,7 +182,7 @@ function parsePart(definition: string): FormulaPart { } // Set field path (excluding the type) - part.fieldPath = fieldPath.slice(1); + part.fieldPath = fieldPath?.slice(1) ?? []; part.functions = functions; return part; @@ -339,14 +336,14 @@ function getDomainName(value: string): string { * Get substring of a value */ function getSubstring(value: string, args: string[]): string { - const start = parseInt(args[0], 10) || 0; - const length = args[1] ? parseInt(args[1], 10) : undefined; + const start = parseInt(args.at(0) ?? '', 10) || 0; + const length = args.at(1) ? parseInt(args.at(1) ?? '', 10) : undefined; if (length !== undefined) { - return value.substr(start, length); + return value.substring(start, start + length); } - return value.substr(start); + return value.substring(start); } /** @@ -359,7 +356,7 @@ function formatDate(dateString: string | undefined, format = 'MM/dd/yyyy'): stri try { const date = new Date(dateString); - if (isNaN(date.getTime())) { + if (Number.isNaN(date.getTime())) { return ''; } @@ -373,15 +370,15 @@ function formatDate(dateString: string | undefined, format = 'MM/dd/yyyy'): stri case 'M/dd/yyyy': return `${month}/${day.toString().padStart(2, '0')}/${year}`; case 'MMMM dd, yyyy': - return `${monthNames[month - 1]} ${day.toString().padStart(2, '0')}, ${year}`; + return `${monthNames.at(month - 1)} ${day.toString().padStart(2, '0')}, ${year}`; case 'dd MMM yyyy': - return `${day.toString().padStart(2, '0')} ${shortMonthNames[month - 1]} ${year}`; + return `${day.toString().padStart(2, '0')} ${shortMonthNames.at(month - 1)} ${year}`; case 'yyyy/MM/dd': return `${year}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}`; case 'yyyy-MM-dd': return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`; case 'MMMM, yyyy': - return `${monthNames[month - 1]}, ${year}`; + return `${monthNames.at(month - 1)}, ${year}`; case 'yy/MM/dd': return `${year.toString().slice(-2)}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}`; case 'dd/MM/yy': @@ -431,11 +428,13 @@ function getOldestReportActionDate(reportID: string): string | undefined { let oldestDate: string | undefined; Object.values(reportActions).forEach((action) => { - if (action?.created) { - if (!oldestDate || action.created < oldestDate) { - oldestDate = action.created; - } + if (!action?.created) { + return; + } + if (!(!oldestDate || action.created < oldestDate)) { + return; } + oldestDate = action.created; }); return oldestDate; @@ -502,6 +501,6 @@ function isFormula(str: string): boolean { return extract(str).length > 0; } -export {extract, parse, compute, isFormula, FORMULA_PART_TYPES}; +export {FORMULA_PART_TYPES, compute, extract, isFormula, parse}; -export type {FormulaPart, FormulaContext}; +export type {FormulaContext, FormulaPart}; diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 59d2282e2890b..dd4c07f74b99a 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -5,13 +5,14 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import Timing from './actions/Timing'; -import * as Formula from './Formula'; +import {compute, FORMULA_PART_TYPES, parse} from './Formula'; +import type {FormulaContext} from './Formula'; import Log from './Log'; import {getUpdateContextAsync} from './OptimisticReportNamesConnectionManager'; import type {UpdateContext} from './OptimisticReportNamesConnectionManager'; import Performance from './Performance'; import Permissions from './Permissions'; -import * as ReportUtils from './ReportUtils'; +import {getTitleReportField, isArchivedReport} from './ReportUtils'; /** * Get the object type from an Onyx key @@ -46,6 +47,7 @@ function getPolicyIDFromKey(key: string): string { /** * Extract transaction ID from an Onyx key */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars -- this will be used in near futur function getTransactionIDFromKey(key: string): string { return key.replace(ONYXKEYS.COLLECTION.TRANSACTION, ''); } @@ -67,7 +69,7 @@ function getPolicyByID(policyID: string, allPolicies: Record): P /** * Get all reports associated with a policy ID */ -function getReportsByPolicyID(policyID: string, allReports: Record): Report[] { +function getReportsByPolicyID(policyID: string, allReports: Record, context: UpdateContext): Report[] { if (policyID === CONST.POLICY.ID_FAKE) { return []; } @@ -88,7 +90,8 @@ function getReportsByPolicyID(policyID: string, allReports: Record): Report | undefined { +function getReportByTransactionID(): Report | undefined { // This is a simplified version - in reality, we'd need to look up the transaction // and get its reportID, but for now we'll return undefined // TODO: Implement proper transaction -> report lookup @@ -128,7 +131,7 @@ function shouldComputeReportName(report: Report, policy: Policy | undefined): bo } // Check if the policy has a title field with a formula - const titleField = ReportUtils.getTitleReportField(policy.fieldList ?? {}); + const titleField = getTitleReportField(policy.fieldList ?? {}); if (!titleField?.defaultValue) { return false; } @@ -166,14 +169,14 @@ function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: O return null; } - const policy = getPolicyByID(targetReport.policyID ?? '', allPolicies); + const policy = getPolicyByID(targetReport.policyID ?? String(CONST.DEFAULT_NUMBER_ID), allPolicies); if (!shouldComputeReportName(targetReport, policy)) { Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); return null; } - const titleField = ReportUtils.getTitleReportField(policy?.fieldList ?? {}); + const titleField = getTitleReportField(policy?.fieldList ?? {}); if (!titleField?.defaultValue) { Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); @@ -183,17 +186,17 @@ function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: O // Quick check: see if the update might affect the report name const updateType = determineObjectTypeByKey(incomingUpdate.key); const formula = titleField.defaultValue; - const formulaParts = Formula.parse(formula); + const formulaParts = parse(formula); // Check if any formula part might be affected by this update const isAffected = formulaParts.some((part) => { - if (part.type === Formula.FORMULA_PART_TYPES.REPORT) { + if (part.type === FORMULA_PART_TYPES.REPORT) { // Checking if the formula part is affected in this manner works, but it could certainly be more precise. // For example, a policy update only affects the part if the formula in the policy changed, or if the report part references a field on the policy. // However, if we run into performance problems, this would be a good place to optimize. return updateType === 'report' || updateType === 'transaction' || updateType === 'policy'; } - if (part.type === Formula.FORMULA_PART_TYPES.FIELD) { + if (part.type === FORMULA_PART_TYPES.FIELD) { return updateType === 'report'; } return false; @@ -206,17 +209,18 @@ function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: O } // Build context with the updated data - const updatedReport = updateType === 'report' && targetReport.reportID === getReportIDFromKey(incomingUpdate.key) ? {...targetReport, ...incomingUpdate.value} : targetReport; + const updatedReport = + updateType === 'report' && targetReport.reportID === getReportIDFromKey(incomingUpdate.key) ? {...targetReport, ...(incomingUpdate.value as Partial)} : targetReport; - const updatedPolicy = updateType === 'policy' && targetReport.policyID === getPolicyIDFromKey(incomingUpdate.key) ? {...policy, ...incomingUpdate.value} : policy; + const updatedPolicy = updateType === 'policy' && targetReport.policyID === getPolicyIDFromKey(incomingUpdate.key) ? {...(policy ?? {}), ...(incomingUpdate.value as Policy)} : policy; // Compute the new name - const formulaContext: Formula.FormulaContext = { + const formulaContext: FormulaContext = { report: updatedReport, policy: updatedPolicy, }; - const newName = Formula.compute(formula, formulaContext); + const newName = compute(formula, formulaContext); // Only return an update if the name actually changed if (newName && newName !== targetReport.reportName) { @@ -290,13 +294,12 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: case 'policy': { const policyID = getPolicyIDFromKey(update.key); - affectedReports = getReportsByPolicyID(policyID, allReports); + affectedReports = getReportsByPolicyID(policyID, allReports, context); break; } case 'transaction': { - const transactionID = getTransactionIDFromKey(update.key); - const report = getReportByTransactionID(transactionID, allReports); + const report = getReportByTransactionID(); if (report) { affectedReports = [report]; } diff --git a/src/libs/OptimisticReportNamesConnectionManager.ts b/src/libs/OptimisticReportNamesConnectionManager.ts index 29e494ec6e038..14248d4a59445 100644 --- a/src/libs/OptimisticReportNamesConnectionManager.ts +++ b/src/libs/OptimisticReportNamesConnectionManager.ts @@ -4,19 +4,22 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type Beta from '@src/types/onyx/Beta'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; +import type ReportNameValuePairs from '@src/types/onyx/ReportNameValuePairs'; type UpdateContext = { betas: OnyxEntry; allReports: Record; allPolicies: Record; + allReportNameValuePairs: Record; }; let betas: OnyxEntry; let allReports: Record; let allPolicies: Record; +let allReportNameValuePairs: Record; let isInitialized = false; let connectionsInitializedCount = 0; -const totalConnections = 3; +const totalConnections = 4; let initializationPromise: Promise | null = null; /** @@ -76,6 +79,16 @@ function initialize(): Promise { checkAndMarkInitialized(); }, }); + + // Connect to all REPORT_NAME_VALUE_PAIRS + Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, + waitForCollectionCallback: true, + callback: (val) => { + allReportNameValuePairs = (val as Record) ?? {}; + checkAndMarkInitialized(); + }, + }); }); return initializationPromise; @@ -90,6 +103,7 @@ function getUpdateContextAsync(): Promise { betas, allReports: allReports ?? {}, allPolicies: allPolicies ?? {}, + allReportNameValuePairs: allReportNameValuePairs ?? {}, })); } diff --git a/tests/perf-test/OptimisticReportNames.perf-test.ts b/tests/perf-test/OptimisticReportNames.perf-test.ts index 80dd319855403..c0414bbb51f77 100644 --- a/tests/perf-test/OptimisticReportNames.perf-test.ts +++ b/tests/perf-test/OptimisticReportNames.perf-test.ts @@ -68,6 +68,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { betas: ['authAutoReportTitle'], allReports: mockReports, allPolicies: mockPolicies, + allReportNameValuePairs: {}, }; beforeAll(() => { @@ -218,6 +219,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { }), 50, ), + allReportNameValuePairs: {}, }; const updates = Array.from({length: 20}, (_, i) => ({ @@ -239,6 +241,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { betas: ['authAutoReportTitle'], allReports: {}, allPolicies: {}, + allReportNameValuePairs: {}, }; const updates = Array.from({length: 10}, (_, i) => ({ diff --git a/tests/unit/OptimisticReportNamesTest.ts b/tests/unit/OptimisticReportNamesTest.ts index b67d68fc6aa75..cf96dc6c48eb3 100644 --- a/tests/unit/OptimisticReportNamesTest.ts +++ b/tests/unit/OptimisticReportNamesTest.ts @@ -50,6 +50,12 @@ describe('OptimisticReportNames', () => { // eslint-disable-next-line @typescript-eslint/naming-convention policy_policy1: mockPolicy, }, + allReportNameValuePairs: { + // eslint-disable-next-line @typescript-eslint/naming-convention + reportNameValuePairs_123: { + private_isArchived: '', + }, + }, }; beforeEach(() => { @@ -183,6 +189,14 @@ describe('OptimisticReportNames', () => { // eslint-disable-next-line @typescript-eslint/naming-convention report_789: {...mockReport, reportID: '789'}, }, + allReportNameValuePairs: { + // eslint-disable-next-line @typescript-eslint/naming-convention + reportNameValuePairs_123: {private_isArchived: ''}, + // eslint-disable-next-line @typescript-eslint/naming-convention + reportNameValuePairs_456: {private_isArchived: ''}, + // eslint-disable-next-line @typescript-eslint/naming-convention + reportNameValuePairs_789: {private_isArchived: ''}, + }, }; const updates = [ From b0dd01155fa96c12898b4e1b5a81e1e7746c52db Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 8 Aug 2025 12:58:17 +0200 Subject: [PATCH 41/54] spellcheck --- src/libs/OptimisticReportNames.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index dd4c07f74b99a..80b5e0fd9a233 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -47,7 +47,7 @@ function getPolicyIDFromKey(key: string): string { /** * Extract transaction ID from an Onyx key */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -- this will be used in near futur +// eslint-disable-next-line @typescript-eslint/no-unused-vars -- this will be used in near future function getTransactionIDFromKey(key: string): string { return key.replace(ONYXKEYS.COLLECTION.TRANSACTION, ''); } From 97a35d55bc6a6744435f0076d46168ea56546bc3 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 8 Aug 2025 13:14:20 +0200 Subject: [PATCH 42/54] typecheck errors --- src/libs/OptimisticReportNames.ts | 6 ++-- .../OptimisticReportNames.perf-test.ts | 33 +++++++++++++------ tests/unit/OptimisticReportNamesTest.ts | 2 +- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 80b5e0fd9a233..b776b29c8e59e 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -1,7 +1,7 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; +import ONYXKEYS, {OnyxKey} from '@src/ONYXKEYS'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import Timing from './actions/Timing'; @@ -112,8 +112,8 @@ function getReportByTransactionID(): Report | undefined { /** * Generate the Onyx key for a report */ -function getReportKey(reportID: string): string { - return `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; +function getReportKey(reportID: string): OnyxKey { + return `${ONYXKEYS.COLLECTION.REPORT}${reportID}` as OnyxKey; } /** diff --git a/tests/perf-test/OptimisticReportNames.perf-test.ts b/tests/perf-test/OptimisticReportNames.perf-test.ts index c0414bbb51f77..85a9eca4960f0 100644 --- a/tests/perf-test/OptimisticReportNames.perf-test.ts +++ b/tests/perf-test/OptimisticReportNames.perf-test.ts @@ -6,7 +6,7 @@ import type {UpdateContext} from '@libs/OptimisticReportNames'; import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxKey} from '@src/ONYXKEYS'; -import type {Policy, Report} from '@src/types/onyx'; +import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; import createCollection from '../utils/collections/createCollection'; import {createRandomReport} from '../utils/collections/reports'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -71,7 +71,7 @@ describe('[OptimisticReportNames] Performance Tests', () => { allReportNameValuePairs: {}, }; - beforeAll(() => { + beforeAll(async () => { Onyx.init({keys: ONYXKEYS}); mockReportUtils.isExpenseReport.mockReturnValue(true); mockReportUtils.getTitleReportField.mockReturnValue(mockPolicy.fieldList?.text_title); @@ -209,14 +209,27 @@ describe('[OptimisticReportNames] Performance Tests', () => { ...mockContext, allPolicies: createCollection( (item) => `policy_${item.id}`, - (index) => ({ - id: `policy${index}`, - name: `Policy ${index}`, - fieldList: { - // eslint-disable-next-line @typescript-eslint/naming-convention - text_title: {defaultValue: 'Static Title'}, // No formula - }, - }), + (index) => + ({ + id: `policy${index}`, + name: `Policy ${index}`, + fieldList: { + // eslint-disable-next-line @typescript-eslint/naming-convention + text_title: { + name: 'Title', + defaultValue: 'Static Title', + fieldID: 'text_title', + orderWeight: 0, + type: 'text' as const, + deletable: true, + values: [], + keys: [], + externalIDs: [], + disabledOptions: [], + isTax: false, + }, + }, + }) as unknown as Policy, 50, ), allReportNameValuePairs: {}, diff --git a/tests/unit/OptimisticReportNamesTest.ts b/tests/unit/OptimisticReportNamesTest.ts index cf96dc6c48eb3..4af57a3c8d0c0 100644 --- a/tests/unit/OptimisticReportNamesTest.ts +++ b/tests/unit/OptimisticReportNamesTest.ts @@ -175,7 +175,7 @@ describe('OptimisticReportNames', () => { const result = updateOptimisticReportNamesFromUpdates(updates, mockContext); expect(result).toHaveLength(2); // Original + name update - expect(result.at(1).value).toEqual({reportName: 'Expense Report - $250.00'}); + expect(result.at(1)?.value).toEqual({reportName: 'Expense Report - $250.00'}); }); test('should handle policy updates affecting multiple reports', () => { From b67b3f7e5bcf70130f28e4de1935a8c457a0401e Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 8 Aug 2025 13:20:12 +0200 Subject: [PATCH 43/54] lint fixes --- src/libs/OptimisticReportNames.ts | 3 ++- tests/perf-test/OptimisticReportNames.perf-test.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index b776b29c8e59e..9d3ca2f209596 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -1,7 +1,8 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; -import ONYXKEYS, {OnyxKey} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {OnyxKey} from '@src/ONYXKEYS'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import Timing from './actions/Timing'; diff --git a/tests/perf-test/OptimisticReportNames.perf-test.ts b/tests/perf-test/OptimisticReportNames.perf-test.ts index 85a9eca4960f0..4c6516ca19b73 100644 --- a/tests/perf-test/OptimisticReportNames.perf-test.ts +++ b/tests/perf-test/OptimisticReportNames.perf-test.ts @@ -1,12 +1,12 @@ import Onyx from 'react-native-onyx'; import {measureFunction} from 'reassure'; -import {computeReportNameIfNeeded, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames'; import type {UpdateContext} from '@libs/OptimisticReportNames'; +import {computeReportNameIfNeeded, updateOptimisticReportNamesFromUpdates} from '@libs/OptimisticReportNames'; // eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock import * as ReportUtils from '@libs/ReportUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxKey} from '@src/ONYXKEYS'; -import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report} from '@src/types/onyx'; import createCollection from '../utils/collections/createCollection'; import {createRandomReport} from '../utils/collections/reports'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; From cb998e126d213d1820b36bd6bcf4ea5d6d216c00 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Fri, 8 Aug 2025 17:17:30 +0200 Subject: [PATCH 44/54] comment cleanse --- tests/unit/FormulaTest.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index f04e0731429e1..c24cc8e201a78 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -9,7 +9,6 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import type {Policy, Report, ReportActions, Transaction} from '@src/types/onyx'; -// Mock ReportActionsUtils and ReportUtils jest.mock('@libs/ReportActionsUtils', () => ({ getAllReportActions: jest.fn(), })); @@ -53,7 +52,7 @@ describe('CustomFormula', () => { describe('parse()', () => { test('should parse report formula parts', () => { const parts = parse('{report:type} {report:startdate}'); - expect(parts).toHaveLength(3); // report:type, report:startdate (space is trimmed) + expect(parts).toHaveLength(3); expect(parts.at(0)).toEqual({ definition: '{report:type}', type: 'report', @@ -118,7 +117,6 @@ describe('CustomFormula', () => { beforeEach(() => { jest.clearAllMocks(); - // Mock getCurrencySymbol to return $ for USD mockCurrencyUtils.getCurrencySymbol.mockImplementation((currency: string) => { if (currency === 'USD') { return '$'; @@ -126,7 +124,6 @@ describe('CustomFormula', () => { return currency; }); - // Mock report actions - test the iteration logic for finding oldest date (for 'created' field) const mockReportActions = { // eslint-disable-next-line @typescript-eslint/naming-convention '1': { @@ -148,7 +145,6 @@ describe('CustomFormula', () => { }, } as unknown as ReportActions; - // Mock transactions - test the iteration logic for finding oldest transaction date (for 'startdate' field) const mockTransactions = [ { transactionID: 'trans1', From 739b79d4283cc0be9e332d40e8146d40125b1dca Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 11 Aug 2025 11:19:14 +0200 Subject: [PATCH 45/54] PR comment fixes --- src/libs/Formula.ts | 14 +++++--------- src/libs/OptimisticReportNames.ts | 8 ++++++-- tests/unit/FormulaTest.ts | 19 +++---------------- tests/unit/OptimisticReportNamesTest.ts | 13 +++++++++++-- 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index 34c73a03ffa9f..b5e6fccc95ea0 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -253,6 +253,8 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { case 'workspacename': return policy?.name ?? ''; case 'created': + // Backend will always return at least one report action (of type created) and its date is equal to report's creation date + // We can make it slightly more efficient in the future by ensuring report.created is always present in backend's responses return formatDate(getOldestReportActionDate(report.reportID), format); default: return part.definition; @@ -431,7 +433,8 @@ function getOldestReportActionDate(reportID: string): string | undefined { if (!action?.created) { return; } - if (!(!oldestDate || action.created < oldestDate)) { + + if (oldestDate && action.created > oldestDate) { return; } oldestDate = action.created; @@ -494,13 +497,6 @@ function getOldestTransactionDate(reportID: string): string | undefined { return oldestDate; } -/** - * Check if a string contains formula parts - */ -function isFormula(str: string): boolean { - return extract(str).length > 0; -} - -export {FORMULA_PART_TYPES, compute, extract, isFormula, parse}; +export {FORMULA_PART_TYPES, compute, extract, parse}; export type {FormulaContext, FormulaPart}; diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 9d3ca2f209596..44775e0b7f8cd 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -63,7 +63,10 @@ function getReportByID(reportID: string, allReports: Record): Re /** * Get policy by ID from the policies collection */ -function getPolicyByID(policyID: string, allPolicies: Record): Policy | undefined { +function getPolicyByID(policyID: string | undefined, allPolicies: Record): Policy | undefined { + if (!policyID) { + return; + } return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; } @@ -170,7 +173,8 @@ function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: O return null; } - const policy = getPolicyByID(targetReport.policyID ?? String(CONST.DEFAULT_NUMBER_ID), allPolicies); + const policy = getPolicyByID(targetReport.policyID, allPolicies); + if (!shouldComputeReportName(targetReport, policy)) { Performance.markEnd(CONST.TIMING.COMPUTE_REPORT_NAME); Timing.end(CONST.TIMING.COMPUTE_REPORT_NAME); diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index c24cc8e201a78..cc79765d2b5e7 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -1,8 +1,8 @@ -// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock +// eslint-disable-next-line no-restricted-syntax -- disabled because we need CurrencyUtils to mock import * as CurrencyUtils from '@libs/CurrencyUtils'; import type {FormulaContext} from '@libs/Formula'; -import {compute, extract, isFormula, parse} from '@libs/Formula'; -// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock +import {compute, extract, parse} from '@libs/Formula'; +// eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportActionsUtils to mock import * as ReportActionsUtils from '@libs/ReportActionsUtils'; // eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock import * as ReportUtils from '@libs/ReportUtils'; @@ -234,19 +234,6 @@ describe('CustomFormula', () => { }); }); - describe('isFormula()', () => { - test('should detect formulas', () => { - expect(isFormula('{report:type}')).toBe(true); - expect(isFormula('Text with {report:type} formula')).toBe(true); - }); - - test('should detect non-formulas', () => { - expect(isFormula('plain text')).toBe(false); - expect(isFormula('\\{escaped}')).toBe(false); - expect(isFormula('')).toBe(false); - }); - }); - describe('Edge Cases', () => { test('should handle malformed braces', () => { const parts = parse('{incomplete'); diff --git a/tests/unit/OptimisticReportNamesTest.ts b/tests/unit/OptimisticReportNamesTest.ts index 4af57a3c8d0c0..dc000fb2bb9a1 100644 --- a/tests/unit/OptimisticReportNamesTest.ts +++ b/tests/unit/OptimisticReportNamesTest.ts @@ -4,7 +4,7 @@ import {computeReportNameIfNeeded, shouldComputeReportName, updateOptimisticRepo // eslint-disable-next-line no-restricted-syntax -- disabled because we need ReportUtils to mock import * as ReportUtils from '@libs/ReportUtils'; import type {OnyxKey} from '@src/ONYXKEYS'; -import type {Policy, Report} from '@src/types/onyx'; +import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; // Mock dependencies jest.mock('@libs/ReportUtils', () => ({ @@ -198,6 +198,7 @@ describe('OptimisticReportNames', () => { reportNameValuePairs_789: {private_isArchived: ''}, }, }; + mockReportUtils.getTitleReportField.mockReturnValue({defaultValue: 'Policy: {report:policyname}'} as unknown as PolicyReportField); const updates = [ { @@ -208,7 +209,15 @@ describe('OptimisticReportNames', () => { ]; const result = updateOptimisticReportNamesFromUpdates(updates, contextWithMultipleReports); - expect(result.length).toBeGreaterThan(1); + + expect(result.length).toEqual(4); + result.forEach((update) => { + if (!update.key.startsWith('report_')) { + return; + } + + expect((update.value as {reportName?: string})?.reportName).toEqual('Policy: Updated Policy Name'); + }); }); test('should handle unknown object types gracefully', () => { From df844a53dd06bcc690e75fa1223a286a0aaf1bb7 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 11 Aug 2025 14:04:17 +0200 Subject: [PATCH 46/54] Make OptimisticReportNames logic synchronous in prepareRequest --- src/libs/API/index.ts | 25 +++++++++++-------- .../OptimisticReportNamesConnectionManager.ts | 25 ++++++++++++++----- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 41f81cfc6441f..5d9fa71baa407 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -8,6 +8,7 @@ import {handleDeletedAccount, HandleUnusedOptimisticID, Logging, Pagination, Rea import {isOffline} from '@libs/Network/NetworkStore'; import {push as pushToSequentialQueue, waitForIdle as waitForSequentialQueueIdle} from '@libs/Network/SequentialQueue'; import * as OptimisticReportNames from '@libs/OptimisticReportNames'; +import {getUpdateContext, initialize as initializeOptimisticReportNamesContext} from '@libs/OptimisticReportNamesConnectionManager'; import Pusher from '@libs/Pusher'; import {processWithMiddleware, use} from '@libs/Request'; import {getAll, getLength as getPersistedRequestsLength} from '@userActions/PersistedRequests'; @@ -43,6 +44,11 @@ use(Pagination); // middlewares after this, because the SequentialQueue depends on the result of this middleware to pause the queue (if needed) to bring the app to an up-to-date state. use(SaveResponseInOnyx); +// Initialize OptimisticReportNames context on module load +initializeOptimisticReportNamesContext().catch((error) => { + Log.error('Failed to initialize OptimisticReportNames context', error); +}); + let requestIndex = 0; type OnyxData = { @@ -77,16 +83,15 @@ function prepareRequest( Log.info('[API] Applying optimistic data', false, {command, type}); // Process optimistic data through report name middleware - OptimisticReportNames.createUpdateContext() - .then((context) => { - const processedOptimisticData = OptimisticReportNames.updateOptimisticReportNamesFromUpdates(optimisticData, context); - Onyx.update(processedOptimisticData); - }) - .catch((error) => { - Log.warn('[API] Failed to process optimistic report names', {error}); - // Fallback to original optimistic data if processing fails - Onyx.update(optimisticData); - }); + try { + const context = getUpdateContext(); + const processedOptimisticData = OptimisticReportNames.updateOptimisticReportNamesFromUpdates(optimisticData, context); + Onyx.update(processedOptimisticData); + } catch (error) { + Log.warn('[API] Failed to process optimistic report names', {error}); + // Fallback to original optimistic data if processing fails + Onyx.update(optimisticData); + } } const isWriteRequest = type === CONST.API_REQUEST_TYPE.WRITE; diff --git a/src/libs/OptimisticReportNamesConnectionManager.ts b/src/libs/OptimisticReportNamesConnectionManager.ts index 14248d4a59445..161203fca1076 100644 --- a/src/libs/OptimisticReportNamesConnectionManager.ts +++ b/src/libs/OptimisticReportNamesConnectionManager.ts @@ -95,17 +95,30 @@ function initialize(): Promise { } /** - * Get the current update context as a promise for backward compatibility - * Initializes connections lazily on first use + * Get the current update context synchronously + * Must be called after initialize() has completed */ -function getUpdateContextAsync(): Promise { - return initialize().then(() => ({ +function getUpdateContext(): UpdateContext { + if (!isInitialized) { + console.log('morwa OptimisticReportNamesConnectionManager: getUpdateContext called before initialization'); + throw new Error('OptimisticReportNamesConnectionManager not initialized. Call initialize() first.'); + } + + return { betas, allReports: allReports ?? {}, allPolicies: allPolicies ?? {}, allReportNameValuePairs: allReportNameValuePairs ?? {}, - })); + }; +} + +/** + * Get the current update context as a promise for backward compatibility + * Initializes connections lazily on first use + */ +function getUpdateContextAsync(): Promise { + return initialize().then(() => getUpdateContext()); } -export {getUpdateContextAsync}; +export {initialize, getUpdateContext, getUpdateContextAsync}; export type {UpdateContext}; From 87989c8c9ce14d7a2028402ee6e4f83962e55a73 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 11 Aug 2025 14:09:33 +0200 Subject: [PATCH 47/54] eslint fixes --- src/libs/API/index.ts | 4 ++-- src/libs/OptimisticReportNamesConnectionManager.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 5d9fa71baa407..24042c34001db 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -45,8 +45,8 @@ use(Pagination); use(SaveResponseInOnyx); // Initialize OptimisticReportNames context on module load -initializeOptimisticReportNamesContext().catch((error) => { - Log.error('Failed to initialize OptimisticReportNames context', error); +initializeOptimisticReportNamesContext().catch(() => { + Log.warn('Failed to initialize OptimisticReportNames context'); }); let requestIndex = 0; diff --git a/src/libs/OptimisticReportNamesConnectionManager.ts b/src/libs/OptimisticReportNamesConnectionManager.ts index 161203fca1076..fe71791edce54 100644 --- a/src/libs/OptimisticReportNamesConnectionManager.ts +++ b/src/libs/OptimisticReportNamesConnectionManager.ts @@ -100,10 +100,9 @@ function initialize(): Promise { */ function getUpdateContext(): UpdateContext { if (!isInitialized) { - console.log('morwa OptimisticReportNamesConnectionManager: getUpdateContext called before initialization'); throw new Error('OptimisticReportNamesConnectionManager not initialized. Call initialize() first.'); } - + return { betas, allReports: allReports ?? {}, From 71fed6b3c7cafd00f50f682a34f3b9b9dee88dbc Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 11 Aug 2025 14:12:53 +0200 Subject: [PATCH 48/54] Skip optimisitc report name processing for OpenReport --- src/libs/API/index.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 24042c34001db..317e51186eaa4 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -17,7 +17,7 @@ import type OnyxRequest from '@src/types/onyx/Request'; import type {PaginatedRequest, PaginationConfig, RequestConflictResolver} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; import type {ApiCommand, ApiRequestCommandParameters, ApiRequestType, CommandOfType, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types'; -import {READ_COMMANDS} from './types'; +import {READ_COMMANDS, WRITE_COMMANDS} from './types'; // Setup API middlewares. Each request made will pass through a series of middleware functions that will get called in sequence (each one passing the result of the previous to the next). // Note: The ordering here is intentional as we want to Log, Recheck Connection, Reauthenticate, and Save the Response in Onyx. Errors thrown in one middleware will bubble to the next. @@ -71,6 +71,8 @@ function prepareRequest( ): OnyxRequest { Log.info('[API] Preparing request', false, {command, type}); + console.log('morwa {command, type}', {command, type}); + let shouldApplyOptimisticData = true; if (conflictResolver?.checkAndFixConflictingRequest) { const requests = getAll(); @@ -83,14 +85,19 @@ function prepareRequest( Log.info('[API] Applying optimistic data', false, {command, type}); // Process optimistic data through report name middleware - try { - const context = getUpdateContext(); - const processedOptimisticData = OptimisticReportNames.updateOptimisticReportNamesFromUpdates(optimisticData, context); - Onyx.update(processedOptimisticData); - } catch (error) { - Log.warn('[API] Failed to process optimistic report names', {error}); - // Fallback to original optimistic data if processing fails + // Skip for OpenReport command to avoid unnecessary processing + if (command === WRITE_COMMANDS.OPEN_REPORT) { Onyx.update(optimisticData); + } else { + try { + const context = getUpdateContext(); + const processedOptimisticData = OptimisticReportNames.updateOptimisticReportNamesFromUpdates(optimisticData, context); + Onyx.update(processedOptimisticData); + } catch (error) { + Log.warn('[API] Failed to process optimistic report names', {error}); + // Fallback to original optimistic data if processing fails + Onyx.update(optimisticData); + } } } From 3defbcb8e6c2466c17015bb734973a559a2721ed Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 11 Aug 2025 14:15:13 +0200 Subject: [PATCH 49/54] remove console.log --- src/libs/API/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 317e51186eaa4..4157c20e04902 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -71,8 +71,6 @@ function prepareRequest( ): OnyxRequest { Log.info('[API] Preparing request', false, {command, type}); - console.log('morwa {command, type}', {command, type}); - let shouldApplyOptimisticData = true; if (conflictResolver?.checkAndFixConflictingRequest) { const requests = getAll(); From c3279654cba63ee64fa97d60c9d0b496c06bf22c Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 11 Aug 2025 14:38:04 +0200 Subject: [PATCH 50/54] chnaged default format to complain with backend --- src/libs/Formula.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index b5e6fccc95ea0..afb8cd4cb4897 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -377,8 +377,6 @@ function formatDate(dateString: string | undefined, format = 'MM/dd/yyyy'): stri return `${day.toString().padStart(2, '0')} ${shortMonthNames.at(month - 1)} ${year}`; case 'yyyy/MM/dd': return `${year}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}`; - case 'yyyy-MM-dd': - return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`; case 'MMMM, yyyy': return `${monthNames.at(month - 1)}, ${year}`; case 'yy/MM/dd': @@ -388,8 +386,10 @@ function formatDate(dateString: string | undefined, format = 'MM/dd/yyyy'): stri case 'yyyy': return year.toString(); case 'MM/dd/yyyy': - default: return `${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}/${year}`; + case 'yyyy-MM-dd': + default: + return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`; } } catch { return ''; From 64f7122dd98e6d5e29c2c867768e5ecfa99284dc Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 11 Aug 2025 14:43:06 +0200 Subject: [PATCH 51/54] Remove tracking for transaction updates --- src/libs/OptimisticReportNames.ts | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 44775e0b7f8cd..5899deaeff225 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -18,16 +18,13 @@ import {getTitleReportField, isArchivedReport} from './ReportUtils'; /** * Get the object type from an Onyx key */ -function determineObjectTypeByKey(key: string): 'report' | 'policy' | 'transaction' | 'unknown' { +function determineObjectTypeByKey(key: string): 'report' | 'policy' | 'unknown' { if (key.startsWith(ONYXKEYS.COLLECTION.REPORT)) { return 'report'; } if (key.startsWith(ONYXKEYS.COLLECTION.POLICY)) { return 'policy'; } - if (key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) { - return 'transaction'; - } return 'unknown'; } @@ -103,16 +100,6 @@ function getReportsByPolicyID(policyID: string, allReports: Record report lookup - return undefined; -} - /** * Generate the Onyx key for a report */ @@ -199,7 +186,7 @@ function computeReportNameIfNeeded(report: Report | undefined, incomingUpdate: O // Checking if the formula part is affected in this manner works, but it could certainly be more precise. // For example, a policy update only affects the part if the formula in the policy changed, or if the report part references a field on the policy. // However, if we run into performance problems, this would be a good place to optimize. - return updateType === 'report' || updateType === 'transaction' || updateType === 'policy'; + return updateType === 'report' || updateType === 'policy'; } if (part.type === FORMULA_PART_TYPES.FIELD) { return updateType === 'report'; @@ -303,14 +290,6 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: break; } - case 'transaction': { - const report = getReportByTransactionID(); - if (report) { - affectedReports = [report]; - } - break; - } - default: continue; } From 869b6ad309c56c606292ba055a2386596962f29b Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 11 Aug 2025 15:08:27 +0200 Subject: [PATCH 52/54] default date format --- src/libs/Formula.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index afb8cd4cb4897..e00efa0e597ba 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -351,7 +351,7 @@ function getSubstring(value: string, args: string[]): string { /** * Format a date value with support for multiple date formats */ -function formatDate(dateString: string | undefined, format = 'MM/dd/yyyy'): string { +function formatDate(dateString: string | undefined, format = 'yyyy-MM-dd'): string { if (!dateString) { return ''; } From 1dc80acfab8cd34eb62186a6227b2927ef215cd3 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Mon, 11 Aug 2025 15:42:05 +0200 Subject: [PATCH 53/54] Test fixes --- tests/unit/FormulaTest.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index cc79765d2b5e7..1298f0414b35d 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -177,17 +177,17 @@ describe('CustomFormula', () => { test('should compute startdate formula using transactions', () => { const result = compute('{report:startdate}', mockContext); - expect(result).toBe('01/08/2025'); // Should use oldest transaction date (2025-01-08) + expect(result).toBe('2025-01-08'); // Should use oldest transaction date (2025-01-08) }); test('should compute created formula using report actions', () => { const result = compute('{report:created}', mockContext); - expect(result).toBe('01/10/2025'); // Should use oldest report action date (2025-01-10) + expect(result).toBe('2025-01-10'); // Should use oldest report action date (2025-01-10) }); test('should compute startdate with custom format', () => { - const result = compute('{report:startdate:yyyy-MM-dd}', mockContext); - expect(result).toBe('2025-01-08'); // Should use oldest transaction date with yyyy-MM-dd format + const result = compute('{report:startdate:MM/dd/yyyy}', mockContext); + expect(result).toBe('01/08/2025'); // Should use oldest transaction date with yyyy-MM-dd format }); test('should compute created with custom format', () => { @@ -266,11 +266,8 @@ describe('CustomFormula', () => { report: {reportID: '123'} as Report, policy: null as unknown as Policy, }; - const expected = new Date().toLocaleDateString('en-US', { - month: '2-digit', - day: '2-digit', - year: 'numeric', - }); + const today = new Date(); + const expected = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; const result = compute('{report:startdate}', context); expect(result).toBe(expected); }); @@ -324,7 +321,7 @@ describe('CustomFormula', () => { }; const result = compute('{report:startdate}', context); - expect(result).toBe('01/12/2025'); + expect(result).toBe('2025-01-12'); }); test('should skip partial transactions (zero amount)', () => { @@ -357,7 +354,7 @@ describe('CustomFormula', () => { }; const result = compute('{report:startdate}', context); - expect(result).toBe('01/12/2025'); + expect(result).toBe('2025-01-12'); }); }); }); From ad97611b847b6de3f1dd3a5398b51861b504044f Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Tue, 12 Aug 2025 10:00:17 +0200 Subject: [PATCH 54/54] PR comment fixes --- src/libs/OptimisticReportNames.ts | 10 ++++---- tests/unit/OptimisticReportNamesTest.ts | 31 ++++++++++++++++++++----- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/libs/OptimisticReportNames.ts b/src/libs/OptimisticReportNames.ts index 5899deaeff225..8f4e48f984168 100644 --- a/src/libs/OptimisticReportNames.ts +++ b/src/libs/OptimisticReportNames.ts @@ -243,12 +243,6 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: Performance.markStart(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); Timing.start(CONST.TIMING.UPDATE_OPTIMISTIC_REPORT_NAMES); - Log.info('[OptimisticReportNames] Processing optimistic updates for report names', false, { - updatesCount: updates.length, - hasReports: Object.keys(context.allReports).length > 0, - hasPolicies: Object.keys(context.allPolicies).length > 0, - }); - const {betas, allReports} = context; // Check if the feature is enabled @@ -258,6 +252,10 @@ function updateOptimisticReportNamesFromUpdates(updates: OnyxUpdate[], context: return updates; } + Log.info('[OptimisticReportNames] Processing optimistic updates for report names', false, { + updatesCount: updates.length, + }); + const additionalUpdates: OnyxUpdate[] = []; for (const update of updates) { diff --git a/tests/unit/OptimisticReportNamesTest.ts b/tests/unit/OptimisticReportNamesTest.ts index dc000fb2bb9a1..29ccaa2d2155a 100644 --- a/tests/unit/OptimisticReportNamesTest.ts +++ b/tests/unit/OptimisticReportNamesTest.ts @@ -210,13 +210,32 @@ describe('OptimisticReportNames', () => { const result = updateOptimisticReportNamesFromUpdates(updates, contextWithMultipleReports); - expect(result.length).toEqual(4); - result.forEach((update) => { - if (!update.key.startsWith('report_')) { - return; - } + expect(result).toHaveLength(4); - expect((update.value as {reportName?: string})?.reportName).toEqual('Policy: Updated Policy Name'); + // Assert the original policy update + expect(result.at(0)).toEqual({ + key: 'policy_policy1', + onyxMethod: Onyx.METHOD.MERGE, + value: {name: 'Updated Policy Name'}, + }); + + // Assert individual report name updates + expect(result.at(1)).toEqual({ + key: 'report_123', + onyxMethod: Onyx.METHOD.MERGE, + value: {reportName: 'Policy: Updated Policy Name'}, + }); + + expect(result.at(2)).toEqual({ + key: 'report_456', + onyxMethod: Onyx.METHOD.MERGE, + value: {reportName: 'Policy: Updated Policy Name'}, + }); + + expect(result.at(3)).toEqual({ + key: 'report_789', + onyxMethod: Onyx.METHOD.MERGE, + value: {reportName: 'Policy: Updated Policy Name'}, }); });