Locutus: Remote Code Execution (RCE) in locutus call_user_func_array due to Code Injection
Description
Locutus brings stdlibs of other programming languages to JavaScript for educational purposes. Prior to version 3.0.0, a remote code execution (RCE) flaw was discovered in the locutus project, specifically within the call_user_func_array function implementation. The vulnerability allows an attacker to inject arbitrary JavaScript code into the application's runtime environment. This issue stems from an insecure implementation of the call_user_func_array function (and its wrapper call_user_func), which fails to properly validate all components of a callback array before passing them to eval(). This issue has been patched in version 3.0.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
locutusnpm | < 3.0.0 | 3.0.0 |
Affected products
1Patches
1977a1fb16944feat: Complete TypeScript migration of all source files (#535)
300 files changed · +12050 −4235
biome.json+5 −0 modified@@ -9,11 +9,13 @@ "!**/*.html", "!index.d.ts", "!test/generated", + "!test/util/type-contracts.generated.d.ts", "!website", "!dist", "!node_modules", "!.yarn", "!src/php/_helpers/_bc.js", + "!src/php/_helpers/_bc.ts", "!src/php/bc" ] }, @@ -69,6 +71,7 @@ }, "security": { "noGlobalEval": "error" }, "style": { + "noNonNullAssertion": "error", "noYodaExpression": "error", "useArrayLiterals": "error", "useBlockStatements": "error", @@ -90,6 +93,7 @@ "noDuplicateObjectKeys": "error", "noDuplicateParameters": "error", "noEmptyBlockStatements": "error", + "noExplicitAny": "error", "noFallthroughSwitchClause": "error", "noFunctionAssign": "error", "noGlobalAssign": "error", @@ -103,6 +107,7 @@ "noShadowRestrictedNames": "error", "noSparseArray": "error", "noTemplateCurlyInString": "error", + "noTsIgnore": "error", "noUnsafeNegation": "error", "noUselessRegexBackrefs": "error", "noVar": "error",
.changeset/typescript-migration.md+14 −0 added@@ -0,0 +1,14 @@ +--- +"locutus": major +--- + +TypeScript migration: all source files converted from CJS to TypeScript with named exports. + +Breaking changes: +- Functions now use named exports instead of default exports + - ESM: `import { sort } from 'locutus/php/array/sort'` (was: `import sort from ...`) + - CJS: use named access (`const { sort } = require('locutus/php/array/sort')`) +- The `ini_get` dependency is no longer required — functions read ini values inline via optional chaining on `globalThis.$locutus` +- Repository tooling now runs as native ESM (`"type": "module"`), and Node script invocations no longer need typeless/strip-type flags. + - Published package now ships dual runtime outputs: ESM for `import` and CJS for `require(...)` deep imports. +- Minimum supported Node.js version is now `22` (`engines.node: >= 22`).
docs/non-php-api-signatures.snapshot+177 −0 added@@ -0,0 +1,177 @@ +src/_util/headerFormatter.ts :: export function checkAll(srcDir: string): CheckAllResult +src/_util/headerFormatter.ts :: export function checkFile(filepath: string): CheckResult +src/_util/headerFormatter.ts :: export function formatAll(srcDir: string): FormatAllResult +src/_util/headerFormatter.ts :: export function formatFile(filepath: string): FormatFileResult +src/_util/headerFormatter.ts :: export function formatHeaderLine(key: string, value: string, longestKeyLength: number): string +src/_util/headerFormatter.ts :: export function formatHeaders(content: string): FormatResult +src/_util/headerFormatter.ts :: export function parseHeaderLine(line: string): ParsedLine +src/_util/headerSchema.ts :: export function getValidHeaderKeys(): string[] +src/_util/headerSchema.ts :: export function isValidHeaderKey(key: string): boolean +src/_util/headerSchema.ts :: export function validateHeaderKeys(headKeys: unknown, filepath: string): HeaderKeys +src/_util/headerSchema.ts :: export type HeaderKeys = z.infer<typeof HeaderKeysSchema> +src/awk/builtin/cos.ts :: export function cos(x: number): number +src/awk/builtin/exp.ts :: export function exp(x: number): number +src/awk/builtin/int.ts :: export function int(x: number): number +src/awk/builtin/length.ts :: export function length(s: string): number +src/awk/builtin/log.ts :: export function log(x: number): number +src/awk/builtin/sin.ts :: export function sin(x: number): number +src/awk/builtin/sqrt.ts :: export function sqrt(x: number): number +src/awk/builtin/substr.ts :: export function substr(str: string, start: number, len?: number): string +src/awk/builtin/tolower.ts :: export function tolower(s: string): string +src/awk/builtin/toupper.ts :: export function toupper(s: string): string +src/c/ctype/isalnum.ts :: export function isalnum(c: string): boolean +src/c/ctype/isalpha.ts :: export function isalpha(c: string): boolean +src/c/ctype/isdigit.ts :: export function isdigit(c: string): boolean +src/c/ctype/islower.ts :: export function islower(c: string): boolean +src/c/ctype/isspace.ts :: export function isspace(c: string): boolean +src/c/ctype/isupper.ts :: export function isupper(c: string): boolean +src/c/ctype/tolower.ts :: export function tolower(c: string): string +src/c/ctype/toupper.ts :: export function toupper(c: string): string +src/c/math/abs.ts :: export function abs(mixedNumber: number): number +src/c/math/frexp.ts :: export function frexp(arg: number): [number, number] +src/c/stdio/sprintf.ts :: export function sprintf(format: string, ...args: unknown[]): string +src/c/stdlib/atof.ts :: export function atof(str: string): number +src/c/stdlib/atoi.ts :: export function atoi(str: string): number +src/c/string/strcat.ts :: export function strcat(dest: string, src: string): string +src/c/string/strchr.ts :: export function strchr(str: string, c: string): string | null +src/c/string/strcmp.ts :: export function strcmp(str1: string, str2: string): number +src/c/string/strlen.ts :: export function strlen(str: string): number +src/c/string/strstr.ts :: export function strstr(haystack: string, needle: string): string | null +src/clojure/Math/abs.ts :: export function abs(x: number): number +src/clojure/Math/ceil.ts :: export function ceil(x: number): number +src/clojure/Math/floor.ts :: export function floor(x: number): number +src/clojure/string/blank.ts :: export function blank(s: string): boolean +src/clojure/string/lower_case.ts :: export function lower_case(s: string): string +src/clojure/string/reverse.ts :: export function reverse(s: string): string +src/clojure/string/trim.ts :: export function trim(s: string): string +src/clojure/string/upper_case.ts :: export function upper_case(s: string): string +src/elixir/Float/ceil.ts :: export function ceil(x: number): number +src/elixir/Float/floor.ts :: export function floor(x: number): number +src/elixir/Kernel/abs.ts :: export function abs(x: number): number +src/elixir/String/downcase.ts :: export function downcase(string: string): string +src/elixir/String/length.ts :: export function length(string: string): number +src/elixir/String/reverse.ts :: export function reverse(string: string): string +src/elixir/String/upcase.ts :: export function upcase(string: string): string +src/golang/strconv/Atoi.ts :: export function Atoi(s: unknown): [number, Error | null] +src/golang/strconv/FormatBool.ts :: export function FormatBool(b: boolean): string +src/golang/strconv/FormatInt.ts :: export function FormatInt(i: number, base: number): string +src/golang/strconv/Itoa.ts :: export function Itoa(i: number): string +src/golang/strconv/ParseBool.ts :: export function ParseBool(str: string): [boolean, Error | null] +src/golang/strconv/ParseInt.ts :: export function ParseInt(s: string, base: number, _bitSize: number): [number, Error | null] +src/golang/strings/Compare.ts :: export function Compare(a: string, b: string): number +src/golang/strings/Contains.ts :: export function Contains(s: string, substr: string): boolean +src/golang/strings/ContainsAny.ts :: export function ContainsAny(s: string, chars: string): boolean | false +src/golang/strings/Count.ts :: export function Count(s: string, sep: string): number +src/golang/strings/EqualFold.ts :: export function EqualFold(s: string, t: string): boolean +src/golang/strings/Fields.ts :: export function Fields(s: string): string[] +src/golang/strings/HasPrefix.ts :: export function HasPrefix(s: string, prefix: string): boolean +src/golang/strings/HasSuffix.ts :: export function HasSuffix(s: string, suffix: string): boolean +src/golang/strings/Index2.ts :: export function Index(s: string, sep: string): number +src/golang/strings/IndexAny.ts :: export function IndexAny(s: string, chars: string): number +src/golang/strings/Join.ts :: export function Join(elems: unknown[] | unknown, sep: string): string +src/golang/strings/LastIndex.ts :: export function LastIndex(s: string, sep: string): number +src/golang/strings/LastIndexAny.ts :: export function LastIndexAny(s: string, chars: string): number +src/golang/strings/Repeat.ts :: export function Repeat(s: string, count: number): string +src/golang/strings/Replace.ts :: export function Replace(s: string, old: string, newStr: string, n: number): string +src/golang/strings/Split.ts :: export function Split(s: string, sep: string): string[] +src/golang/strings/ToLower.ts :: export function ToLower(s: string): string +src/golang/strings/ToUpper.ts :: export function ToUpper(s: string): string +src/golang/strings/Trim.ts :: export function Trim(s: string, cutset: string): string +src/golang/strings/TrimLeft.ts :: export function TrimLeft(s: string, cutset: string): string +src/golang/strings/TrimPrefix.ts :: export function TrimPrefix(s: string, prefix: string): string +src/golang/strings/TrimRight.ts :: export function TrimRight(s: string, cutset: string): string +src/golang/strings/TrimSpace.ts :: export function TrimSpace(s: string): string +src/golang/strings/TrimSuffix.ts :: export function TrimSuffix(s: string, suffix: string): string +src/julia/Base/abs.ts :: export function abs(x: number): number +src/julia/Base/ceil.ts :: export function ceil(x: number): number +src/julia/Base/floor.ts :: export function floor(x: number): number +src/julia/Base/lowercase.ts :: export function lowercase(s: string): string +src/julia/Base/uppercase.ts :: export function uppercase(s: string): string +src/lua/math/abs.ts :: export function abs(x: number): number +src/lua/math/ceil.ts :: export function ceil(x: number): number +src/lua/math/cos.ts :: export function cos(x: number): number +src/lua/math/floor.ts :: export function floor(x: number): number +src/lua/math/max.ts :: export function max(...args: number[]): number +src/lua/math/min.ts :: export function min(...args: number[]): number +src/lua/math/sin.ts :: export function sin(x: number): number +src/lua/math/sqrt.ts :: export function sqrt(x: number): number +src/lua/string/len.ts :: export function len(s: string): number +src/lua/string/lower.ts :: export function lower(s: string): string +src/lua/string/rep.ts :: export function rep(s: string, n: number): string +src/lua/string/reverse.ts :: export function reverse(s: string): string +src/lua/string/sub.ts :: export function sub(s: string, i: number, j?: number): string +src/lua/string/upper.ts :: export function upper(s: string): string +src/perl/POSIX/ceil.ts :: export function ceil(x: number): number +src/perl/POSIX/floor.ts :: export function floor(x: number): number +src/perl/core/lc.ts :: export function lc(str: string): string +src/perl/core/length.ts :: export function length(str: string | null | undefined): number | undefined +src/perl/core/reverse.ts :: export function reverse(str: string): string +src/perl/core/substr.ts :: export function substr(str: string, offset: number, length?: number): string +src/perl/core/uc.ts :: export function uc(str: string): string +src/python/math/ceil.ts :: export function ceil(x: number): number +src/python/math/exp.ts :: export function exp(x: number): number +src/python/math/fabs.ts :: export function fabs(x: number): number +src/python/math/factorial.ts :: export function factorial(n: number | string): number +src/python/math/floor.ts :: export function floor(x: number): number +src/python/math/gcd.ts :: export function gcd(a: number | string, b: number | string): number +src/python/math/isfinite.ts :: export function isfinite(x: number): boolean +src/python/math/isinf.ts :: export function isinf(x: number): boolean +src/python/math/isnan.ts :: export function isnan(x: number): boolean +src/python/math/log.ts :: export function log(x: number, base: number): number +src/python/math/log10.ts :: export function log10(x: number): number +src/python/math/log2.ts :: export function log2(x: number): number +src/python/math/pow.ts :: export function pow(x: number, y: number): number +src/python/math/sqrt.ts :: export function sqrt(x: number): number +src/python/math/trunc.ts :: export function trunc(x: number): number +src/python/string/ascii_letters.ts :: export function ascii_letters(): 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' +src/python/string/ascii_lowercase.ts :: export function ascii_lowercase(): 'abcdefghijklmnopqrstuvwxyz' +src/python/string/ascii_uppercase.ts :: export function ascii_uppercase(): 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +src/python/string/capwords.ts :: export function capwords(str: string): string +src/python/string/digits.ts :: export function digits(): '0123456789' +src/python/string/hexdigits.ts :: export function hexdigits(): '0123456789abcdefABCDEF' +src/python/string/octdigits.ts :: export function octdigits(): '01234567' +src/python/string/printable.ts :: export function printable(): '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c' +src/python/string/punctuation.ts :: export function punctuation(): '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' +src/python/string/whitespace.ts :: export function whitespace(): ' \t\n\r\x0b\x0c' +src/r/base/abs.ts :: export function abs(x: number): number +src/r/base/ceiling.ts :: export function ceiling(x: number): number +src/r/base/floor.ts :: export function floor(x: number): number +src/r/base/max.ts :: export function max(...args: number[]): number +src/r/base/min.ts :: export function min(...args: number[]): number +src/r/base/nchar.ts :: export function nchar(x: string): number +src/r/base/round.ts :: export function round(x: number, digits?: number): number +src/r/base/sqrt.ts :: export function sqrt(x: number): number +src/r/base/tolower.ts :: export function tolower(x: string): string +src/r/base/toupper.ts :: export function toupper(x: string): string +src/ruby/Array/compact.ts :: export function compact<T>(arr: Array<T | null | undefined> | unknown): T[] +src/ruby/Array/first.ts :: export function first<T>(arr: T[] | unknown, n?: number): T | T[] | undefined +src/ruby/Array/flatten.ts :: export function flatten(arr: unknown[] | unknown, depth?: number): unknown[] +src/ruby/Array/last.ts :: export function last<T>(arr: T[] | unknown, n?: number): T | T[] | undefined +src/ruby/Array/sample.ts :: export function sample(arr: unknown[], n?: number): unknown | unknown[] | undefined +src/ruby/Array/uniq.ts :: export function uniq<T>(arr: T[] | unknown): T[] +src/ruby/Math/acos.ts :: export function acos(arg: number): number +src/ruby/Math/asin.ts :: export function asin(x: number): number +src/ruby/Math/atan.ts :: export function atan(x: number): number +src/ruby/Math/cbrt.ts :: export function cbrt(x: number): number +src/ruby/Math/cos.ts :: export function cos(x: number): number +src/ruby/Math/cosh.ts :: export function cosh(x: number): number +src/ruby/Math/exp.ts :: export function exp(x: number): number +src/ruby/Math/log.ts :: export function log(x: number): number +src/ruby/Math/log10.ts :: export function log10(x: number): number +src/ruby/Math/log2.ts :: export function log2(x: number): number +src/ruby/Math/sin.ts :: export function sin(x: number): number +src/ruby/Math/sinh.ts :: export function sinh(x: number): number +src/ruby/Math/sqrt.ts :: export function sqrt(x: number): number +src/ruby/Math/tan.ts :: export function tan(x: number): number +src/ruby/Math/tanh.ts :: export function tanh(x: number): number +src/ruby/String/capitalize.ts :: export function capitalize(str: string): string +src/ruby/String/chomp.ts :: export function chomp(str: string, separator?: string): string +src/ruby/String/chop.ts :: export function chop(str: string): string +src/ruby/String/downcase.ts :: export function downcase(str: string): string +src/ruby/String/end_with.ts :: export function end_with(str: string, suffix: string): boolean +src/ruby/String/include.ts :: export function include(str: string, other: string): boolean +src/ruby/String/length.ts :: export function length(str: string): number +src/ruby/String/reverse.ts :: export function reverse(str: string): string +src/ruby/String/start_with.ts :: export function start_with(str: string, prefix: string): boolean +src/ruby/String/strip.ts :: export function strip(str: string): string +src/ruby/String/upcase.ts :: export function upcase(str: string): string
docs/php-api-signatures.snapshot+453 −0 added@@ -0,0 +1,453 @@ +src/php/_helpers/_arrayPointers.ts :: export function getArrayLikeLength<T>(target: PhpArrayLike<T>): number +src/php/_helpers/_arrayPointers.ts :: export function getEntryAtCursor<T>(target: PhpArrayLike<T>, cursor: number): [string | number, T] | null +src/php/_helpers/_arrayPointers.ts :: export function getPointerState<T>(target: PhpArrayLike<T>, initialize = true): PointerState | null +src/php/_helpers/_bc.ts :: export function _bc(): ReturnType<typeof createBcLibrary> +src/php/_helpers/_callbackResolver.ts :: export function resolveNumericComparator< TLeft extends CallbackValue = CallbackValue, TRight extends CallbackValue = CallbackValue, >( callback: PhpCallableDescriptor<[TLeft, TRight], NumericLike>, invalidMessage: string, ): (left: TLeft, right: TRight) => number +src/php/_helpers/_callbackResolver.ts :: export function resolvePhpCallable<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = CallbackValue>( callback: PhpCallableDescriptor<TArgs, TResult>, options: CallbackResolverOptions, ): ResolvedCallback<TArgs, TResult> +src/php/_helpers/_ctypePattern.ts :: export function getCtypePattern(key: string): RegExp | undefined +src/php/_helpers/_phpCastString.ts :: export function _phpCastString(value: CastStringValue): string +src/php/_helpers/_phpRuntimeState.ts :: export function ensurePhpRuntimeState(): PhpRuntimeState +src/php/_helpers/_phpRuntimeState.ts :: export function getPhpGlobalCallable<TArgs extends PhpInput[] = PhpInput[], TResult = PhpInput>( key: string, ): PhpCallable<TArgs, TResult> | undefined +src/php/_helpers/_phpRuntimeState.ts :: export function getPhpGlobalEntry(key: string): PhpInput | undefined +src/php/_helpers/_phpRuntimeState.ts :: export function getPhpGlobalEntry<TKey extends keyof PhpGlobalKnownEntryMap>( key: TKey, ): PhpGlobalKnownEntryMap[TKey] | undefined +src/php/_helpers/_phpRuntimeState.ts :: export function getPhpGlobalScope(): PhpAssoc<PhpInput> +src/php/_helpers/_phpRuntimeState.ts :: export function getPhpLocaleGroup(category: string, groupKey: string): PhpAssoc<PhpInput> | undefined +src/php/_helpers/_phpRuntimeState.ts :: export function getPhpObjectEntry(value: PhpInput, key: string): PhpInput | undefined +src/php/_helpers/_phpRuntimeState.ts :: export function getPhpRuntimeBoolean(key: PhpRuntimeBooleanKey, fallback: boolean): boolean +src/php/_helpers/_phpRuntimeState.ts :: export function getPhpRuntimeBoolean(key: string, fallback: boolean): boolean +src/php/_helpers/_phpRuntimeState.ts :: export function getPhpRuntimeEntry(key: string): PhpInput | undefined +src/php/_helpers/_phpRuntimeState.ts :: export function getPhpRuntimeEntry<TKey extends keyof PhpRuntimeKnownEntryMap>( key: TKey, ): PhpRuntimeKnownEntryMap[TKey] | undefined +src/php/_helpers/_phpRuntimeState.ts :: export function getPhpRuntimeNumber(key: PhpRuntimeNumberKey, fallback: number): number +src/php/_helpers/_phpRuntimeState.ts :: export function getPhpRuntimeNumber(key: string, fallback: number): number +src/php/_helpers/_phpRuntimeState.ts :: export function getPhpRuntimeString(key: PhpRuntimeStringKey, fallback: string): string +src/php/_helpers/_phpRuntimeState.ts :: export function getPhpRuntimeString(key: string, fallback: string): string +src/php/_helpers/_phpRuntimeState.ts :: export function setPhpGlobalEntry(key: string, value: PhpInput): void +src/php/_helpers/_phpRuntimeState.ts :: export function setPhpLocaleDefault(localeDefault: string): void +src/php/_helpers/_phpRuntimeState.ts :: export function setPhpObjectEntry(value: PhpInput, key: string, entry: PhpInput): boolean +src/php/_helpers/_phpRuntimeState.ts :: export function setPhpRuntimeEntry(key: string, value: PhpInput): void +src/php/_helpers/_phpRuntimeState.ts :: export function setPhpRuntimeEntry<TKey extends keyof PhpRuntimeKnownEntryMap>( key: TKey, value: PhpRuntimeKnownEntryMap[TKey], ): void +src/php/_helpers/_phpRuntimeState.ts :: export interface PhpRuntimeState { ini: PhpAssoc<IniEntry | undefined> locales: PhpAssoc<LocaleEntry | undefined> localeCategories: LocaleCategoryMap pointers: PhpList<PhpInput> locale_default: string | undefined } +src/php/_helpers/_phpTypes.ts :: export function assertIsNumericLike( value: PhpInput, message = 'Expected numeric-like value', ): asserts value is NumericLike +src/php/_helpers/_phpTypes.ts :: export function assertIsObjectLike( value: PhpInput, message = 'Expected object-like value', ): asserts value is PhpArrayLike<PhpInput> +src/php/_helpers/_phpTypes.ts :: export function assertIsPhpAssocObject( value: PhpInput, message = 'Expected associative object value', ): asserts value is PhpAssoc<PhpInput> +src/php/_helpers/_phpTypes.ts :: export function assertIsPhpCallable<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = PhpInput>( value: PhpInput, message = 'Expected callable value', ): asserts value is PhpCallable<TArgs, TResult> +src/php/_helpers/_phpTypes.ts :: export function assertIsPhpKey(value: PhpInput, message = 'Expected key value'): asserts value is PhpKey +src/php/_helpers/_phpTypes.ts :: export function assertIsPhpList(value: PhpInput, message = 'Expected list value'): asserts value is PhpList<PhpInput> +src/php/_helpers/_phpTypes.ts :: export function isNumericLike(value: PhpInput): value is NumericLike +src/php/_helpers/_phpTypes.ts :: export function isObjectLike(value: PhpInput): value is PhpArrayLike<PhpInput> +src/php/_helpers/_phpTypes.ts :: export function isPhpArrayObject<T = PhpInput>(value: PhpInput): value is PhpAssoc<T> +src/php/_helpers/_phpTypes.ts :: export function isPhpAssocObject<T = PhpInput>(value: PhpInput): value is PhpAssoc<T> +src/php/_helpers/_phpTypes.ts :: export function isPhpCallable<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = PhpInput>( value: PhpInput, ): value is PhpCallable<TArgs, TResult> +src/php/_helpers/_phpTypes.ts :: export function isPhpCallableDescriptor<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = PhpInput>( value: PhpInput, ): value is PhpCallableDescriptor<TArgs, TResult> +src/php/_helpers/_phpTypes.ts :: export function isPhpKey(value: PhpInput): value is PhpKey +src/php/_helpers/_phpTypes.ts :: export function isPhpList<T = PhpInput>(value: PhpInput): value is PhpList<T> +src/php/_helpers/_phpTypes.ts :: export function isPhpNullish(value: PhpInput): value is PhpNullish +src/php/_helpers/_phpTypes.ts :: export function isPhpScalar(value: PhpInput): value is PhpScalar +src/php/_helpers/_phpTypes.ts :: export function toPhpArrayObject<T = PhpInput>(value: PhpInput): PhpAssoc<T> +src/php/_helpers/_phpTypes.ts :: export interface PhpRecursiveAssoc { [key: string]: PhpRecursiveValue } +src/php/_helpers/_phpTypes.ts :: export interface PhpRecursiveList extends Array<PhpRecursiveValue> {} +src/php/_helpers/_phpTypes.ts :: export interface PhpRuntimeAssoc { [key: string]: PhpRuntimeValue } +src/php/_helpers/_phpTypes.ts :: export interface PhpRuntimeList extends Array<PhpRuntimeValue> {} +src/php/_helpers/_phpTypes.ts :: export type NumericLike = number | bigint | string +src/php/_helpers/_phpTypes.ts :: export type PhpArrayLike<T = PhpInput> = PhpList<T> | PhpAssoc<T> +src/php/_helpers/_phpTypes.ts :: export type PhpAssoc<T = PhpInput> = { [key: string]: T } +src/php/_helpers/_phpTypes.ts :: export type PhpCallable<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = PhpInput> = ( ...args: TArgs ) => TResult +src/php/_helpers/_phpTypes.ts :: export type PhpCallableArgs = PhpInput[] +src/php/_helpers/_phpTypes.ts :: export type PhpCallableDescriptor<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = PhpInput> = | string | PhpCallable<TArgs, TResult> | PhpCallableTuple<TArgs, TResult> +src/php/_helpers/_phpTypes.ts :: export type PhpCallableScope = PhpInput +src/php/_helpers/_phpTypes.ts :: export type PhpCallableTuple<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = PhpInput> = readonly [ PhpCallableScope, string | PhpCallable<TArgs, TResult>, ] +src/php/_helpers/_phpTypes.ts :: export type PhpComparatorDescriptor<T> = PhpCallableDescriptor<[T, T], NumericLike> +src/php/_helpers/_phpTypes.ts :: export type PhpContainer<T = PhpInput> = PhpList<T> | PhpAssoc<T> +src/php/_helpers/_phpTypes.ts :: export type PhpFunctionValue = (...args: PhpInput[]) => PhpInput +src/php/_helpers/_phpTypes.ts :: export type PhpInput = {} | PhpNullish +src/php/_helpers/_phpTypes.ts :: export type PhpKey = string | number +src/php/_helpers/_phpTypes.ts :: export type PhpKeyComparatorDescriptor = PhpCallableDescriptor<[string, string], NumericLike> +src/php/_helpers/_phpTypes.ts :: export type PhpList<T = PhpInput> = T[] +src/php/_helpers/_phpTypes.ts :: export type PhpLiteral = PhpScalar | null +src/php/_helpers/_phpTypes.ts :: export type PhpNullish = null | undefined +src/php/_helpers/_phpTypes.ts :: export type PhpPrimitive = PhpScalar | bigint +src/php/_helpers/_phpTypes.ts :: export type PhpReadonlyArrayLike<T = PhpInput> = PhpReadonlyList<T> | PhpReadonlyAssoc<T> +src/php/_helpers/_phpTypes.ts :: export type PhpReadonlyAssoc<T = PhpInput> = Readonly<PhpAssoc<T>> +src/php/_helpers/_phpTypes.ts :: export type PhpReadonlyList<T = PhpInput> = readonly T[] +src/php/_helpers/_phpTypes.ts :: export type PhpRecursiveValue = PhpPrimitive | PhpNullish | PhpRecursiveList | PhpRecursiveAssoc +src/php/_helpers/_phpTypes.ts :: export type PhpRuntimeValue = PhpPrimitive | PhpNullish | PhpRuntimeList | PhpRuntimeAssoc | PhpFunctionValue +src/php/_helpers/_phpTypes.ts :: export type PhpScalar = string | number | boolean +src/php/_helpers/_phpTypes.ts :: export type PhpStringish = StringLike | PhpNullish +src/php/_helpers/_phpTypes.ts :: export type StringLike = string | number | boolean | bigint +src/php/_helpers/_php_cast_float.ts :: export function _php_cast_float(value: CastFloatValue): number +src/php/_helpers/_php_cast_int.ts :: export function _php_cast_int(value: CastIntValue): number +src/php/array/array_change_key_case.ts :: export function array_change_key_case(array: number | null, cs?: ChangeKeyCaseMode): false +src/php/array/array_change_key_case.ts :: export function array_change_key_case<TValue extends ChangeValue>( array: ArrayChangeInput<TValue>, cs?: ChangeKeyCaseMode, ): PhpArrayLike<TValue> | false +src/php/array/array_change_key_case.ts :: export function array_change_key_case<TValue extends ChangeValue>( array: PhpAssoc<TValue>, cs?: ChangeKeyCaseMode, ): PhpAssoc<TValue> +src/php/array/array_change_key_case.ts :: export function array_change_key_case<TValue extends ChangeValue>(array: TValue[], cs?: ChangeKeyCaseMode): TValue[] +src/php/array/array_chunk.ts :: export function array_chunk<T>( input: ArrayChunkInput<T>, size: number, preserveKeys?: boolean, ): ArrayChunkOutput<T>[] | null +src/php/array/array_column.ts :: export function array_column<TValue>( input: ColumnInput<TValue> | null | undefined, columnKey: string | number | null, indexKey?: string | number | null, ): ColumnOutput<TValue> | undefined +src/php/array/array_column.ts :: export function array_column<TValue>( input: ColumnInput<TValue>, columnKey: string | number | null, indexKey?: string | number | null, ): ColumnOutput<TValue> +src/php/array/array_combine.ts :: export function array_combine<TKey extends string | number, TValue>( keys: TKey[], values: TValue[], ): PhpAssoc<TValue> | false +src/php/array/array_count_values.ts :: export function array_count_values(array: PhpArrayLike<CountValue>): PhpAssoc<number> +src/php/array/array_diff.ts :: export function array_diff<TValue>( arr1: PhpArrayLike<TValue>, ...arrays: Array<PhpArrayLike<TValue>> ): PhpAssoc<TValue> +src/php/array/array_diff_assoc.ts :: export function array_diff_assoc<TValue>( arr1: PhpArrayLike<TValue>, ...arrays: Array<PhpArrayLike<TValue>> ): PhpAssoc<TValue> +src/php/array/array_diff_key.ts :: export function array_diff_key<TValue>( arr1: PhpArrayLike<TValue>, ...arrays: Array<PhpArrayLike<TValue>> ): PhpAssoc<TValue> +src/php/array/array_diff_uassoc.ts :: export function array_diff_uassoc<T>( arr1: PhpAssoc<T>, ...arraysAndCallback: [arr2: DiffArray<T>, ...rest: Array<DiffArray<T>>, callback: PhpKeyComparatorDescriptor] ): PhpAssoc<T> +src/php/array/array_diff_ukey.ts :: export function array_diff_ukey<T>( arr1: PhpAssoc<T>, ...arraysAndCallback: [arr2: DiffArray<T>, ...rest: Array<DiffArray<T>>, callback: PhpKeyComparatorDescriptor] ): PhpAssoc<T> +src/php/array/array_fill.ts :: export function array_fill<TValue>(startIndex: number, num: number, mixedVal: TValue): PhpAssoc<TValue> +src/php/array/array_fill_keys.ts :: export function array_fill_keys<TKey extends PhpKey | StringLike, TValue>( keys: PhpArrayLike<TKey>, value: TValue, ): PhpAssoc<TValue> +src/php/array/array_filter.ts :: export function array_filter<T>(arr: PhpAssoc<T> | PhpList<T>, func?: FilterCallback<T>): PhpAssoc<T> | PhpList<T> +src/php/array/array_filter.ts :: export function array_filter<T>(arr: PhpAssoc<T>, func?: AssocFilterCallback<T>): PhpAssoc<T> +src/php/array/array_filter.ts :: export function array_filter<T>(arr: PhpList<T>, func?: ListFilterCallback<T>): PhpList<T> +src/php/array/array_flip.ts :: export function array_flip<TValue extends PhpKey | StringLike>(trans: PhpArrayLike<TValue>): PhpAssoc<string> +src/php/array/array_intersect.ts :: export function array_intersect<TValue>( arr1: PhpArrayLike<TValue>, ...arrays: Array<PhpArrayLike<TValue>> ): PhpAssoc<TValue> +src/php/array/array_intersect_assoc.ts :: export function array_intersect_assoc<TValue>( arr1: PhpArrayLike<TValue>, ...arrays: Array<PhpArrayLike<TValue>> ): PhpAssoc<TValue> +src/php/array/array_intersect_key.ts :: export function array_intersect_key<TValue>( arr1: PhpArrayLike<TValue>, ...arrays: Array<PhpArrayLike<TValue>> ): PhpAssoc<TValue> +src/php/array/array_intersect_uassoc.ts :: export function array_intersect_uassoc<T>( arr1: PhpAssoc<T>, ...arraysAndCallback: [ arr2: IntersectArray<T>, ...rest: Array<IntersectArray<T>>, callback: PhpKeyComparatorDescriptor, ] ): PhpAssoc<T> +src/php/array/array_intersect_ukey.ts :: export function array_intersect_ukey<T>( arr1: PhpAssoc<T>, ...arraysAndCallback: [ arr2: IntersectArray<T>, ...rest: Array<IntersectArray<T>>, callback: PhpKeyComparatorDescriptor, ] ): PhpAssoc<T> +src/php/array/array_key_exists.ts :: export function array_key_exists<TValue>(key: string | number, search: PhpArrayLike<TValue>): boolean +src/php/array/array_keys.ts :: export function array_keys<T>(input: PhpAssoc<T>, searchValue?: T, argStrict?: boolean): string[] +src/php/array/array_map.ts :: export function array_map<TInputs extends ArrayMapInputs, TResult>( callback: PhpCallableDescriptor<ArrayMapTupleArgs<TInputs>, TResult>, ...inputArrays: TInputs ): PhpList<TResult> +src/php/array/array_map.ts :: export function array_map<TInputs extends ArrayMapInputs>( callback: null | undefined, ...inputArrays: TInputs ): PhpList<ArrayMapTupleArgs<TInputs>> +src/php/array/array_map.ts :: export function array_map<TResult = ArrayMapValue>( callback: PhpCallableDescriptor<ArrayMapCallbackArgsVariadic, TResult> | null | undefined, ...inputArrays: ArrayMapInputs ): PhpList<TResult> | PhpList<ArrayMapCallbackArgsVariadic> +src/php/array/array_merge.ts :: export function array_merge<T>( ...args: [first: PhpArrayLike<T>, ...rest: Array<PhpArrayLike<T>>] ): PhpList<T> | AssociativeArray<T> +src/php/array/array_merge.ts :: export function array_merge<T>(...args: [first: PhpArrayLike<T>, ...rest: Array<PhpArrayLike<T>>]): AssociativeArray<T> +src/php/array/array_merge.ts :: export function array_merge<T>(...args: [first: PhpList<T>, ...rest: Array<PhpList<T>>]): PhpList<T> +src/php/array/array_merge_recursive.ts :: export function array_merge_recursive(arr1: MergeObject, arr2: MergeObject): MergeObject +src/php/array/array_multisort.ts :: export function array_multisort(arr: SortValue[] | SortableObject, ...rest: SortArg[]): boolean +src/php/array/array_pad.ts :: export function array_pad<TInput extends PadValue, TPad extends PadValue>( input: TInput[] | NonArrayPadInput, padSize: number, padValue: TPad, ): Array<TInput | TPad> +src/php/array/array_pad.ts :: export function array_pad<TInput extends PadValue, TPad extends PadValue>( input: TInput[], padSize: number, padValue: TPad, ): Array<TInput | TPad> +src/php/array/array_pad.ts :: export function array_pad<TPad extends PadValue>(input: NonArrayPadInput, padSize: number, padValue: TPad): TPad[] +src/php/array/array_pop.ts :: export function array_pop<T>(inputArr: Record<string, T> | T[]): T | null +src/php/array/array_product.ts :: export function array_product(input: PhpList<ProductValue> | ProductInput): number | null +src/php/array/array_product.ts :: export function array_product(input: PhpList<ProductValue>): number +src/php/array/array_product.ts :: export function array_product(input: ProductInput): number | null +src/php/array/array_push.ts :: export function array_push<TValue>(inputArr: PhpArrayLike<TValue>, ...values: TValue[]): number +src/php/array/array_rand.ts :: export function array_rand<TValue extends RandomValue>( array: PhpArrayLike<TValue>, num: null | undefined, ): string | null +src/php/array/array_rand.ts :: export function array_rand<TValue extends RandomValue>( array: PhpArrayLike<TValue>, num: number, ): string | string[] | null +src/php/array/array_rand.ts :: export function array_rand<TValue extends RandomValue>( array: PhpArrayLike<TValue>, num?: number | null, ): string | string[] | null +src/php/array/array_rand.ts :: export function array_rand<TValue extends RandomValue>(array: PhpArrayLike<TValue>): string | null +src/php/array/array_rand.ts :: export function array_rand<TValue extends RandomValue>(array: PhpArrayLike<TValue>, num: 1): string | null +src/php/array/array_reduce.ts :: export function array_reduce( aInput: readonly PhpRuntimeValue[], callback: (carry: PhpRuntimeValue | null, value: PhpRuntimeValue) => PhpRuntimeValue, initial?: PhpRuntimeValue, ): PhpRuntimeValue | null +src/php/array/array_reduce.ts :: export function array_reduce<TValue, TCarry>( aInput: readonly TValue[], callback: (carry: TCarry, value: TValue) => TCarry, initial: TCarry, ): TCarry +src/php/array/array_reduce.ts :: export function array_reduce<TValue>( aInput: readonly TValue[], callback: (carry: TValue | null, value: TValue) => TValue, ): TValue | null +src/php/array/array_replace.ts :: export function array_replace<TValue>( arr: PhpArrayLike<TValue>, firstReplacement: PhpArrayLike<TValue> | null | undefined, ...additionalReplacements: Array<PhpArrayLike<TValue> | null | undefined> ): PhpAssoc<TValue | undefined> +src/php/array/array_replace_recursive.ts :: export function array_replace_recursive( arr: RecursiveReplaceTarget, ...replacements: [replacement: RecursiveReplaceTarget, ...additionalReplacements: RecursiveReplaceTarget[]] ): RecursiveReplaceTarget +src/php/array/array_reverse.ts :: export function array_reverse( array: ReverseValue[] | PhpAssoc<ReverseValue>, preserveKeys?: boolean, ): ReverseValue[] | PhpAssoc<ReverseValue> +src/php/array/array_reverse.ts :: export function array_reverse<TValue extends ReverseValue>( array: PhpAssoc<TValue>, preserveKeys: true, ): PhpAssoc<TValue> +src/php/array/array_reverse.ts :: export function array_reverse<TValue extends ReverseValue>( array: PhpAssoc<TValue>, preserveKeys?: false | undefined, ): PhpAssoc<TValue> | TValue[] +src/php/array/array_reverse.ts :: export function array_reverse<TValue extends ReverseValue>(array: TValue[], preserveKeys: true): PhpAssoc<TValue> +src/php/array/array_reverse.ts :: export function array_reverse<TValue extends ReverseValue>(array: TValue[], preserveKeys?: false | undefined): TValue[] +src/php/array/array_search.ts :: export function array_search( needle: SearchValue | RegExp, haystack: PhpAssoc<SearchValue>, argStrict?: boolean, ): string | false +src/php/array/array_search.ts :: export function array_search<TValue extends SearchValue>( needle: TValue | RegExp, haystack: PhpAssoc<TValue>, argStrict: true, ): string | false +src/php/array/array_shift.ts :: export function array_shift<T>(inputArr: T[]): T | null +src/php/array/array_slice.ts :: export function array_slice<TValue extends SliceValue>( arr: PhpAssoc<TValue>, offst: number, lgth?: number, preserveKeys?: boolean, ): PhpAssoc<TValue | undefined> +src/php/array/array_slice.ts :: export function array_slice<TValue extends SliceValue>( arr: TValue[] | PhpAssoc<TValue>, offst: number, lgth?: number, preserveKeys?: boolean, ): TValue[] | PhpAssoc<TValue | undefined> +src/php/array/array_slice.ts :: export function array_slice<TValue extends SliceValue>( arr: TValue[], offst: number, lgth: number | undefined, preserveKeys: true, ): TValue[] | PhpAssoc<TValue | undefined> +src/php/array/array_slice.ts :: export function array_slice<TValue extends SliceValue>( arr: TValue[], offst: number, lgth?: number, preserveKeys?: false | undefined, ): TValue[] +src/php/array/array_splice.ts :: export function array_splice<T extends SpliceValue>( arr: Array<T | undefined> | AssocArray<T>, offst: number, lgth?: number, replacement?: ReplacementValue<T>, ): Array<T | undefined> | AssocArray<T> +src/php/array/array_splice.ts :: export function array_splice<T extends SpliceValue>( arr: Array<T | undefined>, offst: number, lgth?: number, replacement?: ReplacementValue<T>, ): Array<T | undefined> +src/php/array/array_splice.ts :: export function array_splice<T extends SpliceValue>( arr: AssocArray<T>, offst: number, lgth?: number, replacement?: ReplacementValue<T>, ): Array<T | undefined> | AssocArray<T> +src/php/array/array_sum.ts :: export function array_sum(array: PhpArrayLike<SummableValue> | null): number | null +src/php/array/array_udiff.ts :: export function array_udiff<T>( arr1: PhpAssoc<T>, ...arraysAndCallback: [arr2: DiffArray<T>, ...rest: Array<DiffArray<T>>, callback: PhpComparatorDescriptor<T>] ): PhpAssoc<T> +src/php/array/array_udiff_assoc.ts :: export function array_udiff_assoc<T>( arr1: PhpAssoc<T>, ...arraysAndCallback: [arr2: DiffArray<T>, ...rest: Array<DiffArray<T>>, callback: PhpComparatorDescriptor<T>] ): PhpAssoc<T> +src/php/array/array_udiff_uassoc.ts :: export function array_udiff_uassoc<T>( arr1: PhpAssoc<T>, ...arraysAndComparators: [ arr2: DiffArray<T>, ...rest: Array<DiffArray<T>>, valueCallback: PhpComparatorDescriptor<T>, keyCallback: PhpKeyComparatorDescriptor, ] ): PhpAssoc<T> +src/php/array/array_uintersect.ts :: export function array_uintersect<T>( arr1: PhpAssoc<T>, ...arraysAndCallback: [ arr2: IntersectArray<T>, ...rest: Array<IntersectArray<T>>, callback: PhpComparatorDescriptor<T>, ] ): PhpAssoc<T> +src/php/array/array_uintersect_uassoc.ts :: export function array_uintersect_uassoc<T>( arr1: PhpAssoc<T>, ...arraysAndComparators: [ arr2: IntersectArray<T>, ...rest: Array<IntersectArray<T>>, valueCallback: PhpComparatorDescriptor<T>, keyCallback: PhpKeyComparatorDescriptor, ] ): PhpAssoc<T> +src/php/array/array_unique.ts :: export function array_unique<T>(inputArr: T[] | PhpAssoc<T>): PhpAssoc<T> | false +src/php/array/array_unshift.ts :: export function array_unshift<TValue extends UnshiftValue>(array: TValue[], ...values: TValue[]): number +src/php/array/array_values.ts :: export function array_values<T>(input: T[] | PhpAssoc<T>): T[] +src/php/array/array_walk.ts :: export function array_walk< TValue extends ArrayWalkInput = ArrayWalkInput, TUserdata extends ArrayWalkInput = ArrayWalkInput, >( array: TValue[] | PhpAssoc<TValue>, funcname: ArrayWalkCallback<number, TValue, TUserdata> | ArrayWalkCallback<string, TValue, TUserdata>, userdata?: TUserdata, ): boolean +src/php/array/array_walk.ts :: export function array_walk< TValue extends ArrayWalkInput = ArrayWalkInput, TUserdata extends ArrayWalkInput = ArrayWalkInput, >(array: PhpAssoc<TValue>, funcname: ArrayWalkCallback<string, TValue, TUserdata>, userdata?: TUserdata): boolean +src/php/array/array_walk.ts :: export function array_walk< TValue extends ArrayWalkInput = ArrayWalkInput, TUserdata extends ArrayWalkInput = ArrayWalkInput, >(array: TValue[], funcname: ArrayWalkCallback<number, TValue, TUserdata>, userdata?: TUserdata): boolean +src/php/array/array_walk_recursive.ts :: export function array_walk_recursive< TValue extends RecursiveWalkInput = RecursiveWalkInput, TUserdata extends RecursiveWalkInput = RecursiveWalkInput, >( array: RecursiveWalkAssoc<TValue>, funcname: ArrayWalkRecursiveCallback<string, TValue, TUserdata>, userdata?: TUserdata, ): boolean +src/php/array/array_walk_recursive.ts :: export function array_walk_recursive< TValue extends RecursiveWalkInput = RecursiveWalkInput, TUserdata extends RecursiveWalkInput = RecursiveWalkInput, >( array: RecursiveWalkCollection<TValue>, funcname: | ArrayWalkRecursiveCallback<number, TValue, TUserdata> | ArrayWalkRecursiveCallback<string, TValue, TUserdata>, userdata?: TUserdata, ): boolean +src/php/array/array_walk_recursive.ts :: export function array_walk_recursive< TValue extends RecursiveWalkInput = RecursiveWalkInput, TUserdata extends RecursiveWalkInput = RecursiveWalkInput, >( array: RecursiveWalkList<TValue>, funcname: ArrayWalkRecursiveCallback<number, TValue, TUserdata>, userdata?: TUserdata, ): boolean +src/php/array/arsort.ts :: export function arsort<T extends SortValue>( inputArr: Record<string, T>, sortFlags?: SortFlag, ): boolean | Record<string, T> +src/php/array/asort.ts :: export function asort<T extends SortValue>( inputArr: Record<string, T>, sortFlags?: SortFlag, ): boolean | Record<string, T> +src/php/array/count.ts :: export function count(mixedVar: Countable | null | undefined, mode: CountMode = 0): number +src/php/array/count.ts :: export function count(mixedVar: Countable, mode?: CountMode): number +src/php/array/count.ts :: export function count(mixedVar: null | undefined, mode?: CountMode): 0 +src/php/array/count.ts :: export type CountMode = 0 | 1 | 'COUNT_NORMAL' | 'COUNT_RECURSIVE' +src/php/array/count.ts :: export type Countable = CountableList | CountableAssoc +src/php/array/current.ts :: export function current<T>(arr: PhpArrayLike<T>): T | false +src/php/array/each.ts :: export function each<T>(arr: PhpArrayLike<T>): EachResult<T> +src/php/array/end.ts :: export function end<T>(arr: PhpArrayLike<T>): T | false +src/php/array/in_array.ts :: export function in_array(needle: InArrayValue, haystack: PhpArrayLike<InArrayValue>, argStrict?: boolean): boolean +src/php/array/in_array.ts :: export function in_array<TNeedle extends InArrayValue>( needle: TNeedle, haystack: PhpArrayLike<TNeedle>, argStrict: true, ): boolean +src/php/array/key.ts :: export function key<T>(arr: PhpArrayLike<T>): string | number | false +src/php/array/krsort.ts :: export function krsort<T>(inputArr: Record<string, T>, sortFlags?: SortFlag): boolean | Record<string, T> +src/php/array/ksort.ts :: export function ksort<T>(inputArr: Record<string, T>, sortFlags?: SortFlag): boolean | Record<string, T> +src/php/array/natcasesort.ts :: export function natcasesort<T extends string | number>(inputArr: Record<string, T>): boolean | Record<string, T> +src/php/array/natsort.ts :: export function natsort<T extends string | number>(inputArr: Record<string, T>): boolean | Record<string, T> +src/php/array/next.ts :: export function next<T>(arr: PhpArrayLike<T>): T | false +src/php/array/pos.ts :: export function pos<T>(arr: T[] | Record<string, T>): T | false +src/php/array/prev.ts :: export function prev<T>(arr: PhpArrayLike<T>): T | false +src/php/array/range.ts :: export function range(low: string | number, high: string | number, step?: number): Array<string | number> +src/php/array/reset.ts :: export function reset<T>(arr: PhpArrayLike<T>): T | false +src/php/array/rsort.ts :: export function rsort<T extends SortValue>( inputArr: Record<string, T>, sortFlags?: SortFlag, ): boolean | Record<string, T> +src/php/array/shuffle.ts :: export function shuffle<T>(inputArr: Record<string, T>): boolean | Record<string, T> | T[] +src/php/array/sizeof.ts :: export function sizeof(mixedVar: Countable, mode?: CountMode): number +src/php/array/sort.ts :: export function sort<T extends SortValue>( inputArr: Record<string, T>, sortFlags?: SortFlag, ): boolean | Record<string, T> +src/php/array/uasort.ts :: export function uasort<T>( this: PhpAssoc<SortContextValue>, inputArr: Record<string, T>, sorter: PhpComparatorDescriptor<T>, ): boolean | Record<string, T> +src/php/array/uksort.ts :: export function uksort<T>( this: PhpAssoc<SortContextValue> & { window?: PhpAssoc<SortContextValue> }, inputArr: Record<string, T>, sorter: PhpKeyComparatorDescriptor, ): boolean | Record<string, T> +src/php/array/usort.ts :: export function usort<T>( this: PhpAssoc<SortContextValue>, inputArr: Record<string, T>, sorter: PhpComparatorDescriptor<T>, ): boolean | Record<string, T> +src/php/bc/bcadd.ts :: export function bcadd(leftOperand: string, rightOperand: string, scale?: number): string +src/php/bc/bccomp.ts :: export function bccomp(leftOperand: string | number, rightOperand: string | number, scale?: number): number +src/php/bc/bcdiv.ts :: export function bcdiv(leftOperand: string | number, rightOperand: string | number, scale?: number): string +src/php/bc/bcmul.ts :: export function bcmul(leftOperand: string, rightOperand: string, scale?: number): string +src/php/bc/bcround.ts :: export function bcround(val: number, precision: number): string +src/php/bc/bcscale.ts :: export function bcscale(scale: number | string): boolean +src/php/bc/bcsub.ts :: export function bcsub(leftOperand: string, rightOperand: string, scale?: number): string +src/php/ctype/ctype_alnum.ts :: export function ctype_alnum(text: string): boolean +src/php/ctype/ctype_alpha.ts :: export function ctype_alpha(text: string): boolean +src/php/ctype/ctype_cntrl.ts :: export function ctype_cntrl(text: string): boolean +src/php/ctype/ctype_digit.ts :: export function ctype_digit(text: string): boolean +src/php/ctype/ctype_graph.ts :: export function ctype_graph(text: string): boolean +src/php/ctype/ctype_lower.ts :: export function ctype_lower(text: string): boolean +src/php/ctype/ctype_print.ts :: export function ctype_print(text: string): boolean +src/php/ctype/ctype_punct.ts :: export function ctype_punct(text: string): boolean +src/php/ctype/ctype_space.ts :: export function ctype_space(text: string): boolean +src/php/ctype/ctype_upper.ts :: export function ctype_upper(text: string): boolean +src/php/ctype/ctype_xdigit.ts :: export function ctype_xdigit(text: string): boolean +src/php/datetime/checkdate.ts :: export function checkdate(m: number, d: number, y: number): boolean +src/php/datetime/date.ts :: export function date(format: string, timestamp?: number | Date | string): string +src/php/datetime/date_parse.ts :: export function date_parse(date: string): DateParseResult | false +src/php/datetime/getdate.ts :: export function getdate(timestamp?: number | string | Date): GetDateResult +src/php/datetime/gettimeofday.ts :: export function gettimeofday(returnFloat?: boolean): number | GetTimeOfDayObject +src/php/datetime/gmdate.ts :: export function gmdate(format: string, timestamp?: string | number | Date): string +src/php/datetime/gmmktime.ts :: export function gmmktime( hour?: number | string, minute?: number | string, second?: number | string, month?: number | string, day?: number | string, year?: number | string, ): number | false +src/php/datetime/gmstrftime.ts :: export function gmstrftime(format: string, timestamp?: number | Date): string +src/php/datetime/idate.ts :: export function idate(format?: string, timestamp?: number | string | Date): number +src/php/datetime/microtime.ts :: export function microtime(getAsFloat?: boolean): string | number +src/php/datetime/mktime.ts :: export function mktime( hour?: number | string, minute?: number | string, second?: number | string, month?: number | string, day?: number | string, year?: number | string, ): number | false +src/php/datetime/strftime.ts :: export function strftime(fmt: string, timestamp?: Date | number | string): string +src/php/datetime/strptime.ts :: export function strptime(dateStr: string, format: string): StrptimeResult | false +src/php/datetime/strtotime.ts :: export function strtotime(str: string, now?: number): false | number +src/php/datetime/time.ts :: export function time(): number +src/php/exec/escapeshellarg.ts :: export function escapeshellarg(arg: string): string +src/php/filesystem/basename.ts :: export function basename(path: string, suffix?: string): string +src/php/filesystem/dirname.ts :: export function dirname(path: string): string +src/php/filesystem/file_exists.ts :: export function file_exists(filename: string): boolean +src/php/filesystem/file_get_contents.ts :: export function file_get_contents( url: string, _flags?: FileGetContentsInput, _context?: FileGetContentsInput, _offset?: number, _maxLen?: number, ): string | false +src/php/filesystem/pathinfo.ts :: export function pathinfo(path: string, options?: PathInfoOptions): PathInfoMap | string | false +src/php/filesystem/realpath.ts :: export function realpath(path: string): string +src/php/funchand/call_user_func.ts :: export function call_user_func<TResult = FunctionArg, TArgs extends PhpCallableArgs = PhpCallableArgs>( cb: PhpCallableDescriptor<TArgs, TResult>, ...parameters: TArgs ): TResult +src/php/funchand/call_user_func_array.ts :: export function call_user_func_array<TResult = FunctionValue, TArgs extends PhpCallableArgs = PhpCallableArgs>( cb: PhpCallableDescriptor<TArgs, TResult>, parameters: [...TArgs], ): TResult +src/php/funchand/create_function.ts :: export function create_function(args: string, code: string): PhpCallable | false +src/php/funchand/function_exists.ts :: export function function_exists(funcName: string): boolean +src/php/funchand/get_defined_functions.ts :: export function get_defined_functions(): string[] +src/php/i18n/i18n_loc_get_default.ts :: export function i18n_loc_get_default(): string +src/php/i18n/i18n_loc_set_default.ts :: export function i18n_loc_set_default(name: string): boolean +src/php/info/assert_options.ts :: export function assert_options(what: string, _value?: AssertOptionValue): string | number | null +src/php/info/getenv.ts :: export function getenv(varname: string): string | false +src/php/info/ini_get.ts :: export function ini_get(varname: string): string +src/php/info/ini_set.ts :: export function ini_set(varname: string, newvalue: IniValue): IniValue | undefined +src/php/info/ini_set.ts :: export type IniValue = IniEntryValue | undefined +src/php/info/set_time_limit.ts :: export function set_time_limit(seconds: number): void +src/php/info/version_compare.ts :: export function version_compare(v1: string, v2: string, operator?: string): number | boolean | null +src/php/json/json_decode.ts :: export function json_decode<T = JsonValue>(strJson: string): T | null +src/php/json/json_encode.ts :: export function json_encode(mixedVal: JsonEncodeInput): string | null +src/php/json/json_last_error.ts :: export function json_last_error(): number +src/php/math/abs.ts :: export function abs(mixedNumber: string | number): number +src/php/math/acos.ts :: export function acos(arg: number): number +src/php/math/acosh.ts :: export function acosh(arg: number): number +src/php/math/asin.ts :: export function asin(arg: number): number +src/php/math/asinh.ts :: export function asinh(arg: number): number +src/php/math/atan.ts :: export function atan(arg: number): number +src/php/math/atan2.ts :: export function atan2(y: number, x: number): number +src/php/math/atanh.ts :: export function atanh(arg: number): number +src/php/math/base_convert.ts :: export function base_convert(number: string, frombase: number, tobase: number): string +src/php/math/bindec.ts :: export function bindec(binaryString: string): number +src/php/math/ceil.ts :: export function ceil(value: number): number +src/php/math/cos.ts :: export function cos(arg: number): number +src/php/math/cosh.ts :: export function cosh(arg: number): number +src/php/math/decbin.ts :: export function decbin(number: string | number): string +src/php/math/dechex.ts :: export function dechex(number: number): string +src/php/math/decoct.ts :: export function decoct(number: number): string +src/php/math/deg2rad.ts :: export function deg2rad(angle: number): number +src/php/math/exp.ts :: export function exp(arg: number): number +src/php/math/expm1.ts :: export function expm1(x: number): number +src/php/math/floor.ts :: export function floor(value: number): number +src/php/math/fmod.ts :: export function fmod(x: number | string, y: number | string): number +src/php/math/getrandmax.ts :: export function getrandmax(): number +src/php/math/hexdec.ts :: export function hexdec(hexString: string): number +src/php/math/hypot.ts :: export function hypot(x: HypotInput, y: HypotInput): number | null +src/php/math/is_finite.ts :: export function is_finite(val: NumberCheckValue): boolean +src/php/math/is_infinite.ts :: export function is_infinite(val: NumberCheckValue): boolean +src/php/math/is_nan.ts :: export function is_nan(val: NumberCheckValue): boolean +src/php/math/lcg_value.ts :: export function lcg_value(): number +src/php/math/log.ts :: export function log(arg: number, base: number): number +src/php/math/log10.ts :: export function log10(arg: number): number +src/php/math/log1p.ts :: export function log1p(x: number): number | string +src/php/math/max.ts :: export function max(...args: PhpMinMaxValue[]): PhpMinMaxValue +src/php/math/min.ts :: export function min(...args: PhpMinMaxValue[]): PhpMinMaxValue +src/php/math/mt_getrandmax.ts :: export function mt_getrandmax(): number +src/php/math/mt_rand.ts :: export function mt_rand(): number +src/php/math/mt_rand.ts :: export function mt_rand(...providedArgs: [min?: number | string, max?: number | string]): number +src/php/math/mt_rand.ts :: export function mt_rand(min: number | string, max: number | string): number +src/php/math/octdec.ts :: export function octdec(octString: string): number +src/php/math/pi.ts :: export function pi(): number +src/php/math/pow.ts :: export function pow(base: number, exp: number): number +src/php/math/rad2deg.ts :: export function rad2deg(angle: number): number +src/php/math/rand.ts :: export function rand(): number +src/php/math/rand.ts :: export function rand(...providedArgs: [min?: number, max?: number]): number +src/php/math/rand.ts :: export function rand(min: number, max: number): number +src/php/math/round.ts :: export function round(value: number, precision = 0, mode = 'PHP_ROUND_HALF_UP'): number +src/php/math/sin.ts :: export function sin(arg: number): number +src/php/math/sinh.ts :: export function sinh(arg: number): number +src/php/math/sqrt.ts :: export function sqrt(arg: number): number +src/php/math/tan.ts :: export function tan(arg: number): number +src/php/math/tanh.ts :: export function tanh(arg: number): number +src/php/misc/pack.ts :: export function pack(format: string, ...inputArgs: PackArgument[]): string +src/php/misc/uniqid.ts :: export function uniqid(prefix?: string, moreEntropy?: boolean): string +src/php/net-gopher/gopher_parsedir.ts :: export function gopher_parsedir(dirent: string): GopherKnownEntry | GopherUnknownEntry +src/php/network/inet_ntop.ts :: export function inet_ntop(a: string | number): string | false +src/php/network/inet_pton.ts :: export function inet_pton(a: string): string | false +src/php/network/ip2long.ts :: export function ip2long(argIP: string): number | false +src/php/network/long2ip.ts :: export function long2ip(ip: number): string | false +src/php/network/setcookie.ts :: export function setcookie( name: string, value: string, expires?: string | number | Date | null, path?: string | null, domain?: string | null, secure?: boolean, ): boolean +src/php/network/setrawcookie.ts :: export function setrawcookie( name: string, value: string, expires?: CookieExpires, path?: string | null, domain?: string | null, secure?: boolean, ): boolean +src/php/pcre/preg_match.ts :: export function preg_match(regex: string, str: string): boolean +src/php/pcre/preg_quote.ts :: export function preg_quote(str: string, delimiter: string): string +src/php/pcre/preg_replace.ts :: export function preg_replace(pattern: string, replacement: string, string: string): string +src/php/pcre/sql_regcase.ts :: export function sql_regcase(str: string): string +src/php/strings/addcslashes.ts :: export function addcslashes(str: string, charlist: string): string +src/php/strings/addslashes.ts :: export function addslashes(str: string): string +src/php/strings/bin2hex.ts :: export function bin2hex(s: string): string +src/php/strings/chop.ts :: export function chop(str: string, charlist: string): string +src/php/strings/chr.ts :: export function chr(codePt: number): string +src/php/strings/chunk_split.ts :: export function chunk_split(body: string, chunklen?: number | string, end?: string): string | false +src/php/strings/convert_cyr_string.ts :: export function convert_cyr_string(str: string, from: string, to: string): string +src/php/strings/convert_uuencode.ts :: export function convert_uuencode(str: UUEncodeInput): string | false +src/php/strings/count_chars.ts :: export function count_chars( str: string | number | boolean | null | undefined, mode = 0, ): { [key: string]: number } | string +src/php/strings/crc32.ts :: export function crc32(str: string): number +src/php/strings/echo.ts :: export function echo(...args: EchoValue[]): void +src/php/strings/explode.ts :: export function explode( ...args: [string | boolean | null | undefined, string | KeyedValues | (() => ExplodeValue) | undefined, number?] ): string[] | false | { 0: string } | null +src/php/strings/get_html_translation_table.ts :: export function get_html_translation_table( table: string | number = 'HTML_SPECIALCHARS', quoteStyle: string | number = 'ENT_COMPAT', ): Record<string, string> +src/php/strings/hex2bin.ts :: export function hex2bin(s: string | number): string | false +src/php/strings/html_entity_decode.ts :: export function html_entity_decode(string: string, quoteStyle?: string | number): string | false +src/php/strings/htmlentities.ts :: export function htmlentities( string: string, quoteStyle?: string | number, charset?: string, doubleEncode?: boolean | null, ): string +src/php/strings/htmlspecialchars.ts :: export function htmlspecialchars( string: string, quoteStyle?: HtmlSpecialCharsQuoteStyle, charset?: null, doubleEncode?: boolean, ): string +src/php/strings/htmlspecialchars_decode.ts :: export function htmlspecialchars_decode(string: string, quoteStyle?: HtmlSpecialCharsQuoteStyle): string +src/php/strings/implode.ts :: export function implode( ...providedArgs: [ glueOrPieces?: ImplodeValue[] | KeyedValues | string, pieces?: ImplodeValue[] | KeyedValues | string | undefined, ] ): string +src/php/strings/implode.ts :: export function implode(glue: string, pieces: ImplodeValue[] | KeyedValues | string | undefined): string +src/php/strings/implode.ts :: export function implode(pieces: ImplodeValue[] | KeyedValues | string | undefined): string +src/php/strings/join.ts :: export function join(glue: string, pieces: JoinValue[]): string +src/php/strings/lcfirst.ts :: export function lcfirst(str: string): string +src/php/strings/levenshtein.ts :: export function levenshtein(s1: string, s2: string, costIns?: number, costRep?: number, costDel?: number): number +src/php/strings/localeconv.ts :: export function localeconv(): LocaleValues +src/php/strings/ltrim.ts :: export function ltrim(str: string, charlist: string): string +src/php/strings/md5.ts :: export function md5(str: string): string +src/php/strings/md5_file.ts :: export function md5_file(str_filename: string): string | false +src/php/strings/metaphone.ts :: export function metaphone( word: MetaphoneValue, maxPhonemes?: string | number | boolean | null | undefined, ): string | false | null +src/php/strings/money_format.ts :: export function money_format(format: string, number: number): string | null +src/php/strings/nl2br.ts :: export function nl2br(str: string | null, isXhtml?: boolean): string +src/php/strings/nl_langinfo.ts :: export function nl_langinfo(item: string): string | string[] | false +src/php/strings/number_format.ts :: export function number_format( number: string | number, decimals: number | undefined, decPoint: string | undefined, thousandsSep: string | undefined, ): string +src/php/strings/ord.ts :: export function ord(string: string): number +src/php/strings/parse_str.ts :: export function parse_str(str: string, array?: ParseObject): void +src/php/strings/printf.ts :: export function printf(format: string, ...args: PrintfValue[]): number +src/php/strings/quoted_printable_decode.ts :: export function quoted_printable_decode(str: string): string +src/php/strings/quoted_printable_encode.ts :: export function quoted_printable_encode(str: string): string +src/php/strings/quotemeta.ts :: export function quotemeta(str: string): string +src/php/strings/rtrim.ts :: export function rtrim(str: string, charlist: string): string +src/php/strings/setlocale.ts :: export function setlocale(category: string, locale: LocaleInput): string | false +src/php/strings/sha1.ts :: export function sha1(str: string): string +src/php/strings/sha1_file.ts :: export function sha1_file(str_filename: string): string | false +src/php/strings/similar_text.ts :: export function similar_text( first: string | number | boolean | null | undefined, second: string | number | boolean | null | undefined, percent?: boolean | number | null | undefined, ): number +src/php/strings/soundex.ts :: export function soundex(str: SoundexValue): string +src/php/strings/split.ts :: export function split( delimiter: string | boolean | null | undefined, string: string | KeyedValues | (() => SplitValue) | undefined, ): string[] | false | { 0: string } | null +src/php/strings/sprintf.ts :: export function sprintf(format: string, ...args: SprintfValue[]): string | false +src/php/strings/sscanf.ts :: export function sscanf(str: string, format: string, ...refs: SscanfRef[]): SscanfValue[] | number +src/php/strings/str_getcsv.ts :: export function str_getcsv( input: string, delimiter?: string | null, enclosure?: string | null, escapeCharacter?: string | null, ): string[] +src/php/strings/str_ireplace.ts :: export function str_ireplace( search: string | string[], replace: string | string[], subject: string | string[], countObj?: CountObj, ): string | string[] +src/php/strings/str_pad.ts :: export function str_pad(input: string, padLength: number, padString: string, padType: string): string +src/php/strings/str_repeat.ts :: export function str_repeat(input: string, multiplier: number): string +src/php/strings/str_replace.ts :: export function str_replace( search: string | string[], replace: string | string[], subject: string | string[], countObj?: CountObj, ): string | string[] +src/php/strings/str_rot13.ts :: export function str_rot13(str: string | number): string +src/php/strings/str_shuffle.ts :: export function str_shuffle(...providedArgs: [input?: string | null | undefined]): string +src/php/strings/str_shuffle.ts :: export function str_shuffle(input: string | null | undefined): string +src/php/strings/str_split.ts :: export function str_split( string: string | number | boolean | null, splitLength?: number | string | null, ): string[] | false +src/php/strings/str_word_count.ts :: export function str_word_count( str: string, format?: 0 | 1 | 2, charlist?: string, ): number | string[] | { [key: number]: string } +src/php/strings/strcasecmp.ts :: export function strcasecmp(fString1: string, fString2: string): number +src/php/strings/strchr.ts :: export function strchr(haystack: string, needle: string, bool?: boolean): string | false +src/php/strings/strcmp.ts :: export function strcmp(str1: string, str2: string): number +src/php/strings/strcoll.ts :: export function strcoll(str1: string, str2: string): number +src/php/strings/strcspn.ts :: export function strcspn(str: string, mask: string, start?: number, length?: number): number +src/php/strings/strip_tags.ts :: export function strip_tags(input: string | number, allowed?: string): string +src/php/strings/stripos.ts :: export function stripos(fHaystack: string, fNeedle: string, fOffset?: number): number | false +src/php/strings/stripslashes.ts :: export function stripslashes(str: string): string +src/php/strings/stristr.ts :: export function stristr(haystack: string, needle: string, bool?: boolean): string | false +src/php/strings/strlen.ts :: export function strlen(string: string): number +src/php/strings/strnatcasecmp.ts :: export function strnatcasecmp(...providedArgs: [a?: string | number, b?: string | number]): number | null +src/php/strings/strnatcasecmp.ts :: export function strnatcasecmp(a: string | number, b: string | number): number +src/php/strings/strnatcmp.ts :: export function strnatcmp(left: NatCmpValue, right: NatCmpValue): number +src/php/strings/strncasecmp.ts :: export function strncasecmp(argStr1: string, argStr2: string, len: number): number +src/php/strings/strncmp.ts :: export function strncmp(str1: string, str2: string, lgth: number): number +src/php/strings/strpbrk.ts :: export function strpbrk(haystack: string, charList: string): string | false +src/php/strings/strpos.ts :: export function strpos(haystack: string, needle: string, offset: number): number | false +src/php/strings/strrchr.ts :: export function strrchr(haystack: string, needle: string): string | false +src/php/strings/strrev.ts :: export function strrev(string: string): string +src/php/strings/strripos.ts :: export function strripos(haystack: string, needle: string, offset?: number): number | false +src/php/strings/strrpos.ts :: export function strrpos(haystack: string, needle: string, offset?: boolean | number): number | false +src/php/strings/strspn.ts :: export function strspn(str1: string, str2: string, start?: number, lgth?: number): number +src/php/strings/strstr.ts :: export function strstr(haystack: string, needle: string, bool?: boolean): string | false +src/php/strings/strtok.ts :: export function strtok(str: string, tokens?: string): string | false +src/php/strings/strtolower.ts :: export function strtolower(str: string): string +src/php/strings/strtoupper.ts :: export function strtoupper(str: string): string +src/php/strings/strtr.ts :: export function strtr(str: string, trFrom: string | ReplacementMap | string[], trTo?: string | StrtrValue[]): string +src/php/strings/substr.ts :: export function substr(input: string | number, start: number, len?: number): string | false +src/php/strings/substr_compare.ts :: export function substr_compare( mainStr: string, str: string, offset: number, length: number, caseInsensitivity?: boolean, ): number | false +src/php/strings/substr_count.ts :: export function substr_count(haystack: string, needle: string, offset?: number, length?: number): number | false +src/php/strings/substr_replace.ts :: export function substr_replace(str: string, replace: string, start: number, length?: number): string +src/php/strings/trim.ts :: export function trim(str: string | number, charlist?: string | number): string +src/php/strings/ucfirst.ts :: export function ucfirst(str: string): string +src/php/strings/ucwords.ts :: export function ucwords(str: string): string +src/php/strings/vprintf.ts :: export function vprintf(format: string, ...args: PrintfValue[]): number +src/php/strings/vprintf.ts :: export function vprintf(format: string, ...restArgs: [PrintfValue[]] | PrintfValue[]): number +src/php/strings/vprintf.ts :: export function vprintf(format: string, args: PrintfValue[]): number +src/php/strings/vsprintf.ts :: export function vsprintf(format: string, args: VsprintfValue[]): string | false +src/php/strings/wordwrap.ts :: export function wordwrap(...rawArgs: [str: string, intWidth?: number, strBreak?: string, cut?: boolean]): string +src/php/url/base64_decode.ts :: export function base64_decode(encodedData: string | null | undefined): string | null | undefined +src/php/url/base64_encode.ts :: export function base64_encode(stringToEncode: string | null | undefined): string | null | undefined +src/php/url/http_build_query.ts :: export function http_build_query( formdata: QueryObject | QueryValue[], numericPrefix?: string | number, argSeparator?: string, encType?: HttpBuildQueryEncType, ): string +src/php/url/parse_url.ts :: export function parse_url(str: string, component?: string): ParseUrlResult | string +src/php/url/rawurldecode.ts :: export function rawurldecode(str: string): string +src/php/url/rawurlencode.ts :: export function rawurlencode(str: string): string +src/php/url/urldecode.ts :: export function urldecode(str: string): string +src/php/url/urlencode.ts :: export function urlencode(str: string): string +src/php/var/boolval.ts :: export function boolval(mixedVar: BoolValue): boolean +src/php/var/doubleval.ts :: export function doubleval(mixedVar: FloatvalInput): number +src/php/var/empty.ts :: export function empty(mixedVar: EmptyValue): boolean +src/php/var/floatval.ts :: export function floatval(mixedVar: FloatvalInput): number +src/php/var/floatval.ts :: export type FloatvalInput = NumericLike | boolean | PhpNullish +src/php/var/gettype.ts :: export function gettype(mixedVar: TypeInput): string +src/php/var/intval.ts :: export function intval(mixedVar: IntvalInput, base?: number): number +src/php/var/is_array.ts :: export function is_array(mixedVar: IsArrayValue): boolean +src/php/var/is_binary.ts :: export function is_binary(vr: string): boolean +src/php/var/is_bool.ts :: export function is_bool(mixedVar: boolean | number): boolean +src/php/var/is_buffer.ts :: export function is_buffer(vr: string): boolean +src/php/var/is_callable.ts :: export function is_callable(mixedVar: CallableValue, syntaxOnly?: boolean, callableName?: string): boolean +src/php/var/is_double.ts :: export function is_double(mixedVar: number): boolean +src/php/var/is_float.ts :: export function is_float(mixedVar: number): boolean +src/php/var/is_int.ts :: export function is_int(mixedVar: IntValue): mixedVar is number +src/php/var/is_integer.ts :: export function is_integer(mixedVar: number): boolean +src/php/var/is_long.ts :: export function is_long(mixedVar: number): boolean +src/php/var/is_null.ts :: export function is_null(mixedVar: string | null): boolean +src/php/var/is_numeric.ts :: export function is_numeric(mixedVar: NumericValue): mixedVar is number | string +src/php/var/is_object.ts :: export function is_object(mixedVar: ObjectValue): mixedVar is PhpAssoc<ObjectValue> +src/php/var/is_real.ts :: export function is_real(mixedVar: number): boolean +src/php/var/is_scalar.ts :: export function is_scalar(mixedVar: ScalarCheckValue): mixedVar is PhpScalar +src/php/var/is_string.ts :: export function is_string(mixedVar: string | number): boolean +src/php/var/is_unicode.ts :: export function is_unicode(vr: UnicodeValue): vr is string +src/php/var/isset.ts :: export function isset(...values: IssetValue[]): boolean +src/php/var/print_r.ts :: export function print_r(array: PrintValue, returnVal?: boolean): string | true +src/php/var/serialize.ts :: export function serialize(mixedValue: SerializeValue): string +src/php/var/strval.ts :: export function strval(str: StringValue): string +src/php/var/unserialize.ts :: export function unserialize(str: UnserializeInput, errorMode: ErrorMode = 'log'): UnserializedValue | false +src/php/var/var_dump.ts :: export function var_dump(...args: DumpValue[]): string +src/php/var/var_export.ts :: export function var_export( mixedExpression: VarExportInput, boolReturn?: boolean, idtLevel = 2, ): VarExportResult | null +src/php/var/var_export.ts :: export function var_export( mixedExpression: VarExportInput, boolReturn?: boolean, idtLevel?: number, ): VarExportResult | null +src/php/var/var_export.ts :: export function var_export(mixedExpression: VarExportInput, boolReturn: true, idtLevel?: number): VarExportResult +src/php/var/var_export.ts :: export function var_export(mixedExpression: VarExportInput, boolReturn?: false | undefined, idtLevel?: number): null +src/php/xdiff/xdiff_string_diff.ts :: export function xdiff_string_diff( oldData: string, newData: string, contextLines?: number, _minimal?: boolean, ): string | false +src/php/xdiff/xdiff_string_patch.ts :: export function xdiff_string_patch( originalStr: string, patch: string, flags?: number | string | string[], errorObj?: PatchErrorObject, ): string | false +src/php/xml/utf8_decode.ts :: export function utf8_decode(strData: string | number | boolean | null | undefined): string +src/php/xml/utf8_encode.ts :: export function utf8_encode(argString: string | null | undefined): string
docs/typescript.md+2378 −0 added.github/workflows/ci.yml+10 −1 modified@@ -20,7 +20,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: Engine bump guardrail run: | - node scripts/check-engine-bump.js + node scripts/check-engine-bump.ts - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT @@ -45,6 +45,15 @@ jobs: - name: TypeScript Check run: | corepack yarn lint:ts + - name: TypeScript Strict-Next Check + run: | + corepack yarn lint:ts:strict-next + - name: Type Debt Policy Check + run: | + corepack yarn lint:ts:debt:policy + - name: API Signature Snapshot Check + run: | + corepack yarn lint:api:snapshot - name: Header Format Check run: | corepack yarn lint:headers
package.json+26 −14 modified@@ -19,6 +19,7 @@ "type": "git", "url": "https://github.com/locutusjs/locutus.git" }, + "type": "module", "license": "MIT", "author": "Kevin van Zonneveld <kevin@vanzonneveld.net>", "browser": { @@ -28,27 +29,39 @@ "scripts": { "browser:bundle": "browserify test/browser/app.js --outfile test/browser/bundle.js", "browser:watch": "budo test/browser/app.js --live --serve test/browser/bundle.js", - "build:dist": "rimraf dist && cp -r src dist && rm -rf dist/_util && cp package.json README.md dist/", - "build:indices": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON src/_util/cli.ts reindex", - "build:tests:noskip": "rimraf test/generated && node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON src/_util/cli.ts writetests --noskip", - "build:tests": "rimraf test/generated && node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON src/_util/cli.ts writetests", + "build:dist": "rimraf dist && cp -r src dist && cp package.json README.md dist && tsc -p tsconfig.build.json --outDir dist && tsc -p tsconfig.build.esm.json --outDir dist/esm && find dist -name '*.ts' ! -name '*.d.ts' -delete && node scripts/fix-cjs-exports.ts dist && rm -rf dist/_util dist/esm/_util", + "build:indices": "node src/_util/cli.ts reindex", + "build:tests:noskip": "rimraf test/generated && node src/_util/cli.ts writetests --noskip", + "build:tests": "rimraf test/generated && node src/_util/cli.ts writetests", "build": "npm-run-all 'build:*'", - "injectweb": "rimraf website/source/{c,golang,php,python,ruby} && node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON src/_util/cli.ts injectweb", - "check": "npm-run-all --serial fix:biome lint lint:ts lint:headers lint:no-stray-js test:languages", + "injectweb": "rimraf website/source/{c,golang,php,python,ruby} && node src/_util/cli.ts injectweb", + "check": "npm-run-all --serial fix:biome lint lint:ts lint:ts:strict-next lint:ts:debt:policy lint:api:snapshot lint:type:contracts lint:headers lint:no-stray-js test:languages", "fix:biome": "biome check --write .", - "fix:headers": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON src/_util/formatHeaders.ts fix", - "lint:headers": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON src/_util/formatHeaders.ts check", - "lint:no-stray-js": "bash -c 'found=$(find src test -name \"*.js\" | grep -vE \"^src/(awk|c|clojure|elixir|golang|julia|lua|perl|php|python|r|ruby)/|/index\\.js$|^test/browser/\"); if [ -n \"$found\" ]; then echo \"Stray .js files found (should be .ts):\"; echo \"$found\"; exit 1; fi'", + "fix:headers": "node src/_util/formatHeaders.ts fix", + "lint:headers": "node src/_util/formatHeaders.ts check", + "compare:dt:signatures": "node scripts/compare-definitelytyped-signatures.ts", + "lint:no-stray-js": "node scripts/check-allowed-js-files.ts", "lint:ts": "tsc --noEmit -p tsconfig.json", + "lint:ts:strict-next": "tsc --noEmit -p tsconfig.strict-next.json", + "lint:ts:debt:policy": "node scripts/check-ts-debt-policy.ts", + "lint:api:snapshot:php": "node scripts/check-api-signature-snapshot.ts --scope=php", + "lint:api:snapshot:nonphp": "node scripts/check-api-signature-snapshot.ts --scope=non-php", + "lint:api:snapshot": "npm-run-all --serial lint:api:snapshot:php lint:api:snapshot:nonphp", + "lint:type:contracts": "node scripts/check-type-contracts-snapshot.ts", + "fix:api:snapshot:php": "node scripts/check-api-signature-snapshot.ts --scope=php --update", + "fix:api:snapshot:nonphp": "node scripts/check-api-signature-snapshot.ts --scope=non-php --update", + "fix:api:snapshot": "npm-run-all --serial fix:api:snapshot:php fix:api:snapshot:nonphp", + "fix:type:contracts": "node scripts/check-type-contracts-snapshot.ts --update", "fix:markdown": "remark {README,CONTRIBUTING}.md --output", "fix": "npm-run-all --serial 'fix:**'", "lint": "biome check .", "playground:start": "cd test/browser && node server.js", "test:languages:noskip": "yarn build:tests:noskip && vitest run", "test:languages": "yarn build:tests && vitest run", - "test:module": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON test/module/module.ts", - "test:parity": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON test/parity/index.ts", - "test:parity:php": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON test/parity/index.ts php", + "test:module:smoke": "node test/module/module.ts", + "test:module": "npm-run-all --serial build:dist test:module:smoke", + "test:parity": "node test/parity/index.ts", + "test:parity:php": "node test/parity/index.ts php", "test:util": "vitest run test/util/", "test": "npm-run-all test:languages test:module", "website:install": "cd website && yarn", @@ -64,7 +77,6 @@ "budo": "11.8.4", "cross-env": "10", "debug": "^4.4.3", - "esprima": "4.0.1", "globby": "4.1.0", "indent-string": "2.1.0", "js-yaml": "4.1.1", @@ -81,7 +93,7 @@ }, "packageManager": "yarn@4.12.0", "engines": { - "node": ">= 10", + "node": ">= 22", "yarn": ">= 1" }, "readmeFilename": "README.md"
README.md+12 −45 modified@@ -5,66 +5,33 @@ [](https://locutus.io/php/) [](https://locutus.io/python/) -> All your standard libraries will be assimilated into our JavaScript collective. Resistance is futile. +> All your standard libraries will be assimilated into our ~~JavaScript~~ TypeScript collective. Resistance is futile. -Welcome to Locutus, where the boundaries of coding languages blur. We're a dedicated collective developers on a mission -to explore the possibilities of porting standard libraries from various programming language (Go, Ruby, PHP, C) to -JavaScript. Our journey is one of discovery, innovation, and sometimes, delightful chaos. +Locutus is ~500 TypeScript implementations of standard library functions from PHP, Go, Python, Ruby, C, and [more](https://locutus.io/). Each function is individually importable and tree-shakeable. -From the complex to the quirky, we assimilate libraries with a spirit of curiosity and a penchant for experimentation. -Our creations typically start as rainy Sunday afternoon puzzles, and end up ranging from groundbreaking functions that -enhance the JavaScript ecosystem, to unique oddities that challenge the norms of coding. - -As we navigate through this uncharted territory, we invite you to join us. Whether to contribute, learn, or simply -marvel at the wonders of cross-language integration and portability, your presence on GitHub is valued. - -Embark on this journey with us at [locutus.io](https://locutus.io/). - -Use our creations at your own risk, and may they inspire you to push the boundaries of what's possible with JavaScript. - -## Table of contents - -- [Install](#install) -- [Use](#use) -- [Development](#development) -- [License](#license) +Most of these started as rainy Sunday afternoon puzzles. Some are genuinely useful. Some are just fun to write. All of them are a way to learn how different languages solve the same problems. ## Install ```bash -yarn add locutus +npm install locutus ``` ## Use -```bash -$ vim php.js -``` +```typescript +import { sprintf } from 'locutus/php/strings/sprintf' -```javascript -const sprintf = require('locutus/php/strings/sprintf') -const echo = require('locutus/php/strings/echo') const effectiveness = 'futile' -echo(sprintf('Resistance is %s', effectiveness)) +console.log(sprintf('Resistance is %s', effectiveness)) +// Resistance is futile ``` -```bash -$ node php.js -Resistance is futile -``` - -```bash -$ vim go.js -``` +```typescript +import { Contains } from 'locutus/golang/strings/Contains' -```javascript -const strings = require('locutus/golang/strings') -console.log(strings.Contains('Locutus', 'cut')) -``` - -```bash -$ node go.js -true +console.log(Contains('Locutus', 'cut')) +// true ``` ## Development
scripts/check-allowed-js-files.ts+40 −0 added@@ -0,0 +1,40 @@ +import { execFileSync } from 'node:child_process' +import fs from 'node:fs' + +const allowedJsFiles = new Set<string>([ + '.remarkrc.mjs', + 'test/browser/app.js', + 'website/themes/icarus/scripts/fancybox.js', + 'website/themes/icarus/scripts/locutus_stats.js', + 'website/themes/icarus/scripts/meta.js', + 'website/themes/icarus/scripts/thumbnail.js', + 'website/themes/icarus/source/js/insight.js', + 'website/themes/icarus/source/js/locutus.js', + 'website/themes/icarus/source/js/main.js', + 'website/themes/icarus/source/vendor/fancybox/helpers/jquery.fancybox-buttons.js', + 'website/themes/icarus/source/vendor/fancybox/helpers/jquery.fancybox-media.js', + 'website/themes/icarus/source/vendor/fancybox/helpers/jquery.fancybox-thumbs.js', + 'website/themes/icarus/source/vendor/fancybox/jquery.fancybox.js', + 'website/themes/icarus/source/vendor/fancybox/jquery.fancybox.pack.js', + 'website/themes/icarus/source/vendor/jquery/2.1.3/jquery.min.js', +]) + +const output = execFileSync('git', ['ls-files', '*.js', '*.mjs'], { encoding: 'utf8' }).trim() +const trackedJsFiles = output + .split('\n') + .map((filePath) => filePath.trim()) + .filter((filePath) => filePath.length > 0) + .filter((filePath) => fs.existsSync(filePath)) + .sort((left, right) => left.localeCompare(right)) + +const unexpectedFiles = trackedJsFiles.filter((filePath) => !allowedJsFiles.has(filePath)) + +if (unexpectedFiles.length > 0) { + console.error('Unexpected .js/.mjs files detected. Port these to TypeScript or update allowlist intentionally:') + for (const filePath of unexpectedFiles) { + console.error(` - ${filePath}`) + } + process.exit(1) +} + +console.log(`js policy ok: ${trackedJsFiles.length} allowed .js/.mjs file(s)`)
scripts/check-api-signature-snapshot.ts+102 −0 added@@ -0,0 +1,102 @@ +import fs from 'node:fs' +import path from 'node:path' +import ts from 'typescript' + +const cwd = process.cwd() +const shouldUpdate = process.argv.includes('--update') +const scopeArg = process.argv.find((arg) => arg.startsWith('--scope=')) +const scope = scopeArg?.slice('--scope='.length) ?? 'php' + +if (scope !== 'php' && scope !== 'non-php') { + console.error(`Invalid --scope value: ${scope}`) + console.error("Allowed values: 'php', 'non-php'") + process.exit(1) +} + +const isPhpScope = scope === 'php' +const snapshotPath = path.join( + cwd, + 'docs', + isPhpScope ? 'php-api-signatures.snapshot' : 'non-php-api-signatures.snapshot', +) +const srcRootDir = path.join(cwd, 'src') +const srcPhpDir = path.join(srcRootDir, 'php') +const label = isPhpScope ? 'PHP' : 'Non-PHP' + +const hasExportModifier = (node: ts.Node): boolean => + !!node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) + +const normalizeWhitespace = (value: string): string => value.replace(/\s+/g, ' ').trim() + +const getFunctionSignatureText = (sourceFile: ts.SourceFile, declaration: ts.FunctionDeclaration): string => { + const sourceText = sourceFile.getFullText() + const start = declaration.getStart(sourceFile) + const end = declaration.body ? declaration.body.getStart(sourceFile) : declaration.end + return normalizeWhitespace(sourceText.slice(start, end)) +} + +const sourceFiles = ts.sys + .readDirectory(isPhpScope ? srcPhpDir : srcRootDir, ['.ts'], undefined, undefined) + .filter((filePath) => !filePath.endsWith('.d.ts')) + .filter((filePath) => (isPhpScope ? true : !filePath.includes(`${path.sep}src${path.sep}php${path.sep}`))) + +const lines = new Set<string>() + +for (const filePath of sourceFiles) { + const sourceText = fs.readFileSync(filePath, 'utf8') + const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS) + const relativePath = path.relative(cwd, filePath).replaceAll(path.sep, '/') + + sourceFile.forEachChild((node) => { + if (ts.isFunctionDeclaration(node) && hasExportModifier(node) && node.name) { + const signature = getFunctionSignatureText(sourceFile, node) + lines.add(`${relativePath} :: ${signature}`) + return + } + + if (ts.isTypeAliasDeclaration(node) && hasExportModifier(node)) { + lines.add(`${relativePath} :: ${normalizeWhitespace(node.getText(sourceFile))}`) + return + } + + if (ts.isInterfaceDeclaration(node) && hasExportModifier(node)) { + lines.add(`${relativePath} :: ${normalizeWhitespace(node.getText(sourceFile))}`) + } + }) +} + +const snapshotContent = [...lines].sort().join('\n') + '\n' + +if (shouldUpdate) { + fs.writeFileSync(snapshotPath, snapshotContent) + console.log(`Updated API signature snapshot at ${path.relative(cwd, snapshotPath)}`) + process.exit(0) +} + +if (!fs.existsSync(snapshotPath)) { + console.error(`Snapshot file missing: ${path.relative(cwd, snapshotPath)}`) + console.error('Run: corepack yarn fix:api:snapshot') + process.exit(1) +} + +const expected = fs.readFileSync(snapshotPath, 'utf8') +if (expected !== snapshotContent) { + const expectedLines = expected.split('\n') + const currentLines = snapshotContent.split('\n') + let firstDiffIndex = 0 + while ( + firstDiffIndex < expectedLines.length && + firstDiffIndex < currentLines.length && + expectedLines[firstDiffIndex] === currentLines[firstDiffIndex] + ) { + firstDiffIndex += 1 + } + + console.error(`${label} API signature snapshot mismatch at line ${firstDiffIndex + 1}`) + console.error(`Expected: ${expectedLines[firstDiffIndex] ?? '<EOF>'}`) + console.error(`Current: ${currentLines[firstDiffIndex] ?? '<EOF>'}`) + console.error('If this change is intentional, run: corepack yarn fix:api:snapshot') + process.exit(1) +} + +console.log(`${label} API signature snapshot is up to date (${path.relative(cwd, snapshotPath)})`)
scripts/check-engine-bump.js+0 −82 removed@@ -1,82 +0,0 @@ -#!/usr/bin/env node - -const { execSync } = require('node:child_process') -const { readFileSync } = require('node:fs') - -function parseSemver(input) { - if (!input) { - return null - } - - const match = String(input).match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/) - if (!match) { - return null - } - - return [Number(match[1]), Number(match[2] || 0), Number(match[3] || 0)] -} - -function compareSemver(a, b) { - for (let i = 0; i < 3; i += 1) { - if (a[i] > b[i]) { - return 1 - } - if (a[i] < b[i]) { - return -1 - } - } - return 0 -} - -function readJson(ref) { - if (!ref) { - return JSON.parse(readFileSync('package.json', 'utf8')) - } - - const content = execSync(`git show ${ref}:package.json`, { encoding: 'utf8' }) - return JSON.parse(content) -} - -const baseRef = process.env.GITHUB_BASE_REF -if (!baseRef) { - console.log('[engine-bump] GITHUB_BASE_REF not set; skipping check.') - process.exit(0) -} - -const basePkg = readJson(`origin/${baseRef}`) -const headPkg = readJson(null) - -const baseEngine = basePkg.engines && basePkg.engines.node -const headEngine = headPkg.engines && headPkg.engines.node - -if (baseEngine === headEngine) { - process.exit(0) -} - -if (!headEngine) { - console.log('[engine-bump] engines.node removed or unset; skipping check.') - process.exit(0) -} - -const baseMin = parseSemver(baseEngine) -const headMin = parseSemver(headEngine) - -let raised = true -if (baseMin && headMin) { - raised = compareSemver(headMin, baseMin) > 0 -} - -if (!raised) { - process.exit(0) -} - -const baseVersion = parseSemver(basePkg.version) || [0, 0, 0] -const headVersion = parseSemver(headPkg.version) || [0, 0, 0] - -if (headVersion[0] <= baseVersion[0]) { - console.error( - `[engine-bump] engines.node increased from "${baseEngine}" to "${headEngine}" without a major version bump (${basePkg.version} -> ${headPkg.version}).`, - ) - console.error('Engine bumps are treated as breaking changes. Bump the major version or revert engines.node.') - process.exit(1) -}
scripts/check-engine-bump.ts+137 −0 added@@ -0,0 +1,137 @@ +import { execSync } from 'node:child_process' +import { readdirSync, readFileSync } from 'node:fs' +import path from 'node:path' + +type Semver = [number, number, number] + +interface PackageJsonShape { + version?: string + engines?: { + node?: string + } +} + +const parseSemver = (input: unknown): Semver | null => { + if (!input) { + return null + } + + const match = String(input).match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/) + if (!match) { + return null + } + + return [Number(match[1]), Number(match[2] || 0), Number(match[3] || 0)] +} + +const compareSemver = (left: Semver, right: Semver): number => { + for (let i = 0; i < 3; i += 1) { + if (left[i] > right[i]) { + return 1 + } + if (left[i] < right[i]) { + return -1 + } + } + return 0 +} + +const readJson = (ref?: string): PackageJsonShape => { + if (!ref) { + return JSON.parse(readFileSync('package.json', 'utf8')) as PackageJsonShape + } + + const content = execSync(`git show ${ref}:package.json`, { encoding: 'utf8' }) + return JSON.parse(content) as PackageJsonShape +} + +const escapeRegex = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + +const hasPendingMajorChangeset = (packageName: string): boolean => { + const changesetDir = '.changeset' + let entries: ReturnType<typeof readdirSync> + + try { + entries = readdirSync(changesetDir, { withFileTypes: true }) + } catch { + return false + } + + const markdownFiles = entries + .filter((entry) => entry.isFile() && entry.name.endsWith('.md')) + .map((entry) => path.join(changesetDir, entry.name)) + + for (const filePath of markdownFiles) { + let content = '' + try { + content = readFileSync(filePath, 'utf8') + } catch { + continue + } + + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/) + if (!frontmatterMatch) { + continue + } + + const frontmatter = frontmatterMatch[1] + const packageRegex = new RegExp( + String.raw`["']?${escapeRegex(packageName)}["']?\s*:\s*["']?(major|premajor)["']?\b`, + 'i', + ) + if (packageRegex.test(frontmatter)) { + return true + } + } + + return false +} + +const baseRef = process.env.GITHUB_BASE_REF +if (!baseRef) { + console.log('[engine-bump] GITHUB_BASE_REF not set; skipping check.') + process.exit(0) +} + +const basePkg = readJson(`origin/${baseRef}`) +const headPkg = readJson() + +const baseEngine = basePkg.engines?.node +const headEngine = headPkg.engines?.node + +if (baseEngine === headEngine) { + process.exit(0) +} + +if (!headEngine) { + console.log('[engine-bump] engines.node removed or unset; skipping check.') + process.exit(0) +} + +const baseMin = parseSemver(baseEngine) +const headMin = parseSemver(headEngine) + +let raised = true +if (baseMin && headMin) { + raised = compareSemver(headMin, baseMin) > 0 +} + +if (!raised) { + process.exit(0) +} + +const baseVersion = parseSemver(basePkg.version) || [0, 0, 0] +const headVersion = parseSemver(headPkg.version) || [0, 0, 0] + +if (headVersion[0] <= baseVersion[0]) { + if (hasPendingMajorChangeset('locutus')) { + console.log('[engine-bump] engines.node increased with pending major changeset for "locutus"; allowing.') + process.exit(0) + } + + console.error( + `[engine-bump] engines.node increased from "${baseEngine}" to "${headEngine}" without a major version bump (${basePkg.version} -> ${headPkg.version}).`, + ) + console.error('Engine bumps are treated as breaking changes. Bump the major version or revert engines.node.') + process.exit(1) +}
scripts/check-ts-debt-policy.ts+1301 −0 added@@ -0,0 +1,1301 @@ +import fs from 'node:fs' +import path from 'node:path' +import ts from 'typescript' + +interface Finding { + file: string + count: number +} + +const MAX_SRC_PHP_RAW_INDEX_SIGNATURE_UNKNOWN = 0 +const MAX_SRC_PHP_EXPORTED_UNKNOWN_RETURN_TYPES = 0 +const MAX_SRC_PHP_EXPORTED_OBJECT_KEYWORD = 0 +const MAX_SRC_PHP_EXPORTED_EMPTY_OBJECT_TYPE = 0 +const MAX_SRC_PHP_EXPORTED_FUNCTION_WITHOUT_RETURN_TYPE = 0 +const MAX_SRC_NON_PHP_EXPORTED_FUNCTION_WITHOUT_RETURN_TYPE = 0 +const MAX_SRC_NON_PHP_LOCAL_NULLISH_ALIAS = 0 +const MAX_SRC_NON_PHP_LOCAL_OBJECT_ALIAS = 0 +const MAX_SRC_NON_PHP_LOCAL_UNKNOWN_ALIAS = 0 +const MAX_SRC_PHP_EXPORTED_PHPVALUE_IDENTIFIER = 0 +const MAX_SRC_PHP_EXPORTED_PHPINPUT_OUTSIDE_HELPERS = 0 +const MAX_SRC_PHP_ARRAY_EXPORTED_PHPVALUE_IDENTIFIER = 0 +const MAX_SRC_PHP_ARRAY_EXPORTED_SORTFLAG_STRING_PARAM = 0 +const MAX_SRC_PHP_ARRAY_EXPORTED_MODE_CASE_STRING_PARAM = 0 +const MAX_SRC_PHP_UNKNOWN_KEYWORD = 0 +const MAX_SRC_PHP_ARRAY_UNKNOWN_KEYWORD = 0 +const MAX_SRC_PHP_VAR_UNKNOWN_KEYWORD = 0 +const MAX_SRC_PHP_STRINGS_AS_EXPRESSION = 0 +const MAX_SRC_PHP_CTYPE_AS_EXPRESSION = 0 +const MAX_SRC_PHP_INFO_AS_EXPRESSION = 0 +const MAX_SRC_PHP_HELPERS_AS_EXPRESSION = 0 +const MAX_SRC_PHP_URL_AS_EXPRESSION = 0 +const MAX_SRC_PHP_FUNCHAND_AS_EXPRESSION = 0 +const MAX_SRC_PHP_JSON_AS_EXPRESSION = 0 +const MAX_SRC_PHP_DATETIME_AS_EXPRESSION = 0 +const MAX_SRC_PHP_ARRAY_AS_EXPRESSION = 0 +const MAX_SRC_PHP_BC_AS_EXPRESSION = 0 +const MAX_SRC_PHP_FILESYSTEM_AS_EXPRESSION = 0 +const MAX_SRC_PHP_MISC_AS_EXPRESSION = 0 +const MAX_SRC_PHP_PCRE_AS_EXPRESSION = 0 +const MAX_SRC_PHP_VAR_AS_EXPRESSION = 0 +const MAX_SRC_PHP_XDIFF_AS_EXPRESSION = 0 +const MAX_SRC_PHP_LOCAL_PHPVALUE_ALIAS = 0 +const MAX_SRC_PHP_LOCAL_PHPINPUT_ALIAS_OUTSIDE_HELPERS = 0 +const MAX_SRC_PHP_DIRECT_INI_GLOBAL_READS = 0 +const MAX_SRC_PHP_PHPVALUE_IDENTIFIER = 0 +const MAX_SRC_PHP_INLINE_NULLISH_ALIAS = 0 +const MAX_SRC_PHP_REFLECT_GET_SET = 0 +const MAX_SRC_PHP_GLOBALTHIS_IDENTIFIER = 0 +const MAX_SRC_PHP_PHPMIXED_KEYWORD = 0 +const MAX_SRC_PHP_ARRAY_PHPMIXED_KEYWORD = 0 +const MAX_SRC_PHP_VAR_PHPMIXED_KEYWORD = 0 +const MAX_SRC_PHP_STRINGS_PHPMIXED_KEYWORD = 0 + +const cwd = process.cwd() +const srcDir = path.join(cwd, 'src') + +const sourceFiles = ts.sys + .readDirectory(srcDir, ['.ts'], undefined, undefined) + .filter((filePath) => !filePath.endsWith('.d.ts')) + +const tsNoCheckFiles: string[] = [] +const tsIgnoreFindings: Finding[] = [] +const tsExpectErrorFindings: Finding[] = [] +const recordStringUnknownFindings: Finding[] = [] +const functionTypeFindings: Finding[] = [] +const asUnknownAsFindings: Finding[] = [] +const argumentsIdentifierFindings: Finding[] = [] +const exportedUnknownReturnTypeFindings: Finding[] = [] +const exportedObjectKeywordFindings: Finding[] = [] +const exportedEmptyObjectTypeFindings: Finding[] = [] +const exportedFunctionWithoutReturnTypeFindings: Finding[] = [] +const nonPhpExportedFunctionWithoutReturnTypeFindings: Finding[] = [] +const nonPhpLocalNullishAliasFindings: Finding[] = [] +const nonPhpLocalObjectAliasFindings: Finding[] = [] +const nonPhpLocalUnknownAliasFindings: Finding[] = [] +const exportedPhpValueIdentifierFindings: Finding[] = [] +const exportedPhpInputOutsideHelpersFindings: Finding[] = [] +const arrayExportedPhpValueIdentifierFindings: Finding[] = [] +const arrayExportedSortFlagStringParamFindings: Finding[] = [] +const arrayExportedModeCaseStringParamFindings: Finding[] = [] +const localPhpValueAliasFindings: Finding[] = [] +const localPhpInputAliasOutsideHelpersFindings: Finding[] = [] +const directIniGlobalReadFindings: Finding[] = [] +const stringsAsExpressionFindings: Finding[] = [] +const ctypeAsExpressionFindings: Finding[] = [] +const infoAsExpressionFindings: Finding[] = [] +const helpersAsExpressionFindings: Finding[] = [] +const urlAsExpressionFindings: Finding[] = [] +const funchandAsExpressionFindings: Finding[] = [] +const jsonAsExpressionFindings: Finding[] = [] +const datetimeAsExpressionFindings: Finding[] = [] +const arrayAsExpressionFindings: Finding[] = [] +const bcAsExpressionFindings: Finding[] = [] +const filesystemAsExpressionFindings: Finding[] = [] +const miscAsExpressionFindings: Finding[] = [] +const pcreAsExpressionFindings: Finding[] = [] +const varAsExpressionFindings: Finding[] = [] +const xdiffAsExpressionFindings: Finding[] = [] +const phpMixedFindings: Finding[] = [] +const arrayPhpMixedFindings: Finding[] = [] +const varPhpMixedFindings: Finding[] = [] +const stringsPhpMixedFindings: Finding[] = [] +const phpValueIdentifierFindings: Finding[] = [] +const inlineNullishAliasFindings: Finding[] = [] +const reflectGetSetFindings: Finding[] = [] +const globalThisIdentifierFindings: Finding[] = [] +let srcPhpRawIndexSignatureUnknownCount = 0 +let srcPhpExportedUnknownReturnTypeCount = 0 +let srcPhpExportedObjectKeywordCount = 0 +let srcPhpExportedEmptyObjectTypeCount = 0 +let srcPhpExportedFunctionWithoutReturnTypeCount = 0 +let srcNonPhpExportedFunctionWithoutReturnTypeCount = 0 +let srcNonPhpLocalNullishAliasCount = 0 +let srcNonPhpLocalObjectAliasCount = 0 +let srcNonPhpLocalUnknownAliasCount = 0 +let srcPhpExportedPhpValueIdentifierCount = 0 +let srcPhpExportedPhpInputOutsideHelpersCount = 0 +let srcPhpArrayExportedPhpValueIdentifierCount = 0 +let srcPhpArrayExportedSortFlagStringParamCount = 0 +let srcPhpArrayExportedModeCaseStringParamCount = 0 +let srcPhpUnknownKeywordCount = 0 +let srcPhpArrayUnknownKeywordCount = 0 +let srcPhpVarUnknownKeywordCount = 0 +let srcPhpStringsAsExpressionCount = 0 +let srcPhpCtypeAsExpressionCount = 0 +let srcPhpInfoAsExpressionCount = 0 +let srcPhpHelpersAsExpressionCount = 0 +let srcPhpUrlAsExpressionCount = 0 +let srcPhpFunchandAsExpressionCount = 0 +let srcPhpJsonAsExpressionCount = 0 +let srcPhpDatetimeAsExpressionCount = 0 +let srcPhpArrayAsExpressionCount = 0 +let srcPhpBcAsExpressionCount = 0 +let srcPhpFilesystemAsExpressionCount = 0 +let srcPhpMiscAsExpressionCount = 0 +let srcPhpPcreAsExpressionCount = 0 +let srcPhpVarAsExpressionCount = 0 +let srcPhpXdiffAsExpressionCount = 0 +let srcPhpLocalPhpValueAliasCount = 0 +let srcPhpLocalPhpInputAliasOutsideHelpersCount = 0 +let srcPhpDirectIniGlobalReadCount = 0 +let srcPhpPhpValueIdentifierCount = 0 +let srcPhpInlineNullishAliasCount = 0 +let srcPhpReflectGetSetCount = 0 +let srcPhpGlobalThisIdentifierCount = 0 +let srcPhpPhpMixedKeywordCount = 0 +let srcPhpArrayPhpMixedKeywordCount = 0 +let srcPhpVarPhpMixedKeywordCount = 0 +let srcPhpStringsPhpMixedKeywordCount = 0 + +const countFunctionTypeReferences = (sourceFile: ts.SourceFile): number => { + let count = 0 + + const visit = (node: ts.Node): void => { + if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === 'Function') { + count += 1 + } + ts.forEachChild(node, visit) + } + + visit(sourceFile) + return count +} + +const countArgumentsIdentifiers = (sourceFile: ts.SourceFile): number => { + let count = 0 + + const visit = (node: ts.Node): void => { + if (ts.isIdentifier(node) && node.text === 'arguments') { + count += 1 + } + ts.forEachChild(node, visit) + } + + visit(sourceFile) + return count +} + +const countAsExpressions = (sourceFile: ts.SourceFile): number => { + let count = 0 + + const visit = (node: ts.Node): void => { + if (ts.isAsExpression(node)) { + count += 1 + } + ts.forEachChild(node, visit) + } + + visit(sourceFile) + return count +} + +const hasExportModifier = (node: ts.Node): boolean => + !!node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) + +const isUnknownTopLevelReturnType = (typeNode: ts.TypeNode): boolean => { + if (typeNode.kind === ts.SyntaxKind.UnknownKeyword) { + return true + } + + if (ts.isUnionTypeNode(typeNode)) { + return typeNode.types.some((member) => member.kind === ts.SyntaxKind.UnknownKeyword) + } + + return false +} + +const countExportedUnknownReturnTypes = (sourceFile: ts.SourceFile): number => { + let count = 0 + + const visit = (node: ts.Node): void => { + if ( + ts.isFunctionDeclaration(node) && + hasExportModifier(node) && + node.type && + isUnknownTopLevelReturnType(node.type) + ) { + count += 1 + } + ts.forEachChild(node, visit) + } + + visit(sourceFile) + return count +} + +const countObjectAndEmptyObjectTypeNodes = ( + typeNode: ts.TypeNode, +): { objectKeywordCount: number; emptyObjectTypeCount: number } => { + let objectKeywordCount = 0 + let emptyObjectTypeCount = 0 + + const visit = (node: ts.Node): void => { + if (node.kind === ts.SyntaxKind.ObjectKeyword) { + objectKeywordCount += 1 + } + if (ts.isTypeLiteralNode(node) && node.members.length === 0) { + emptyObjectTypeCount += 1 + } + ts.forEachChild(node, visit) + } + + visit(typeNode) + return { objectKeywordCount, emptyObjectTypeCount } +} + +const countTypeIdentifierNodes = (typeNode: ts.TypeNode, identifierName: string): number => { + let count = 0 + + const visit = (node: ts.Node): void => { + if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === identifierName) { + count += 1 + } + ts.forEachChild(node, visit) + } + + visit(typeNode) + return count +} + +const countExportedTypeBroadMarkers = ( + sourceFile: ts.SourceFile, +): { + objectKeywordCount: number + emptyObjectTypeCount: number + functionWithoutReturnTypeCount: number + phpValueIdentifierCount: number + phpInputIdentifierCount: number +} => { + let objectKeywordCount = 0 + let emptyObjectTypeCount = 0 + let functionWithoutReturnTypeCount = 0 + let phpValueIdentifierCount = 0 + let phpInputIdentifierCount = 0 + + const addTypeNodeCounts = (typeNode: ts.TypeNode): void => { + const counts = countObjectAndEmptyObjectTypeNodes(typeNode) + objectKeywordCount += counts.objectKeywordCount + emptyObjectTypeCount += counts.emptyObjectTypeCount + phpValueIdentifierCount += countTypeIdentifierNodes(typeNode, 'PhpValue') + phpInputIdentifierCount += countTypeIdentifierNodes(typeNode, 'PhpInput') + } + + sourceFile.forEachChild((node) => { + if (ts.isFunctionDeclaration(node) && hasExportModifier(node)) { + if (node.type) { + addTypeNodeCounts(node.type) + } else if (node.body) { + functionWithoutReturnTypeCount += 1 + } + + for (const parameter of node.parameters) { + if (parameter.type) { + addTypeNodeCounts(parameter.type) + } + } + + for (const typeParameter of node.typeParameters ?? []) { + if (typeParameter.constraint) { + addTypeNodeCounts(typeParameter.constraint) + } + if (typeParameter.default) { + addTypeNodeCounts(typeParameter.default) + } + } + return + } + + if (ts.isVariableStatement(node) && hasExportModifier(node)) { + for (const declaration of node.declarationList.declarations) { + if (declaration.type) { + addTypeNodeCounts(declaration.type) + } + } + return + } + + // Intentionally scoped to exported runtime APIs (functions/vars). + // Type aliases/interfaces are tracked separately and can evolve in staged passes. + }) + + return { + objectKeywordCount, + emptyObjectTypeCount, + functionWithoutReturnTypeCount, + phpValueIdentifierCount, + phpInputIdentifierCount, + } +} + +const countExportedStringSortFlagParams = (sourceFile: ts.SourceFile): number => { + let count = 0 + const sortFlagParamNames = new Set(['sortFlags', 'sortFlag']) + + sourceFile.forEachChild((node) => { + if (!ts.isFunctionDeclaration(node) || !hasExportModifier(node)) { + return + } + + for (const parameter of node.parameters) { + if (!ts.isIdentifier(parameter.name) || !sortFlagParamNames.has(parameter.name.text) || !parameter.type) { + continue + } + if (parameter.type.kind === ts.SyntaxKind.StringKeyword) { + count += 1 + } + } + }) + + return count +} + +const countExportedStringModeCaseParams = (sourceFile: ts.SourceFile): number => { + let count = 0 + const modeCaseParamNames = new Set(['mode', 'cs']) + + sourceFile.forEachChild((node) => { + if (!ts.isFunctionDeclaration(node) || !hasExportModifier(node)) { + return + } + + for (const parameter of node.parameters) { + if (!ts.isIdentifier(parameter.name) || !modeCaseParamNames.has(parameter.name.text) || !parameter.type) { + continue + } + if (parameter.type.kind === ts.SyntaxKind.StringKeyword) { + count += 1 + } + } + }) + + return count +} + +for (const filePath of sourceFiles) { + const sourceText = fs.readFileSync(filePath, 'utf8') + + if (/@ts-nocheck\b/.test(sourceText)) { + tsNoCheckFiles.push(path.relative(cwd, filePath)) + } + + const tsIgnoreCount = (sourceText.match(/@ts-ignore\b/g) || []).length + if (tsIgnoreCount > 0) { + tsIgnoreFindings.push({ + file: path.relative(cwd, filePath), + count: tsIgnoreCount, + }) + } + + const tsExpectErrorCount = (sourceText.match(/@ts-expect-error\b/g) || []).length + if (tsExpectErrorCount > 0) { + tsExpectErrorFindings.push({ + file: path.relative(cwd, filePath), + count: tsExpectErrorCount, + }) + } + + const recordStringUnknownCount = (sourceText.match(/\bRecord\s*<\s*string\s*,\s*unknown\s*>/g) || []).length + if (recordStringUnknownCount > 0) { + recordStringUnknownFindings.push({ + file: path.relative(cwd, filePath), + count: recordStringUnknownCount, + }) + } + + const asUnknownAsCount = (sourceText.match(/\bas\s+unknown\s+as\b/g) || []).length + if (asUnknownAsCount > 0) { + asUnknownAsFindings.push({ + file: path.relative(cwd, filePath), + count: asUnknownAsCount, + }) + } + + const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS) + const functionTypeCount = countFunctionTypeReferences(sourceFile) + if (functionTypeCount > 0) { + functionTypeFindings.push({ + file: path.relative(cwd, filePath), + count: functionTypeCount, + }) + } + + const isPhpFile = filePath.includes(`${path.sep}src${path.sep}php${path.sep}`) + if (!isPhpFile) { + const nonPhpExportedTypeBroadCounts = countExportedTypeBroadMarkers(sourceFile) + srcNonPhpExportedFunctionWithoutReturnTypeCount += nonPhpExportedTypeBroadCounts.functionWithoutReturnTypeCount + if (nonPhpExportedTypeBroadCounts.functionWithoutReturnTypeCount > 0) { + nonPhpExportedFunctionWithoutReturnTypeFindings.push({ + file: path.relative(cwd, filePath), + count: nonPhpExportedTypeBroadCounts.functionWithoutReturnTypeCount, + }) + } + + const nonPhpLocalNullishAliasCount = ( + sourceText.match(/\btype\s+[A-Za-z0-9_]+\s*=\s*\{\}\s*\|\s*null\s*\|\s*undefined\b/g) || [] + ).length + srcNonPhpLocalNullishAliasCount += nonPhpLocalNullishAliasCount + if (nonPhpLocalNullishAliasCount > 0) { + nonPhpLocalNullishAliasFindings.push({ + file: path.relative(cwd, filePath), + count: nonPhpLocalNullishAliasCount, + }) + } + + const nonPhpLocalObjectAliasCount = (sourceText.match(/^type\s+[A-Za-z0-9_]+\s*=\s*object\s*$/gm) || []).length + srcNonPhpLocalObjectAliasCount += nonPhpLocalObjectAliasCount + if (nonPhpLocalObjectAliasCount > 0) { + nonPhpLocalObjectAliasFindings.push({ + file: path.relative(cwd, filePath), + count: nonPhpLocalObjectAliasCount, + }) + } + + const nonPhpLocalUnknownAliasCount = (sourceText.match(/^type\s+[A-Za-z0-9_]+\s*=\s*unknown\s*$/gm) || []).length + srcNonPhpLocalUnknownAliasCount += nonPhpLocalUnknownAliasCount + if (nonPhpLocalUnknownAliasCount > 0) { + nonPhpLocalUnknownAliasFindings.push({ + file: path.relative(cwd, filePath), + count: nonPhpLocalUnknownAliasCount, + }) + } + } + + if (isPhpFile) { + const reflectGetSetCount = (sourceText.match(/\bReflect\.(?:get|set)\s*\(/g) || []).length + srcPhpReflectGetSetCount += reflectGetSetCount + if (reflectGetSetCount > 0) { + reflectGetSetFindings.push({ + file: path.relative(cwd, filePath), + count: reflectGetSetCount, + }) + } + + const globalThisIdentifierCount = (sourceText.match(/\bglobalThis\b/g) || []).length + srcPhpGlobalThisIdentifierCount += globalThisIdentifierCount + if (globalThisIdentifierCount > 0) { + globalThisIdentifierFindings.push({ + file: path.relative(cwd, filePath), + count: globalThisIdentifierCount, + }) + } + + const srcPhpValueCount = (sourceText.match(/\bPhpValue\b/g) || []).length + srcPhpPhpValueIdentifierCount += srcPhpValueCount + if (srcPhpValueCount > 0) { + phpValueIdentifierFindings.push({ + file: path.relative(cwd, filePath), + count: srcPhpValueCount, + }) + } + + const inlineNullishAliasCount = ( + sourceText.match(/\btype\s+[A-Za-z0-9_]+\s*=\s*\{\}\s*\|\s*null\s*\|\s*undefined\b/g) || [] + ).length + srcPhpInlineNullishAliasCount += inlineNullishAliasCount + if (inlineNullishAliasCount > 0) { + inlineNullishAliasFindings.push({ + file: path.relative(cwd, filePath), + count: inlineNullishAliasCount, + }) + } + + const srcPhpUnknownCount = (sourceText.match(/\bunknown\b/g) || []).length + srcPhpUnknownKeywordCount += srcPhpUnknownCount + const srcPhpMixedCount = (sourceText.match(/\bPhpMixed\b/g) || []).length + srcPhpPhpMixedKeywordCount += srcPhpMixedCount + if (srcPhpMixedCount > 0) { + phpMixedFindings.push({ + file: path.relative(cwd, filePath), + count: srcPhpMixedCount, + }) + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}array${path.sep}`)) { + srcPhpArrayUnknownKeywordCount += srcPhpUnknownCount + srcPhpArrayPhpMixedKeywordCount += srcPhpMixedCount + if (srcPhpMixedCount > 0) { + arrayPhpMixedFindings.push({ + file: path.relative(cwd, filePath), + count: srcPhpMixedCount, + }) + } + const arrayAsExpressionCount = countAsExpressions(sourceFile) + srcPhpArrayAsExpressionCount += arrayAsExpressionCount + if (arrayAsExpressionCount > 0) { + arrayAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: arrayAsExpressionCount, + }) + } + + const arrayExportedSortFlagStringParamCount = countExportedStringSortFlagParams(sourceFile) + srcPhpArrayExportedSortFlagStringParamCount += arrayExportedSortFlagStringParamCount + if (arrayExportedSortFlagStringParamCount > 0) { + arrayExportedSortFlagStringParamFindings.push({ + file: path.relative(cwd, filePath), + count: arrayExportedSortFlagStringParamCount, + }) + } + + const arrayExportedModeCaseStringParamCount = countExportedStringModeCaseParams(sourceFile) + srcPhpArrayExportedModeCaseStringParamCount += arrayExportedModeCaseStringParamCount + if (arrayExportedModeCaseStringParamCount > 0) { + arrayExportedModeCaseStringParamFindings.push({ + file: path.relative(cwd, filePath), + count: arrayExportedModeCaseStringParamCount, + }) + } + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}var${path.sep}`)) { + srcPhpVarUnknownKeywordCount += srcPhpUnknownCount + srcPhpVarPhpMixedKeywordCount += srcPhpMixedCount + if (srcPhpMixedCount > 0) { + varPhpMixedFindings.push({ + file: path.relative(cwd, filePath), + count: srcPhpMixedCount, + }) + } + const varAsExpressionCount = countAsExpressions(sourceFile) + srcPhpVarAsExpressionCount += varAsExpressionCount + if (varAsExpressionCount > 0) { + varAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: varAsExpressionCount, + }) + } + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}strings${path.sep}`)) { + srcPhpStringsPhpMixedKeywordCount += srcPhpMixedCount + if (srcPhpMixedCount > 0) { + stringsPhpMixedFindings.push({ + file: path.relative(cwd, filePath), + count: srcPhpMixedCount, + }) + } + const stringsAsExpressionCount = countAsExpressions(sourceFile) + srcPhpStringsAsExpressionCount += stringsAsExpressionCount + if (stringsAsExpressionCount > 0) { + stringsAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: stringsAsExpressionCount, + }) + } + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}ctype${path.sep}`)) { + const ctypeAsExpressionCount = countAsExpressions(sourceFile) + srcPhpCtypeAsExpressionCount += ctypeAsExpressionCount + if (ctypeAsExpressionCount > 0) { + ctypeAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: ctypeAsExpressionCount, + }) + } + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}info${path.sep}`)) { + const infoAsExpressionCount = countAsExpressions(sourceFile) + srcPhpInfoAsExpressionCount += infoAsExpressionCount + if (infoAsExpressionCount > 0) { + infoAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: infoAsExpressionCount, + }) + } + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}_helpers${path.sep}`)) { + const helpersAsExpressionCount = countAsExpressions(sourceFile) + srcPhpHelpersAsExpressionCount += helpersAsExpressionCount + if (helpersAsExpressionCount > 0) { + helpersAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: helpersAsExpressionCount, + }) + } + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}url${path.sep}`)) { + const urlAsExpressionCount = countAsExpressions(sourceFile) + srcPhpUrlAsExpressionCount += urlAsExpressionCount + if (urlAsExpressionCount > 0) { + urlAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: urlAsExpressionCount, + }) + } + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}funchand${path.sep}`)) { + const funchandAsExpressionCount = countAsExpressions(sourceFile) + srcPhpFunchandAsExpressionCount += funchandAsExpressionCount + if (funchandAsExpressionCount > 0) { + funchandAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: funchandAsExpressionCount, + }) + } + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}json${path.sep}`)) { + const jsonAsExpressionCount = countAsExpressions(sourceFile) + srcPhpJsonAsExpressionCount += jsonAsExpressionCount + if (jsonAsExpressionCount > 0) { + jsonAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: jsonAsExpressionCount, + }) + } + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}datetime${path.sep}`)) { + const datetimeAsExpressionCount = countAsExpressions(sourceFile) + srcPhpDatetimeAsExpressionCount += datetimeAsExpressionCount + if (datetimeAsExpressionCount > 0) { + datetimeAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: datetimeAsExpressionCount, + }) + } + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}bc${path.sep}`)) { + const bcAsExpressionCount = countAsExpressions(sourceFile) + srcPhpBcAsExpressionCount += bcAsExpressionCount + if (bcAsExpressionCount > 0) { + bcAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: bcAsExpressionCount, + }) + } + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}filesystem${path.sep}`)) { + const filesystemAsExpressionCount = countAsExpressions(sourceFile) + srcPhpFilesystemAsExpressionCount += filesystemAsExpressionCount + if (filesystemAsExpressionCount > 0) { + filesystemAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: filesystemAsExpressionCount, + }) + } + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}misc${path.sep}`)) { + const miscAsExpressionCount = countAsExpressions(sourceFile) + srcPhpMiscAsExpressionCount += miscAsExpressionCount + if (miscAsExpressionCount > 0) { + miscAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: miscAsExpressionCount, + }) + } + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}pcre${path.sep}`)) { + const pcreAsExpressionCount = countAsExpressions(sourceFile) + srcPhpPcreAsExpressionCount += pcreAsExpressionCount + if (pcreAsExpressionCount > 0) { + pcreAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: pcreAsExpressionCount, + }) + } + } + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}xdiff${path.sep}`)) { + const xdiffAsExpressionCount = countAsExpressions(sourceFile) + srcPhpXdiffAsExpressionCount += xdiffAsExpressionCount + if (xdiffAsExpressionCount > 0) { + xdiffAsExpressionFindings.push({ + file: path.relative(cwd, filePath), + count: xdiffAsExpressionCount, + }) + } + } + + const rawIndexSignatureUnknownCount = (sourceText.match(/\{\s*\[\s*key\s*:\s*string\s*]\s*:\s*unknown\s*}/g) || []) + .length + srcPhpRawIndexSignatureUnknownCount += rawIndexSignatureUnknownCount + + const localPhpValueAliasCount = filePath.endsWith( + `${path.sep}src${path.sep}php${path.sep}_helpers${path.sep}_phpTypes.ts`, + ) + ? 0 + : (sourceText.match(/\btype\s+PhpValue\s*=\s*\{\}\s*\|\s*null\s*\|\s*undefined\b/g) || []).length + srcPhpLocalPhpValueAliasCount += localPhpValueAliasCount + if (localPhpValueAliasCount > 0) { + localPhpValueAliasFindings.push({ + file: path.relative(cwd, filePath), + count: localPhpValueAliasCount, + }) + } + + const localPhpInputAliasOutsideHelpersCount = filePath.includes( + `${path.sep}src${path.sep}php${path.sep}_helpers${path.sep}`, + ) + ? 0 + : (sourceText.match(/^type\s+[A-Za-z0-9_]+\s*=\s*PhpInput\s*$/gm) || []).length + srcPhpLocalPhpInputAliasOutsideHelpersCount += localPhpInputAliasOutsideHelpersCount + if (localPhpInputAliasOutsideHelpersCount > 0) { + localPhpInputAliasOutsideHelpersFindings.push({ + file: path.relative(cwd, filePath), + count: localPhpInputAliasOutsideHelpersCount, + }) + } + + const directIniGlobalReadCount = (sourceText.match(/\$locutus\?\.\s*php\?\.\s*ini\b/g) || []).length + srcPhpDirectIniGlobalReadCount += directIniGlobalReadCount + if (directIniGlobalReadCount > 0) { + directIniGlobalReadFindings.push({ + file: path.relative(cwd, filePath), + count: directIniGlobalReadCount, + }) + } + + const argumentsIdentifierCount = countArgumentsIdentifiers(sourceFile) + if (argumentsIdentifierCount > 0) { + argumentsIdentifierFindings.push({ + file: path.relative(cwd, filePath), + count: argumentsIdentifierCount, + }) + } + + const exportedUnknownReturnTypeCount = countExportedUnknownReturnTypes(sourceFile) + srcPhpExportedUnknownReturnTypeCount += exportedUnknownReturnTypeCount + if (exportedUnknownReturnTypeCount > 0) { + exportedUnknownReturnTypeFindings.push({ + file: path.relative(cwd, filePath), + count: exportedUnknownReturnTypeCount, + }) + } + + const exportedTypeBroadCounts = countExportedTypeBroadMarkers(sourceFile) + + srcPhpExportedObjectKeywordCount += exportedTypeBroadCounts.objectKeywordCount + if (exportedTypeBroadCounts.objectKeywordCount > 0) { + exportedObjectKeywordFindings.push({ + file: path.relative(cwd, filePath), + count: exportedTypeBroadCounts.objectKeywordCount, + }) + } + + srcPhpExportedEmptyObjectTypeCount += exportedTypeBroadCounts.emptyObjectTypeCount + if (exportedTypeBroadCounts.emptyObjectTypeCount > 0) { + exportedEmptyObjectTypeFindings.push({ + file: path.relative(cwd, filePath), + count: exportedTypeBroadCounts.emptyObjectTypeCount, + }) + } + + srcPhpExportedFunctionWithoutReturnTypeCount += exportedTypeBroadCounts.functionWithoutReturnTypeCount + if (exportedTypeBroadCounts.functionWithoutReturnTypeCount > 0) { + exportedFunctionWithoutReturnTypeFindings.push({ + file: path.relative(cwd, filePath), + count: exportedTypeBroadCounts.functionWithoutReturnTypeCount, + }) + } + + srcPhpExportedPhpValueIdentifierCount += exportedTypeBroadCounts.phpValueIdentifierCount + if (exportedTypeBroadCounts.phpValueIdentifierCount > 0) { + exportedPhpValueIdentifierFindings.push({ + file: path.relative(cwd, filePath), + count: exportedTypeBroadCounts.phpValueIdentifierCount, + }) + if (filePath.includes(`${path.sep}src${path.sep}php${path.sep}array${path.sep}`)) { + srcPhpArrayExportedPhpValueIdentifierCount += exportedTypeBroadCounts.phpValueIdentifierCount + arrayExportedPhpValueIdentifierFindings.push({ + file: path.relative(cwd, filePath), + count: exportedTypeBroadCounts.phpValueIdentifierCount, + }) + } + } + + const isHelpersFile = filePath.includes(`${path.sep}src${path.sep}php${path.sep}_helpers${path.sep}`) + if (!isHelpersFile) { + srcPhpExportedPhpInputOutsideHelpersCount += exportedTypeBroadCounts.phpInputIdentifierCount + if (exportedTypeBroadCounts.phpInputIdentifierCount > 0) { + exportedPhpInputOutsideHelpersFindings.push({ + file: path.relative(cwd, filePath), + count: exportedTypeBroadCounts.phpInputIdentifierCount, + }) + } + } + } +} + +let hasFailure = false + +if (tsNoCheckFiles.length > 0) { + hasFailure = true + console.error(`Forbidden @ts-nocheck directives found in ${tsNoCheckFiles.length} file(s):`) + for (const file of tsNoCheckFiles) { + console.error(` - ${file}`) + } +} + +if (tsExpectErrorFindings.length > 0) { + hasFailure = true + const total = tsExpectErrorFindings.reduce((sum, finding) => sum + finding.count, 0) + console.error(`Forbidden @ts-expect-error directives found: ${total}`) + for (const finding of tsExpectErrorFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (tsIgnoreFindings.length > 0) { + hasFailure = true + const total = tsIgnoreFindings.reduce((sum, finding) => sum + finding.count, 0) + console.error(`Forbidden @ts-ignore directives found: ${total}`) + for (const finding of tsIgnoreFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (recordStringUnknownFindings.length > 0) { + hasFailure = true + const total = recordStringUnknownFindings.reduce((sum, finding) => sum + finding.count, 0) + console.error(`Forbidden Record<string, unknown> usages found: ${total}`) + for (const finding of recordStringUnknownFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (functionTypeFindings.length > 0) { + hasFailure = true + const total = functionTypeFindings.reduce((sum, finding) => sum + finding.count, 0) + console.error(`Forbidden Function type usages found: ${total}`) + for (const finding of functionTypeFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (asUnknownAsFindings.length > 0) { + hasFailure = true + const total = asUnknownAsFindings.reduce((sum, finding) => sum + finding.count, 0) + console.error(`Forbidden 'as unknown as' casts found: ${total}`) + for (const finding of asUnknownAsFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (argumentsIdentifierFindings.length > 0) { + hasFailure = true + const total = argumentsIdentifierFindings.reduce((sum, finding) => sum + finding.count, 0) + console.error(`Forbidden 'arguments' identifier usages in src/php found: ${total}`) + for (const finding of argumentsIdentifierFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpRawIndexSignatureUnknownCount > MAX_SRC_PHP_RAW_INDEX_SIGNATURE_UNKNOWN) { + hasFailure = true + console.error( + `src/php raw '{ [key: string]: unknown }' count increased: ${srcPhpRawIndexSignatureUnknownCount} > ${MAX_SRC_PHP_RAW_INDEX_SIGNATURE_UNKNOWN}`, + ) +} + +if (srcPhpExportedUnknownReturnTypeCount > MAX_SRC_PHP_EXPORTED_UNKNOWN_RETURN_TYPES) { + hasFailure = true + console.error( + `src/php exported unknown return-type count increased: ${srcPhpExportedUnknownReturnTypeCount} > ${MAX_SRC_PHP_EXPORTED_UNKNOWN_RETURN_TYPES}`, + ) + for (const finding of exportedUnknownReturnTypeFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpExportedObjectKeywordCount > MAX_SRC_PHP_EXPORTED_OBJECT_KEYWORD) { + hasFailure = true + console.error( + `src/php exported signature 'object' keyword count increased: ${srcPhpExportedObjectKeywordCount} > ${MAX_SRC_PHP_EXPORTED_OBJECT_KEYWORD}`, + ) + for (const finding of exportedObjectKeywordFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpExportedEmptyObjectTypeCount > MAX_SRC_PHP_EXPORTED_EMPTY_OBJECT_TYPE) { + hasFailure = true + console.error( + `src/php exported signature empty-object '{}' count increased: ${srcPhpExportedEmptyObjectTypeCount} > ${MAX_SRC_PHP_EXPORTED_EMPTY_OBJECT_TYPE}`, + ) + for (const finding of exportedEmptyObjectTypeFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpExportedFunctionWithoutReturnTypeCount > MAX_SRC_PHP_EXPORTED_FUNCTION_WITHOUT_RETURN_TYPE) { + hasFailure = true + console.error( + `src/php exported function-without-return-type count increased: ${srcPhpExportedFunctionWithoutReturnTypeCount} > ${MAX_SRC_PHP_EXPORTED_FUNCTION_WITHOUT_RETURN_TYPE}`, + ) + for (const finding of exportedFunctionWithoutReturnTypeFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcNonPhpExportedFunctionWithoutReturnTypeCount > MAX_SRC_NON_PHP_EXPORTED_FUNCTION_WITHOUT_RETURN_TYPE) { + hasFailure = true + console.error( + `src non-php exported function-without-return-type count increased: ${srcNonPhpExportedFunctionWithoutReturnTypeCount} > ${MAX_SRC_NON_PHP_EXPORTED_FUNCTION_WITHOUT_RETURN_TYPE}`, + ) + for (const finding of nonPhpExportedFunctionWithoutReturnTypeFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcNonPhpLocalNullishAliasCount > MAX_SRC_NON_PHP_LOCAL_NULLISH_ALIAS) { + hasFailure = true + console.error( + `src non-php local 'type X = {} | null | undefined' alias count increased: ${srcNonPhpLocalNullishAliasCount} > ${MAX_SRC_NON_PHP_LOCAL_NULLISH_ALIAS}`, + ) + for (const finding of nonPhpLocalNullishAliasFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcNonPhpLocalObjectAliasCount > MAX_SRC_NON_PHP_LOCAL_OBJECT_ALIAS) { + hasFailure = true + console.error( + `src non-php local 'type X = object' alias count increased: ${srcNonPhpLocalObjectAliasCount} > ${MAX_SRC_NON_PHP_LOCAL_OBJECT_ALIAS}`, + ) + for (const finding of nonPhpLocalObjectAliasFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcNonPhpLocalUnknownAliasCount > MAX_SRC_NON_PHP_LOCAL_UNKNOWN_ALIAS) { + hasFailure = true + console.error( + `src non-php local 'type X = unknown' alias count increased: ${srcNonPhpLocalUnknownAliasCount} > ${MAX_SRC_NON_PHP_LOCAL_UNKNOWN_ALIAS}`, + ) + for (const finding of nonPhpLocalUnknownAliasFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpExportedPhpValueIdentifierCount > MAX_SRC_PHP_EXPORTED_PHPVALUE_IDENTIFIER) { + hasFailure = true + console.error( + `src/php exported signature 'PhpValue' identifier count increased: ${srcPhpExportedPhpValueIdentifierCount} > ${MAX_SRC_PHP_EXPORTED_PHPVALUE_IDENTIFIER}`, + ) + for (const finding of exportedPhpValueIdentifierFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpExportedPhpInputOutsideHelpersCount > MAX_SRC_PHP_EXPORTED_PHPINPUT_OUTSIDE_HELPERS) { + hasFailure = true + console.error( + `src/php exported signature 'PhpInput' identifier count outside _helpers increased: ${srcPhpExportedPhpInputOutsideHelpersCount} > ${MAX_SRC_PHP_EXPORTED_PHPINPUT_OUTSIDE_HELPERS}`, + ) + for (const finding of exportedPhpInputOutsideHelpersFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpArrayExportedPhpValueIdentifierCount > MAX_SRC_PHP_ARRAY_EXPORTED_PHPVALUE_IDENTIFIER) { + hasFailure = true + console.error( + `src/php/array exported signature 'PhpValue' identifier count increased: ${srcPhpArrayExportedPhpValueIdentifierCount} > ${MAX_SRC_PHP_ARRAY_EXPORTED_PHPVALUE_IDENTIFIER}`, + ) + for (const finding of arrayExportedPhpValueIdentifierFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpArrayExportedSortFlagStringParamCount > MAX_SRC_PHP_ARRAY_EXPORTED_SORTFLAG_STRING_PARAM) { + hasFailure = true + console.error( + `src/php/array exported sort-flag string parameter count increased: ${srcPhpArrayExportedSortFlagStringParamCount} > ${MAX_SRC_PHP_ARRAY_EXPORTED_SORTFLAG_STRING_PARAM}`, + ) + for (const finding of arrayExportedSortFlagStringParamFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpArrayExportedModeCaseStringParamCount > MAX_SRC_PHP_ARRAY_EXPORTED_MODE_CASE_STRING_PARAM) { + hasFailure = true + console.error( + `src/php/array exported mode/case string parameter count increased: ${srcPhpArrayExportedModeCaseStringParamCount} > ${MAX_SRC_PHP_ARRAY_EXPORTED_MODE_CASE_STRING_PARAM}`, + ) + for (const finding of arrayExportedModeCaseStringParamFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpUnknownKeywordCount > MAX_SRC_PHP_UNKNOWN_KEYWORD) { + hasFailure = true + console.error( + `src/php 'unknown' keyword count increased: ${srcPhpUnknownKeywordCount} > ${MAX_SRC_PHP_UNKNOWN_KEYWORD}`, + ) +} + +if (srcPhpPhpMixedKeywordCount > MAX_SRC_PHP_PHPMIXED_KEYWORD) { + hasFailure = true + console.error( + `src/php 'PhpMixed' keyword count increased: ${srcPhpPhpMixedKeywordCount} > ${MAX_SRC_PHP_PHPMIXED_KEYWORD}`, + ) + for (const finding of phpMixedFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpArrayUnknownKeywordCount > MAX_SRC_PHP_ARRAY_UNKNOWN_KEYWORD) { + hasFailure = true + console.error( + `src/php/array 'unknown' keyword count increased: ${srcPhpArrayUnknownKeywordCount} > ${MAX_SRC_PHP_ARRAY_UNKNOWN_KEYWORD}`, + ) +} + +if (srcPhpArrayPhpMixedKeywordCount > MAX_SRC_PHP_ARRAY_PHPMIXED_KEYWORD) { + hasFailure = true + console.error( + `src/php/array 'PhpMixed' keyword count increased: ${srcPhpArrayPhpMixedKeywordCount} > ${MAX_SRC_PHP_ARRAY_PHPMIXED_KEYWORD}`, + ) + for (const finding of arrayPhpMixedFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpArrayAsExpressionCount > MAX_SRC_PHP_ARRAY_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/array 'as' expression count increased: ${srcPhpArrayAsExpressionCount} > ${MAX_SRC_PHP_ARRAY_AS_EXPRESSION}`, + ) + for (const finding of arrayAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpVarUnknownKeywordCount > MAX_SRC_PHP_VAR_UNKNOWN_KEYWORD) { + hasFailure = true + console.error( + `src/php/var 'unknown' keyword count increased: ${srcPhpVarUnknownKeywordCount} > ${MAX_SRC_PHP_VAR_UNKNOWN_KEYWORD}`, + ) +} + +if (srcPhpVarPhpMixedKeywordCount > MAX_SRC_PHP_VAR_PHPMIXED_KEYWORD) { + hasFailure = true + console.error( + `src/php/var 'PhpMixed' keyword count increased: ${srcPhpVarPhpMixedKeywordCount} > ${MAX_SRC_PHP_VAR_PHPMIXED_KEYWORD}`, + ) + for (const finding of varPhpMixedFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpVarAsExpressionCount > MAX_SRC_PHP_VAR_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/var 'as' expression count increased: ${srcPhpVarAsExpressionCount} > ${MAX_SRC_PHP_VAR_AS_EXPRESSION}`, + ) + for (const finding of varAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpStringsAsExpressionCount > MAX_SRC_PHP_STRINGS_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/strings 'as' expression count increased: ${srcPhpStringsAsExpressionCount} > ${MAX_SRC_PHP_STRINGS_AS_EXPRESSION}`, + ) + for (const finding of stringsAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpStringsPhpMixedKeywordCount > MAX_SRC_PHP_STRINGS_PHPMIXED_KEYWORD) { + hasFailure = true + console.error( + `src/php/strings 'PhpMixed' keyword count increased: ${srcPhpStringsPhpMixedKeywordCount} > ${MAX_SRC_PHP_STRINGS_PHPMIXED_KEYWORD}`, + ) + for (const finding of stringsPhpMixedFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpCtypeAsExpressionCount > MAX_SRC_PHP_CTYPE_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/ctype 'as' expression count increased: ${srcPhpCtypeAsExpressionCount} > ${MAX_SRC_PHP_CTYPE_AS_EXPRESSION}`, + ) + for (const finding of ctypeAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpInfoAsExpressionCount > MAX_SRC_PHP_INFO_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/info 'as' expression count increased: ${srcPhpInfoAsExpressionCount} > ${MAX_SRC_PHP_INFO_AS_EXPRESSION}`, + ) + for (const finding of infoAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpHelpersAsExpressionCount > MAX_SRC_PHP_HELPERS_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/_helpers 'as' expression count increased: ${srcPhpHelpersAsExpressionCount} > ${MAX_SRC_PHP_HELPERS_AS_EXPRESSION}`, + ) + for (const finding of helpersAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpUrlAsExpressionCount > MAX_SRC_PHP_URL_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/url 'as' expression count increased: ${srcPhpUrlAsExpressionCount} > ${MAX_SRC_PHP_URL_AS_EXPRESSION}`, + ) + for (const finding of urlAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpFunchandAsExpressionCount > MAX_SRC_PHP_FUNCHAND_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/funchand 'as' expression count increased: ${srcPhpFunchandAsExpressionCount} > ${MAX_SRC_PHP_FUNCHAND_AS_EXPRESSION}`, + ) + for (const finding of funchandAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpJsonAsExpressionCount > MAX_SRC_PHP_JSON_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/json 'as' expression count increased: ${srcPhpJsonAsExpressionCount} > ${MAX_SRC_PHP_JSON_AS_EXPRESSION}`, + ) + for (const finding of jsonAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpDatetimeAsExpressionCount > MAX_SRC_PHP_DATETIME_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/datetime 'as' expression count increased: ${srcPhpDatetimeAsExpressionCount} > ${MAX_SRC_PHP_DATETIME_AS_EXPRESSION}`, + ) + for (const finding of datetimeAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpBcAsExpressionCount > MAX_SRC_PHP_BC_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/bc 'as' expression count increased: ${srcPhpBcAsExpressionCount} > ${MAX_SRC_PHP_BC_AS_EXPRESSION}`, + ) + for (const finding of bcAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpFilesystemAsExpressionCount > MAX_SRC_PHP_FILESYSTEM_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/filesystem 'as' expression count increased: ${srcPhpFilesystemAsExpressionCount} > ${MAX_SRC_PHP_FILESYSTEM_AS_EXPRESSION}`, + ) + for (const finding of filesystemAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpMiscAsExpressionCount > MAX_SRC_PHP_MISC_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/misc 'as' expression count increased: ${srcPhpMiscAsExpressionCount} > ${MAX_SRC_PHP_MISC_AS_EXPRESSION}`, + ) + for (const finding of miscAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpPcreAsExpressionCount > MAX_SRC_PHP_PCRE_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/pcre 'as' expression count increased: ${srcPhpPcreAsExpressionCount} > ${MAX_SRC_PHP_PCRE_AS_EXPRESSION}`, + ) + for (const finding of pcreAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpXdiffAsExpressionCount > MAX_SRC_PHP_XDIFF_AS_EXPRESSION) { + hasFailure = true + console.error( + `src/php/xdiff 'as' expression count increased: ${srcPhpXdiffAsExpressionCount} > ${MAX_SRC_PHP_XDIFF_AS_EXPRESSION}`, + ) + for (const finding of xdiffAsExpressionFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpLocalPhpValueAliasCount > MAX_SRC_PHP_LOCAL_PHPVALUE_ALIAS) { + hasFailure = true + console.error( + `src/php local 'type PhpValue = {} | null | undefined' alias count increased: ${srcPhpLocalPhpValueAliasCount} > ${MAX_SRC_PHP_LOCAL_PHPVALUE_ALIAS}`, + ) + for (const finding of localPhpValueAliasFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpLocalPhpInputAliasOutsideHelpersCount > MAX_SRC_PHP_LOCAL_PHPINPUT_ALIAS_OUTSIDE_HELPERS) { + hasFailure = true + console.error( + `src/php local 'type X = PhpInput' alias count outside _helpers increased: ${srcPhpLocalPhpInputAliasOutsideHelpersCount} > ${MAX_SRC_PHP_LOCAL_PHPINPUT_ALIAS_OUTSIDE_HELPERS}`, + ) + for (const finding of localPhpInputAliasOutsideHelpersFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpDirectIniGlobalReadCount > MAX_SRC_PHP_DIRECT_INI_GLOBAL_READS) { + hasFailure = true + console.error( + `src/php direct '$locutus?.php?.ini' read count increased: ${srcPhpDirectIniGlobalReadCount} > ${MAX_SRC_PHP_DIRECT_INI_GLOBAL_READS}`, + ) + for (const finding of directIniGlobalReadFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpPhpValueIdentifierCount > MAX_SRC_PHP_PHPVALUE_IDENTIFIER) { + hasFailure = true + console.error( + `src/php 'PhpValue' identifier count increased: ${srcPhpPhpValueIdentifierCount} > ${MAX_SRC_PHP_PHPVALUE_IDENTIFIER}`, + ) + for (const finding of phpValueIdentifierFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpInlineNullishAliasCount > MAX_SRC_PHP_INLINE_NULLISH_ALIAS) { + hasFailure = true + console.error( + `src/php inline '{}' | null | undefined alias count increased: ${srcPhpInlineNullishAliasCount} > ${MAX_SRC_PHP_INLINE_NULLISH_ALIAS}`, + ) + for (const finding of inlineNullishAliasFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpReflectGetSetCount > MAX_SRC_PHP_REFLECT_GET_SET) { + hasFailure = true + console.error(`src/php Reflect.get/set count increased: ${srcPhpReflectGetSetCount} > ${MAX_SRC_PHP_REFLECT_GET_SET}`) + for (const finding of reflectGetSetFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (srcPhpGlobalThisIdentifierCount > MAX_SRC_PHP_GLOBALTHIS_IDENTIFIER) { + hasFailure = true + console.error( + `src/php globalThis identifier count increased: ${srcPhpGlobalThisIdentifierCount} > ${MAX_SRC_PHP_GLOBALTHIS_IDENTIFIER}`, + ) + for (const finding of globalThisIdentifierFindings) { + console.error(` - ${finding.file}: ${finding.count}`) + } +} + +if (hasFailure) { + process.exit(1) +} + +console.log( + 'ts debt policy ok: @ts-nocheck 0, @ts-ignore 0, @ts-expect-error 0, Function type 0, Record<string, unknown> 0, as unknown as 0, src/php arguments 0, src/php raw index-signature unknown not increased, src/php exported unknown return-types not increased, src/php exported object keyword count not increased, src/php exported empty-object count not increased, src/php exported missing return-type count not increased, src non-php exported missing return-type count not increased, src non-php local nullish/object alias count not increased, src non-php local object alias count not increased, src non-php local unknown alias count not increased, src/php exported PhpValue identifier count not increased, src/php exported PhpInput identifier count outside _helpers not increased, src/php/array exported PhpValue identifier count not increased, src/php/array exported sort-flag string parameter count not increased, src/php/array exported mode/case string parameter count not increased, src/php unknown keyword count not increased, src/php PhpMixed keyword count not increased, src/php/array unknown keyword count not increased, src/php/array PhpMixed keyword count not increased, src/php/array as-expression count not increased, src/php/var unknown keyword count not increased, src/php/var PhpMixed keyword count not increased, src/php/var as-expression count not increased, src/php/strings as-expression count not increased, src/php/strings PhpMixed keyword count not increased, src/php/ctype as-expression count not increased, src/php/info as-expression count not increased, src/php/_helpers as-expression count not increased, src/php/url as-expression count not increased, src/php/funchand as-expression count not increased, src/php/json as-expression count not increased, src/php/datetime as-expression count not increased, src/php/bc as-expression count not increased, src/php/filesystem as-expression count not increased, src/php/misc as-expression count not increased, src/php/pcre as-expression count not increased, src/php/xdiff as-expression count not increased, src/php local PhpValue alias count not increased, src/php local PhpInput alias count outside _helpers not increased, src/php direct $locutus?.php?.ini reads not increased, src/php PhpValue identifier count not increased, src/php inline nullish alias count not increased, src/php Reflect.get/set count not increased, src/php globalThis identifier count not increased', +)
scripts/check-type-contracts-snapshot.ts+329 −0 added@@ -0,0 +1,329 @@ +import fs from 'node:fs' +import path from 'node:path' +import ts from 'typescript' + +type LiteralParamContract = { + invalidLiteral: number | string + validLiteral: number | string +} + +type PrimitiveBoundaryContract = { + rejectBoolean: boolean + rejectNumber: boolean + rejectString: boolean +} + +const cwd = process.cwd() +const shouldUpdate = process.argv.includes('--update') +const outputPath = path.join(cwd, 'test', 'util', 'type-contracts.generated.d.ts') +const srcDir = path.join(cwd, 'src') + +const hasExportModifier = (node: ts.Node): boolean => + !!node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) + +const getConfigPath = (): string => ts.findConfigFile(cwd, ts.sys.fileExists, 'tsconfig.json') || '' +const getStableHashId = (value: string): string => { + let hash = 0 + for (const char of value) { + hash = (hash * 31 + char.charCodeAt(0)) >>> 0 + } + + return hash.toString(36) +} + +const getTypeLiteralContract = (parameterType: ts.Type): LiteralParamContract | null => { + const typeMembers = parameterType.isUnion() ? parameterType.types : [parameterType] + + const stringLiterals: string[] = [] + const numberLiterals: number[] = [] + let hasBroadString = false + let hasBroadNumber = false + + for (const member of typeMembers) { + if (member.flags & ts.TypeFlags.String) { + hasBroadString = true + continue + } + + if (member.flags & ts.TypeFlags.Number) { + hasBroadNumber = true + continue + } + + if (member.isStringLiteral()) { + stringLiterals.push(member.value) + continue + } + + if (member.isNumberLiteral()) { + numberLiterals.push(member.value) + } + } + + if (!hasBroadString && stringLiterals.length > 0) { + const sentinelBase = '__LOCUTUS_INVALID_LITERAL__' + const invalidLiteral = stringLiterals.includes(sentinelBase) ? `${sentinelBase}_X` : sentinelBase + return { validLiteral: stringLiterals[0], invalidLiteral } + } + + if (!hasBroadNumber && numberLiterals.length > 0) { + const sentinelBase = -9007199254740991 + const invalidLiteral = numberLiterals.includes(sentinelBase) ? sentinelBase + 1 : sentinelBase + return { validLiteral: numberLiterals[0], invalidLiteral } + } + + return null +} + +const getPrimitiveBoundaryContract = (parameterType: ts.Type): PrimitiveBoundaryContract | null => { + const typeMembers = parameterType.isUnion() ? parameterType.types : [parameterType] + let hasBroadBoolean = false + let hasBroadNumber = false + let hasBroadString = false + + for (const member of typeMembers) { + if (member.flags & ts.TypeFlags.Boolean) { + hasBroadBoolean = true + continue + } + if (member.flags & ts.TypeFlags.Number) { + hasBroadNumber = true + continue + } + if (member.flags & ts.TypeFlags.String) { + hasBroadString = true + } + } + + const contract: PrimitiveBoundaryContract = { + rejectBoolean: !hasBroadBoolean && (hasBroadNumber || hasBroadString), + rejectNumber: !hasBroadNumber && (hasBroadBoolean || hasBroadString), + rejectString: !hasBroadString && (hasBroadBoolean || hasBroadNumber), + } + + if (!contract.rejectBoolean && !contract.rejectNumber && !contract.rejectString) { + return null + } + + return contract +} + +const configPath = getConfigPath() +if (!configPath) { + console.error('Unable to find tsconfig.json for type contract generation.') + process.exit(1) +} + +const parsedConfig = ts.getParsedCommandLineOfConfigFile( + configPath, + {}, + { + ...ts.sys, + onUnRecoverableConfigFileDiagnostic: (diagnostic) => { + const host = { + getCanonicalFileName: (fileName: string) => fileName, + getCurrentDirectory: () => cwd, + getNewLine: () => '\n', + } + console.error(ts.formatDiagnostic(diagnostic, host)) + process.exit(1) + }, + }, +) + +if (!parsedConfig) { + console.error('Failed to parse tsconfig.json for type contract generation.') + process.exit(1) +} + +const program = ts.createProgram({ + rootNames: parsedConfig.fileNames, + options: parsedConfig.options, +}) +const checker = program.getTypeChecker() + +const importEntries: Array<{ importPath: string; line: string }> = [] +const contractLines: string[] = [] +const sourceFiles = program + .getSourceFiles() + .filter((sourceFile) => !sourceFile.isDeclarationFile) + .filter((sourceFile) => sourceFile.fileName.startsWith(srcDir)) + .filter((sourceFile) => !sourceFile.fileName.includes(`${path.sep}src${path.sep}_util${path.sep}`)) + .sort((left, right) => (left.fileName < right.fileName ? -1 : left.fileName > right.fileName ? 1 : 0)) + +let functionIndex = 0 +const usedModuleAliases = new Set<string>() + +for (const sourceFile of sourceFiles) { + const relativeSourcePath = path.relative(cwd, sourceFile.fileName).replaceAll(path.sep, '/') + const importPath = `../../${relativeSourcePath}` + const functionDeclarations = sourceFile.statements.filter( + (node): node is ts.FunctionDeclaration => + ts.isFunctionDeclaration(node) && hasExportModifier(node) && !!node.name && !!node.body, + ) + + if (functionDeclarations.length === 0) { + continue + } + + const baseAlias = `m_${getStableHashId(relativeSourcePath)}` + let moduleAlias = baseAlias + let aliasSuffix = 1 + while (usedModuleAliases.has(moduleAlias)) { + moduleAlias = `${baseAlias}_${aliasSuffix}` + aliasSuffix += 1 + } + usedModuleAliases.add(moduleAlias) + importEntries.push({ + importPath, + line: `import type * as ${moduleAlias} from '${importPath}'`, + }) + + for (const node of functionDeclarations) { + if (!node.name) { + continue + } + + const functionName = node.name.text + const contractId = String(functionIndex).padStart(4, '0') + const contractPrefix = `_TypeContract_${contractId}` + functionIndex += 1 + + const functionRef = `${moduleAlias}.${functionName}` + contractLines.push(`type ${contractPrefix}_ReturnNotAny = ExpectFalse<IsAny<ReturnType<typeof ${functionRef}>>>`) + + const functionType = checker.getTypeAtLocation(node.name) + const callSignatures = checker.getSignaturesOfType(functionType, ts.SignatureKind.Call) + const primarySignature = callSignatures[callSignatures.length - 1] + if (!primarySignature) { + continue + } + + primarySignature.parameters.forEach((parameterSymbol, parameterIndex) => { + const paramRef = `Parameters<typeof ${functionRef}>[${parameterIndex}]` + contractLines.push(`type ${contractPrefix}_Param${parameterIndex}_NotAny = ExpectFalse<IsAny<${paramRef}>>`) + + const declaration = parameterSymbol.valueDeclaration + if (declaration && ts.isParameter(declaration) && !declaration.dotDotDotToken) { + const isOptionalParam = Boolean(declaration.questionToken || declaration.initializer) + if (isOptionalParam) { + contractLines.push( + `type ${contractPrefix}_Param${parameterIndex}_OptionalAcceptsUndefined = ExpectTrue<IsAssignable<undefined, ${paramRef}>>`, + ) + } + } + + const parameterType = checker.getTypeOfSymbolAtLocation(parameterSymbol, node.name) + const literalContract = getTypeLiteralContract(parameterType) + if (literalContract) { + const validLiteralText = + typeof literalContract.validLiteral === 'string' + ? `'${literalContract.validLiteral.replaceAll("'", "\\'")}'` + : `${literalContract.validLiteral}` + const invalidLiteralText = + typeof literalContract.invalidLiteral === 'string' + ? `'${literalContract.invalidLiteral.replaceAll("'", "\\'")}'` + : `${literalContract.invalidLiteral}` + + contractLines.push( + `type ${contractPrefix}_Param${parameterIndex}_AcceptsKnownLiteral = ExpectTrue<IsAssignable<${validLiteralText}, ${paramRef}>>`, + ) + contractLines.push( + `type ${contractPrefix}_Param${parameterIndex}_RejectsInvalidLiteral = ExpectFalse<IsAssignable<${invalidLiteralText}, ${paramRef}>>`, + ) + } + + const primitiveContract = getPrimitiveBoundaryContract(parameterType) + if (!primitiveContract) { + return + } + + if (primitiveContract.rejectString) { + contractLines.push( + `type ${contractPrefix}_Param${parameterIndex}_RejectsString = ExpectFalse<IsAssignable<'__LOCUTUS_INVALID_STRING__', ${paramRef}>>`, + ) + } + if (primitiveContract.rejectNumber) { + contractLines.push( + `type ${contractPrefix}_Param${parameterIndex}_RejectsNumber = ExpectFalse<IsAssignable<123456789, ${paramRef}>>`, + ) + } + if (primitiveContract.rejectBoolean) { + contractLines.push( + `type ${contractPrefix}_Param${parameterIndex}_RejectsBoolean = ExpectFalse<IsAssignable<true, ${paramRef}>>`, + ) + } + }) + } +} + +const header = [ + '// This file is generated by scripts/check-type-contracts-snapshot.ts', + '// Do not edit manually. Run: corepack yarn fix:type:contracts', + '', + 'type IsAny<T> = 0 extends 1 & T ? true : false', + 'type IsAssignable<From, To> = [From] extends [To] ? true : false', + 'type ExpectTrue<T extends true> = T', + 'type ExpectFalse<T extends false> = T', + '', +] +const sortedImportLines = [...importEntries] + .sort((left, right) => { + const leftKey = left.importPath.toLowerCase() + const rightKey = right.importPath.toLowerCase() + if (leftKey < rightKey) { + return -1 + } + if (leftKey > rightKey) { + return 1 + } + + return left.importPath < right.importPath ? -1 : left.importPath > right.importPath ? 1 : 0 + }) + .map((entry) => entry.line) +const content = [...header, ...sortedImportLines, '', ...contractLines, ''].join('\n') +const normalizeForComparison = (value: string): string => { + const lines = value + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.length > 0) + const importLines = lines.filter((line) => line.startsWith('import type * as ')) + const otherLines = lines.filter((line) => !line.startsWith('import type * as ')) + const normalizedImports = importLines.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0)) + return [...normalizedImports, ...otherLines].join('\n').replaceAll(/\s+/g, ' ').trim() +} + +if (shouldUpdate) { + fs.writeFileSync(outputPath, content) + console.log(`Updated type contract snapshot at ${path.relative(cwd, outputPath)}`) + process.exit(0) +} + +if (!fs.existsSync(outputPath)) { + console.error(`Type contract snapshot file missing: ${path.relative(cwd, outputPath)}`) + console.error('Run: corepack yarn fix:type:contracts') + process.exit(1) +} + +const expected = fs.readFileSync(outputPath, 'utf8') +if (normalizeForComparison(expected) !== normalizeForComparison(content)) { + const expectedLines = expected.split('\n') + const currentLines = content.split('\n') + let firstDiffIndex = 0 + + while ( + firstDiffIndex < expectedLines.length && + firstDiffIndex < currentLines.length && + expectedLines[firstDiffIndex] === currentLines[firstDiffIndex] + ) { + firstDiffIndex += 1 + } + + console.error(`Type contract snapshot mismatch at line ${firstDiffIndex + 1}`) + console.error(`Expected: ${expectedLines[firstDiffIndex] ?? '<EOF>'}`) + console.error(`Current: ${currentLines[firstDiffIndex] ?? '<EOF>'}`) + console.error('If this change is intentional, run: corepack yarn fix:type:contracts') + process.exit(1) +} + +console.log(`Type contract snapshot is up to date (${path.relative(cwd, outputPath)})`)
scripts/compare-definitelytyped-signatures.ts+486 −0 added@@ -0,0 +1,486 @@ +import fs from 'node:fs' +import path from 'node:path' +import ts from 'typescript' + +type SignatureSet = { + modulePath: string + exportName: string + signatures: string[] +} + +type SignatureMetrics = { + anyCount: number + unknownCount: number + functionCount: number + objectCount: number + optionalParamCount: number + restAnyCount: number +} + +type DifferenceKind = 'ours-stricter' | 'dt-stricter' | 'inconclusive' + +type Difference = { + key: string + modulePath: string + exportName: string + ours: string[] + dt: string[] + oursMetrics: SignatureMetrics + dtMetrics: SignatureMetrics + kind: DifferenceKind +} + +type NpmRegistryMetadata = { + 'dist-tags'?: { + latest?: string + } +} + +const cwd = process.cwd() +const srcDir = path.join(cwd, 'src') +const customDtIndexArg = process.argv.find((arg) => arg.startsWith('--dt-index=')) +const maxArg = process.argv.find((arg) => arg.startsWith('--max=')) +const maxDiffOutput = maxArg ? Number.parseInt(maxArg.slice('--max='.length), 10) : 25 +const reportPathArg = process.argv.find((arg) => arg.startsWith('--report=')) + +const normalizeWhitespace = (value: string): string => value.replace(/\s+/g, ' ').trim() + +const sortUnique = (items: string[]): string[] => [...new Set(items)].sort() + +const createMetric = (signatures: string[]): SignatureMetrics => { + const combined = signatures.join(' ') + + return { + anyCount: (combined.match(/\bany\b/g) ?? []).length, + unknownCount: (combined.match(/\bunknown\b/g) ?? []).length, + functionCount: (combined.match(/\bFunction\b/g) ?? []).length, + objectCount: (combined.match(/:\s*object\b/g) ?? []).length, + optionalParamCount: (combined.match(/\?:/g) ?? []).length, + restAnyCount: (combined.match(/\.\.\.\w+:\s*any\[\]/g) ?? []).length, + } +} + +const metricPenalty = (metrics: SignatureMetrics): number => + metrics.anyCount * 100 + + metrics.unknownCount * 25 + + metrics.restAnyCount * 25 + + metrics.functionCount * 15 + + metrics.objectCount * 10 + + metrics.optionalParamCount + +const classifyDifference = (ours: SignatureMetrics, dt: SignatureMetrics): DifferenceKind => { + const oursPenalty = metricPenalty(ours) + const dtPenalty = metricPenalty(dt) + + if (oursPenalty < dtPenalty) { + return 'ours-stricter' + } + + if (oursPenalty > dtPenalty) { + return 'dt-stricter' + } + + return 'inconclusive' +} + +const getTsConfigPath = (): string => ts.findConfigFile(cwd, ts.sys.fileExists, 'tsconfig.json') || '' + +const collectLocalSignatures = (): Map<string, SignatureSet> => { + const configPath = getTsConfigPath() + if (!configPath) { + throw new Error('Unable to find tsconfig.json') + } + + const parsedConfig = ts.getParsedCommandLineOfConfigFile( + configPath, + {}, + { + ...ts.sys, + onUnRecoverableConfigFileDiagnostic: (diagnostic) => { + const host = { + getCanonicalFileName: (fileName: string) => fileName, + getCurrentDirectory: () => cwd, + getNewLine: () => '\n', + } + throw new Error(ts.formatDiagnostic(diagnostic, host)) + }, + }, + ) + + if (!parsedConfig) { + throw new Error('Failed to parse tsconfig.json') + } + + const program = ts.createProgram({ + rootNames: parsedConfig.fileNames, + options: parsedConfig.options, + }) + const checker = program.getTypeChecker() + const results = new Map<string, SignatureSet>() + + for (const sourceFile of program.getSourceFiles()) { + if (sourceFile.isDeclarationFile) { + continue + } + if (!sourceFile.fileName.startsWith(srcDir)) { + continue + } + if (sourceFile.fileName.includes(`${path.sep}src${path.sep}_util${path.sep}`)) { + continue + } + + const moduleSymbol = checker.getSymbolAtLocation(sourceFile) + if (!moduleSymbol) { + continue + } + + const modulePath = `locutus/${path + .relative(srcDir, sourceFile.fileName) + .replaceAll(path.sep, '/') + .replace(/\.ts$/, '')}` + + for (const exportSymbol of checker.getExportsOfModule(moduleSymbol)) { + const declarations = exportSymbol.getDeclarations() + if (!declarations || declarations.length === 0) { + continue + } + + const declarationForType = declarations[declarations.length - 1] + const exportType = checker.getTypeOfSymbolAtLocation(exportSymbol, declarationForType) + const signatures = checker.getSignaturesOfType(exportType, ts.SignatureKind.Call) + if (signatures.length === 0) { + continue + } + + const signatureTexts = sortUnique( + signatures.map((signature) => + normalizeWhitespace( + checker.signatureToString(signature, declarationForType, ts.TypeFormatFlags.NoTruncation), + ), + ), + ) + + const exportName = exportSymbol.getName() + const key = `${modulePath}::${exportName}` + results.set(key, { + modulePath, + exportName, + signatures: signatureTexts, + }) + } + } + + return results +} + +const readLocalDtIndex = (): string | null => { + if (customDtIndexArg) { + const customPath = customDtIndexArg.slice('--dt-index='.length) + const resolvedPath = path.resolve(cwd, customPath) + if (!fs.existsSync(resolvedPath)) { + throw new Error(`--dt-index path does not exist: ${resolvedPath}`) + } + return fs.readFileSync(resolvedPath, 'utf8') + } + + const bundledPath = path.join(cwd, 'node_modules', '@types', 'locutus', 'index.d.ts') + if (fs.existsSync(bundledPath)) { + return fs.readFileSync(bundledPath, 'utf8') + } + + return null +} + +const fetchLatestDtIndex = async (): Promise<{ version: string; body: string }> => { + const registryResponse = await fetch('https://registry.npmjs.org/@types/locutus') + if (!registryResponse.ok) { + throw new Error(`Failed to read npm metadata for @types/locutus: HTTP ${registryResponse.status}`) + } + + const metadata = (await registryResponse.json()) as NpmRegistryMetadata + const latest = metadata['dist-tags']?.latest + if (!latest) { + throw new Error('Could not determine latest @types/locutus version from npm metadata.') + } + + const indexResponse = await fetch(`https://unpkg.com/@types/locutus@${latest}/index.d.ts`) + if (!indexResponse.ok) { + throw new Error(`Failed to fetch @types/locutus@${latest} index.d.ts: HTTP ${indexResponse.status}`) + } + + const body = await indexResponse.text() + return { version: latest, body } +} + +const getModuleBlock = (moduleDeclaration: ts.ModuleDeclaration): ts.ModuleBlock | null => { + let current: ts.ModuleBody | undefined = moduleDeclaration.body + while (current && ts.isModuleDeclaration(current)) { + current = current.body + } + + if (current && ts.isModuleBlock(current)) { + return current + } + + return null +} + +const formatDtSignature = (sourceFile: ts.SourceFile, declaration: ts.FunctionDeclaration): string => { + const typeParameters = declaration.typeParameters + ? `<${declaration.typeParameters.map((typeParameter) => normalizeWhitespace(typeParameter.getText(sourceFile))).join(', ')}>` + : '' + + const parameters = declaration.parameters + .map((parameter) => { + const name = parameter.name.getText(sourceFile) + const prefix = parameter.dotDotDotToken ? '...' : '' + const optional = parameter.questionToken ? '?' : '' + const typeText = parameter.type ? normalizeWhitespace(parameter.type.getText(sourceFile)) : 'any' + return `${prefix}${name}${optional}: ${typeText}` + }) + .join(', ') + + const returnType = declaration.type ? normalizeWhitespace(declaration.type.getText(sourceFile)) : 'any' + return `${typeParameters}(${parameters}): ${returnType}` +} + +const collectDtSignatures = (dtSourceText: string): Map<string, SignatureSet> => { + const sourceFile = ts.createSourceFile('definitelytyped-index.d.ts', dtSourceText, ts.ScriptTarget.Latest, true) + const results = new Map<string, SignatureSet>() + + for (const statement of sourceFile.statements) { + if (!ts.isModuleDeclaration(statement)) { + continue + } + if (!ts.isStringLiteral(statement.name)) { + continue + } + + const modulePath = statement.name.text + const moduleBlock = getModuleBlock(statement) + if (!moduleBlock) { + continue + } + + for (const moduleStatement of moduleBlock.statements) { + if (!ts.isFunctionDeclaration(moduleStatement) || !moduleStatement.name) { + continue + } + + const exportName = moduleStatement.name.text + const key = `${modulePath}::${exportName}` + const previous = results.get(key) + const signatureText = formatDtSignature(sourceFile, moduleStatement) + const signatures = previous ? sortUnique([...previous.signatures, signatureText]) : [signatureText] + + results.set(key, { + modulePath, + exportName, + signatures, + }) + } + } + + return results +} + +const toComparableSignature = (signature: string): string => + normalizeWhitespace(signature) + .replace(/<[^>]*>/g, '') + .replace(/\s+/g, ' ') + .trim() + +const compareSignatureSets = (left: string[], right: string[]): boolean => { + const normalizedLeft = sortUnique(left.map(toComparableSignature)) + const normalizedRight = sortUnique(right.map(toComparableSignature)) + if (normalizedLeft.length !== normalizedRight.length) { + return false + } + + for (let index = 0; index < normalizedLeft.length; index += 1) { + if (normalizedLeft[index] !== normalizedRight[index]) { + return false + } + } + + return true +} + +const toReport = ( + differences: Difference[], + missingInLocal: SignatureSet[], + missingInDt: SignatureSet[], + equalCount: number, + sharedCount: number, + dtSourceLabel: string, +): string => { + const lines: string[] = [] + lines.push('# DefinitelyTyped Signature Comparison') + lines.push('') + lines.push(`- Compared against: ${dtSourceLabel}`) + lines.push(`- Shared exports compared: ${sharedCount}`) + lines.push(`- Signature-identical exports: ${equalCount}`) + lines.push(`- Different exports: ${differences.length}`) + lines.push(`- Missing in local types: ${missingInLocal.length}`) + lines.push(`- Missing in DefinitelyTyped: ${missingInDt.length}`) + lines.push('') + + const oursStricter = differences.filter((difference) => difference.kind === 'ours-stricter') + const dtStricter = differences.filter((difference) => difference.kind === 'dt-stricter') + const inconclusive = differences.filter((difference) => difference.kind === 'inconclusive') + + lines.push('## Difference Breakdown') + lines.push('') + lines.push(`- Ours stricter: ${oursStricter.length}`) + lines.push(`- DT stricter: ${dtStricter.length}`) + lines.push(`- Inconclusive: ${inconclusive.length}`) + lines.push('') + + const appendDiffGroup = (title: string, list: Difference[]): void => { + lines.push(`## ${title}`) + lines.push('') + if (list.length === 0) { + lines.push('- None') + lines.push('') + return + } + for (const item of list) { + lines.push(`- \`${item.modulePath}\` / \`${item.exportName}\``) + lines.push(` ours: ${item.ours.join(' || ')}`) + lines.push(` dt: ${item.dt.join(' || ')}`) + } + lines.push('') + } + + appendDiffGroup('DT Stricter (Review First)', dtStricter) + appendDiffGroup('Inconclusive', inconclusive) + + lines.push('## Missing In Local') + lines.push('') + if (missingInLocal.length === 0) { + lines.push('- None') + } else { + for (const item of missingInLocal) { + lines.push(`- \`${item.modulePath}\` / \`${item.exportName}\``) + } + } + lines.push('') + + lines.push('## Missing In DefinitelyTyped') + lines.push('') + if (missingInDt.length === 0) { + lines.push('- None') + } else { + for (const item of missingInDt) { + lines.push(`- \`${item.modulePath}\` / \`${item.exportName}\``) + } + } + lines.push('') + + return lines.join('\n') +} + +const printDifferenceGroup = (title: string, list: Difference[]): void => { + console.log(`\n${title}: ${list.length}`) + const limited = list.slice(0, Math.max(0, maxDiffOutput)) + for (const item of limited) { + console.log(`- ${item.modulePath} :: ${item.exportName}`) + console.log(` ours: ${item.ours.join(' || ')}`) + console.log(` dt: ${item.dt.join(' || ')}`) + } + if (list.length > limited.length) { + console.log(` ... and ${list.length - limited.length} more`) + } +} + +const main = async (): Promise<void> => { + const localSignatures = collectLocalSignatures() + const localDtIndex = readLocalDtIndex() + + let dtSourceLabel = 'local node_modules/@types/locutus' + let dtSourceText = localDtIndex + + if (!dtSourceText) { + const fetched = await fetchLatestDtIndex() + dtSourceLabel = `@types/locutus@${fetched.version} (fetched from npm/unpkg)` + dtSourceText = fetched.body + } + + const dtSignatures = collectDtSignatures(dtSourceText) + + const localKeys = new Set(localSignatures.keys()) + const dtKeys = new Set(dtSignatures.keys()) + const sharedKeys = [...localKeys].filter((key) => dtKeys.has(key)).sort() + + let equalCount = 0 + const differences: Difference[] = [] + + for (const key of sharedKeys) { + const ours = localSignatures.get(key) + const dt = dtSignatures.get(key) + if (!ours || !dt) { + continue + } + + if (compareSignatureSets(ours.signatures, dt.signatures)) { + equalCount += 1 + continue + } + + const oursMetrics = createMetric(ours.signatures) + const dtMetrics = createMetric(dt.signatures) + + differences.push({ + key, + modulePath: ours.modulePath, + exportName: ours.exportName, + ours: ours.signatures, + dt: dt.signatures, + oursMetrics, + dtMetrics, + kind: classifyDifference(oursMetrics, dtMetrics), + }) + } + + const missingInLocal = [...dtKeys] + .filter((key) => !localKeys.has(key)) + .sort() + .map((key) => dtSignatures.get(key)) + .filter((value): value is SignatureSet => Boolean(value)) + + const missingInDt = [...localKeys] + .filter((key) => !dtKeys.has(key)) + .sort() + .map((key) => localSignatures.get(key)) + .filter((value): value is SignatureSet => Boolean(value)) + + const oursStricter = differences.filter((difference) => difference.kind === 'ours-stricter') + const dtStricter = differences.filter((difference) => difference.kind === 'dt-stricter') + const inconclusive = differences.filter((difference) => difference.kind === 'inconclusive') + + console.log(`Compared against ${dtSourceLabel}`) + console.log(`Shared exports: ${sharedKeys.length}`) + console.log(`Signature-identical: ${equalCount}`) + console.log(`Different signatures: ${differences.length}`) + console.log(`Missing in local: ${missingInLocal.length}`) + console.log(`Missing in DefinitelyTyped: ${missingInDt.length}`) + console.log(`Likely ours stricter: ${oursStricter.length}`) + console.log(`Likely DT stricter: ${dtStricter.length}`) + console.log(`Inconclusive: ${inconclusive.length}`) + + printDifferenceGroup('Likely DT stricter', dtStricter) + printDifferenceGroup('Inconclusive', inconclusive) + + if (reportPathArg) { + const reportPath = path.resolve(cwd, reportPathArg.slice('--report='.length)) + const reportBody = toReport(differences, missingInLocal, missingInDt, equalCount, sharedKeys.length, dtSourceLabel) + fs.writeFileSync(reportPath, reportBody) + console.log(`\nWrote report: ${path.relative(cwd, reportPath)}`) + } +} + +main().catch((error) => { + const message = error instanceof Error ? error.message : String(error) + console.error(`compare-definitelytyped-signatures failed: ${message}`) + process.exit(1) +})
scripts/fix-cjs-exports.ts+94 −0 added@@ -0,0 +1,94 @@ +/** + * Post-process dist output for publication while preserving named exports. + * + * We intentionally do not collapse CJS modules into direct callable defaults. + * Consumers should use named imports/exports in both ESM and CJS: + * - ESM: import { fn } from '...' + * - CJS: const { fn } = require('...') + */ + +import fs from 'node:fs' +import path from 'node:path' + +const distDir = process.argv[2] || 'dist' +const writeGolangIndexCompatShim = (rootDir: string): void => { + const cjsStringsDir = path.join(rootDir, 'golang', 'strings') + const cjsIndex2JsPath = path.join(cjsStringsDir, 'Index2.js') + const cjsIndexJsPath = path.join(cjsStringsDir, 'Index.js') + const cjsIndex2DtsPath = path.join(cjsStringsDir, 'Index2.d.ts') + const cjsIndexDtsPath = path.join(cjsStringsDir, 'Index.d.ts') + + if (fs.existsSync(cjsIndex2JsPath) && !fs.existsSync(cjsIndexJsPath)) { + const cjsShim = + "// Backward-compatible deep import shim for 'locutus/golang/strings/Index'.\n" + + "const mod = require('./Index2.js');\n" + + 'exports.Index = mod.Index;\n' + fs.writeFileSync(cjsIndexJsPath, cjsShim, 'utf-8') + } + + if (fs.existsSync(cjsIndex2DtsPath) && !fs.existsSync(cjsIndexDtsPath)) { + fs.writeFileSync(cjsIndexDtsPath, "export { Index } from './Index2'\n", 'utf-8') + } + + const esmStringsDir = path.join(rootDir, 'esm', 'golang', 'strings') + const esmIndex2JsPath = path.join(esmStringsDir, 'Index2.js') + const esmIndexJsPath = path.join(esmStringsDir, 'Index.js') + if (fs.existsSync(esmIndex2JsPath) && !fs.existsSync(esmIndexJsPath)) { + const esmShim = + "// Backward-compatible deep import shim for 'locutus/golang/strings/Index'.\n" + + "export { Index } from './Index2.js'\n" + fs.writeFileSync(esmIndexJsPath, esmShim, 'utf-8') + } +} + +const writeEsmPackageJson = (rootDir: string): void => { + const esmDir = path.join(rootDir, 'esm') + if (!fs.existsSync(esmDir)) { + return + } + + const esmPackageJsonPath = path.join(esmDir, 'package.json') + const esmPackageJson = { type: 'module' } + fs.writeFileSync(esmPackageJsonPath, `${JSON.stringify(esmPackageJson, null, 2)}\n`, 'utf-8') +} + +const patchDistPackageJson = (rootDir: string): void => { + const packageJsonPath = path.join(rootDir, 'package.json') + if (!fs.existsSync(packageJsonPath)) { + return + } + + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as { + exports?: Record<string, unknown> + type?: string + types?: string + module?: string + main?: string + } + + packageJson.type = 'commonjs' + packageJson.main = './index.js' + packageJson.module = './esm/index.js' + packageJson.types = './index.d.ts' + packageJson.exports = { + '.': { + types: './index.d.ts', + import: './esm/index.js', + require: './index.js', + default: './index.js', + }, + './*': { + types: './*.d.ts', + import: './esm/*.js', + require: './*.js', + default: './*.js', + }, + './package.json': './package.json', + } + + fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf-8') +} + +writeGolangIndexCompatShim(distDir) +writeEsmPackageJson(distDir) +patchDistPackageJson(distDir)
src/awk/builtin/cos.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function cos(x) { - // discuss at: https://locutus.io/awk/cos/ - // parity verified: GNU AWK 5.3 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: cos(0) - // returns 1: 1 - // example 2: cos(1) - // returns 2: 0.5403023058681398 - - return Math.cos(x) -}
src/awk/builtin/cos.ts+11 −0 added@@ -0,0 +1,11 @@ +export function cos(x: number): number { + // discuss at: https://locutus.io/awk/cos/ + // parity verified: GNU AWK 5.3 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: cos(0) + // returns 1: 1 + // example 2: cos(1) + // returns 2: 0.5403023058681398 + + return Math.cos(x) +}
src/awk/builtin/exp.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function exp(x) { - // discuss at: https://locutus.io/awk/exp/ - // parity verified: GNU AWK 5.3 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: exp(0) - // returns 1: 1 - // example 2: exp(1) - // returns 2: 2.718281828459045 - - return Math.exp(x) -}
src/awk/builtin/exp.ts+11 −0 added@@ -0,0 +1,11 @@ +export function exp(x: number): number { + // discuss at: https://locutus.io/awk/exp/ + // parity verified: GNU AWK 5.3 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: exp(0) + // returns 1: 1 + // example 2: exp(1) + // returns 2: 2.718281828459045 + + return Math.exp(x) +}
src/awk/builtin/index.js+0 −10 removed@@ -1,10 +0,0 @@ -module.exports.cos = require('./cos') -module.exports.exp = require('./exp') -module.exports.int = require('./int') -module.exports.length = require('./length') -module.exports.log = require('./log') -module.exports.sin = require('./sin') -module.exports.sqrt = require('./sqrt') -module.exports.substr = require('./substr') -module.exports.tolower = require('./tolower') -module.exports.toupper = require('./toupper')
src/awk/builtin/index.ts+10 −0 added@@ -0,0 +1,10 @@ +export { cos } from './cos.ts' +export { exp } from './exp.ts' +export { int } from './int.ts' +export { length } from './length.ts' +export { log } from './log.ts' +export { sin } from './sin.ts' +export { sqrt } from './sqrt.ts' +export { substr } from './substr.ts' +export { tolower } from './tolower.ts' +export { toupper } from './toupper.ts'
src/awk/builtin/int.js+0 −13 removed@@ -1,13 +0,0 @@ -module.exports = function int(x) { - // discuss at: https://locutus.io/awk/int/ - // parity verified: GNU AWK 5.3 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: int(4.7) - // returns 1: 4 - // example 2: int(-4.7) - // returns 2: -4 - // example 3: int(3) - // returns 3: 3 - - return Math.trunc(x) -}
src/awk/builtin/int.ts+13 −0 added@@ -0,0 +1,13 @@ +export function int(x: number): number { + // discuss at: https://locutus.io/awk/int/ + // parity verified: GNU AWK 5.3 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: int(4.7) + // returns 1: 4 + // example 2: int(-4.7) + // returns 2: -4 + // example 3: int(3) + // returns 3: 3 + + return Math.trunc(x) +}
src/awk/builtin/length.js+0 −16 removed@@ -1,16 +0,0 @@ -module.exports = function length(s) { - // discuss at: https://locutus.io/awk/length/ - // parity verified: GNU AWK 5.3 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: length('hello') - // returns 1: 5 - // example 2: length('') - // returns 2: 0 - // example 3: length('test string') - // returns 3: 11 - - if (s === undefined || s === null) { - return 0 - } - return String(s).length -}
src/awk/builtin/length.ts+16 −0 added@@ -0,0 +1,16 @@ +export function length(s: string): number { + // discuss at: https://locutus.io/awk/length/ + // parity verified: GNU AWK 5.3 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: length('hello') + // returns 1: 5 + // example 2: length('') + // returns 2: 0 + // example 3: length('test string') + // returns 3: 11 + + if (s === undefined || s === null) { + return 0 + } + return String(s).length +}
src/awk/builtin/log.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function log(x) { - // discuss at: https://locutus.io/awk/log/ - // parity verified: GNU AWK 5.3 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: log(1) - // returns 1: 0 - // example 2: log(2.718281828459045) - // returns 2: 1 - - return Math.log(x) -}
src/awk/builtin/log.ts+11 −0 added@@ -0,0 +1,11 @@ +export function log(x: number): number { + // discuss at: https://locutus.io/awk/log/ + // parity verified: GNU AWK 5.3 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: log(1) + // returns 1: 0 + // example 2: log(2.718281828459045) + // returns 2: 1 + + return Math.log(x) +}
src/awk/builtin/sin.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function sin(x) { - // discuss at: https://locutus.io/awk/sin/ - // parity verified: GNU AWK 5.3 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: sin(0) - // returns 1: 0 - // example 2: sin(1) - // returns 2: 0.8414709848078965 - - return Math.sin(x) -}
src/awk/builtin/sin.ts+11 −0 added@@ -0,0 +1,11 @@ +export function sin(x: number): number { + // discuss at: https://locutus.io/awk/sin/ + // parity verified: GNU AWK 5.3 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: sin(0) + // returns 1: 0 + // example 2: sin(1) + // returns 2: 0.8414709848078965 + + return Math.sin(x) +}
src/awk/builtin/sqrt.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function sqrt(x) { - // discuss at: https://locutus.io/awk/sqrt/ - // parity verified: GNU AWK 5.3 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: sqrt(16) - // returns 1: 4 - // example 2: sqrt(2) - // returns 2: 1.4142135623730951 - - return Math.sqrt(x) -}
src/awk/builtin/sqrt.ts+11 −0 added@@ -0,0 +1,11 @@ +export function sqrt(x: number): number { + // discuss at: https://locutus.io/awk/sqrt/ + // parity verified: GNU AWK 5.3 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: sqrt(16) + // returns 1: 4 + // example 2: sqrt(2) + // returns 2: 1.4142135623730951 + + return Math.sqrt(x) +}
src/awk/builtin/substr.js+0 −23 removed@@ -1,23 +0,0 @@ -module.exports = function substr(str, start, len) { - // discuss at: https://locutus.io/awk/substr/ - // parity verified: GNU AWK 5.3 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: AWK uses 1-based indexing, so start=1 is the first character - // example 1: substr('hello', 2, 3) - // returns 1: 'ell' - // example 2: substr('hello', 1, 2) - // returns 2: 'he' - // example 3: substr('hello', 3) - // returns 3: 'llo' - - if (str === undefined || str === null) { - return '' - } - const s = String(str) - // AWK uses 1-based indexing - const startIdx = Math.max(0, start - 1) - if (len === undefined) { - return s.substring(startIdx) - } - return s.substring(startIdx, startIdx + len) -}
src/awk/builtin/substr.ts+23 −0 added@@ -0,0 +1,23 @@ +export function substr(str: string, start: number, len?: number): string { + // discuss at: https://locutus.io/awk/substr/ + // parity verified: GNU AWK 5.3 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: AWK uses 1-based indexing, so start=1 is the first character + // example 1: substr('hello', 2, 3) + // returns 1: 'ell' + // example 2: substr('hello', 1, 2) + // returns 2: 'he' + // example 3: substr('hello', 3) + // returns 3: 'llo' + + if (str === undefined || str === null) { + return '' + } + const s = String(str) + // AWK uses 1-based indexing + const startIdx = Math.max(0, start - 1) + if (len === undefined) { + return s.substring(startIdx) + } + return s.substring(startIdx, startIdx + len) +}
src/awk/builtin/tolower.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function tolower(s) { - // discuss at: https://locutus.io/awk/tolower/ - // parity verified: GNU AWK 5.3 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: tolower('HELLO') - // returns 1: 'hello' - // example 2: tolower('Hello World') - // returns 2: 'hello world' - - return String(s).toLowerCase() -}
src/awk/builtin/tolower.ts+11 −0 added@@ -0,0 +1,11 @@ +export function tolower(s: string): string { + // discuss at: https://locutus.io/awk/tolower/ + // parity verified: GNU AWK 5.3 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: tolower('HELLO') + // returns 1: 'hello' + // example 2: tolower('Hello World') + // returns 2: 'hello world' + + return String(s).toLowerCase() +}
src/awk/builtin/toupper.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function toupper(s) { - // discuss at: https://locutus.io/awk/toupper/ - // parity verified: GNU AWK 5.3 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: toupper('hello') - // returns 1: 'HELLO' - // example 2: toupper('Hello World') - // returns 2: 'HELLO WORLD' - - return String(s).toUpperCase() -}
src/awk/builtin/toupper.ts+11 −0 added@@ -0,0 +1,11 @@ +export function toupper(s: string): string { + // discuss at: https://locutus.io/awk/toupper/ + // parity verified: GNU AWK 5.3 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: toupper('hello') + // returns 1: 'HELLO' + // example 2: toupper('Hello World') + // returns 2: 'HELLO WORLD' + + return String(s).toUpperCase() +}
src/c/ctype/index.js+0 −8 removed@@ -1,8 +0,0 @@ -module.exports.isalnum = require('./isalnum') -module.exports.isalpha = require('./isalpha') -module.exports.isdigit = require('./isdigit') -module.exports.islower = require('./islower') -module.exports.isspace = require('./isspace') -module.exports.isupper = require('./isupper') -module.exports.tolower = require('./tolower') -module.exports.toupper = require('./toupper')
src/c/ctype/index.ts+8 −0 added@@ -0,0 +1,8 @@ +export { isalnum } from './isalnum.ts' +export { isalpha } from './isalpha.ts' +export { isdigit } from './isdigit.ts' +export { islower } from './islower.ts' +export { isspace } from './isspace.ts' +export { isupper } from './isupper.ts' +export { tolower } from './tolower.ts' +export { toupper } from './toupper.ts'
src/c/ctype/isalnum.js+0 −15 removed@@ -1,15 +0,0 @@ -module.exports = function isalnum(c) { - // discuss at: https://locutus.io/c/ctype/isalnum/ - // parity verified: C 23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Checks if the character is alphanumeric (A-Z, a-z, or 0-9). - // example 1: isalnum('A') - // returns 1: true - // example 2: isalnum('5') - // returns 2: true - // example 3: isalnum('_') - // returns 3: false - - c = (c + '').charAt(0) - return /^[A-Za-z0-9]$/.test(c) -}
src/c/ctype/isalnum.ts+15 −0 added@@ -0,0 +1,15 @@ +export function isalnum(c: string): boolean { + // discuss at: https://locutus.io/c/ctype/isalnum/ + // parity verified: C 23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Checks if the character is alphanumeric (A-Z, a-z, or 0-9). + // example 1: isalnum('A') + // returns 1: true + // example 2: isalnum('5') + // returns 2: true + // example 3: isalnum('_') + // returns 3: false + + c = (c + '').charAt(0) + return /^[A-Za-z0-9]$/.test(c) +}
src/c/ctype/isalpha.js+0 −17 removed@@ -1,17 +0,0 @@ -module.exports = function isalpha(c) { - // discuss at: https://locutus.io/c/ctype/isalpha/ - // parity verified: C 23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Checks if the character is an alphabetic letter (A-Z or a-z). - // example 1: isalpha('A') - // returns 1: true - // example 2: isalpha('z') - // returns 2: true - // example 3: isalpha('5') - // returns 3: false - // example 4: isalpha(' ') - // returns 4: false - - c = (c + '').charAt(0) - return /^[A-Za-z]$/.test(c) -}
src/c/ctype/isalpha.ts+17 −0 added@@ -0,0 +1,17 @@ +export function isalpha(c: string): boolean { + // discuss at: https://locutus.io/c/ctype/isalpha/ + // parity verified: C 23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Checks if the character is an alphabetic letter (A-Z or a-z). + // example 1: isalpha('A') + // returns 1: true + // example 2: isalpha('z') + // returns 2: true + // example 3: isalpha('5') + // returns 3: false + // example 4: isalpha(' ') + // returns 4: false + + c = (c + '').charAt(0) + return /^[A-Za-z]$/.test(c) +}
src/c/ctype/isdigit.js+0 −15 removed@@ -1,15 +0,0 @@ -module.exports = function isdigit(c) { - // discuss at: https://locutus.io/c/ctype/isdigit/ - // parity verified: C 23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Checks if the character is a decimal digit (0-9). - // example 1: isdigit('5') - // returns 1: true - // example 2: isdigit('0') - // returns 2: true - // example 3: isdigit('A') - // returns 3: false - - c = (c + '').charAt(0) - return /^[0-9]$/.test(c) -}
src/c/ctype/isdigit.ts+15 −0 added@@ -0,0 +1,15 @@ +export function isdigit(c: string): boolean { + // discuss at: https://locutus.io/c/ctype/isdigit/ + // parity verified: C 23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Checks if the character is a decimal digit (0-9). + // example 1: isdigit('5') + // returns 1: true + // example 2: isdigit('0') + // returns 2: true + // example 3: isdigit('A') + // returns 3: false + + c = (c + '').charAt(0) + return /^[0-9]$/.test(c) +}
src/c/ctype/islower.js+0 −15 removed@@ -1,15 +0,0 @@ -module.exports = function islower(c) { - // discuss at: https://locutus.io/c/ctype/islower/ - // parity verified: C 23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Checks if the character is a lowercase letter (a-z). - // example 1: islower('a') - // returns 1: true - // example 2: islower('A') - // returns 2: false - // example 3: islower('5') - // returns 3: false - - c = (c + '').charAt(0) - return /^[a-z]$/.test(c) -}
src/c/ctype/islower.ts+15 −0 added@@ -0,0 +1,15 @@ +export function islower(c: string): boolean { + // discuss at: https://locutus.io/c/ctype/islower/ + // parity verified: C 23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Checks if the character is a lowercase letter (a-z). + // example 1: islower('a') + // returns 1: true + // example 2: islower('A') + // returns 2: false + // example 3: islower('5') + // returns 3: false + + c = (c + '').charAt(0) + return /^[a-z]$/.test(c) +}
src/c/ctype/isspace.js+0 −15 removed@@ -1,15 +0,0 @@ -module.exports = function isspace(c) { - // discuss at: https://locutus.io/c/ctype/isspace/ - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Checks if the character is a whitespace character. - // note 1: Includes space, tab, newline, vertical tab, form feed, carriage return. - // example 1: isspace(' ') - // returns 1: true - // example 2: isspace('\t') - // returns 2: true - // example 3: isspace('a') - // returns 3: false - - c = (c + '').charAt(0) - return /^[\s]$/.test(c) -}
src/c/ctype/isspace.ts+15 −0 added@@ -0,0 +1,15 @@ +export function isspace(c: string): boolean { + // discuss at: https://locutus.io/c/ctype/isspace/ + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Checks if the character is a whitespace character. + // note 1: Includes space, tab, newline, vertical tab, form feed, carriage return. + // example 1: isspace(' ') + // returns 1: true + // example 2: isspace('\t') + // returns 2: true + // example 3: isspace('a') + // returns 3: false + + c = (c + '').charAt(0) + return /^[\s]$/.test(c) +}
src/c/ctype/isupper.js+0 −15 removed@@ -1,15 +0,0 @@ -module.exports = function isupper(c) { - // discuss at: https://locutus.io/c/ctype/isupper/ - // parity verified: C 23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Checks if the character is an uppercase letter (A-Z). - // example 1: isupper('A') - // returns 1: true - // example 2: isupper('a') - // returns 2: false - // example 3: isupper('5') - // returns 3: false - - c = (c + '').charAt(0) - return /^[A-Z]$/.test(c) -}
src/c/ctype/isupper.ts+15 −0 added@@ -0,0 +1,15 @@ +export function isupper(c: string): boolean { + // discuss at: https://locutus.io/c/ctype/isupper/ + // parity verified: C 23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Checks if the character is an uppercase letter (A-Z). + // example 1: isupper('A') + // returns 1: true + // example 2: isupper('a') + // returns 2: false + // example 3: isupper('5') + // returns 3: false + + c = (c + '').charAt(0) + return /^[A-Z]$/.test(c) +}
src/c/ctype/tolower.js+0 −15 removed@@ -1,15 +0,0 @@ -module.exports = function tolower(c) { - // discuss at: https://locutus.io/c/ctype/tolower/ - // parity verified: C 23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Converts an uppercase letter to lowercase. - // example 1: tolower('A') - // returns 1: 'a' - // example 2: tolower('a') - // returns 2: 'a' - // example 3: tolower('5') - // returns 3: '5' - - c = (c + '').charAt(0) - return c.toLowerCase() -}
src/c/ctype/tolower.ts+15 −0 added@@ -0,0 +1,15 @@ +export function tolower(c: string): string { + // discuss at: https://locutus.io/c/ctype/tolower/ + // parity verified: C 23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Converts an uppercase letter to lowercase. + // example 1: tolower('A') + // returns 1: 'a' + // example 2: tolower('a') + // returns 2: 'a' + // example 3: tolower('5') + // returns 3: '5' + + c = (c + '').charAt(0) + return c.toLowerCase() +}
src/c/ctype/toupper.js+0 −15 removed@@ -1,15 +0,0 @@ -module.exports = function toupper(c) { - // discuss at: https://locutus.io/c/ctype/toupper/ - // parity verified: C 23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Converts a lowercase letter to uppercase. - // example 1: toupper('a') - // returns 1: 'A' - // example 2: toupper('A') - // returns 2: 'A' - // example 3: toupper('5') - // returns 3: '5' - - c = (c + '').charAt(0) - return c.toUpperCase() -}
src/c/ctype/toupper.ts+15 −0 added@@ -0,0 +1,15 @@ +export function toupper(c: string): string { + // discuss at: https://locutus.io/c/ctype/toupper/ + // parity verified: C 23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Converts a lowercase letter to uppercase. + // example 1: toupper('a') + // returns 1: 'A' + // example 2: toupper('A') + // returns 2: 'A' + // example 3: toupper('5') + // returns 3: '5' + + c = (c + '').charAt(0) + return c.toUpperCase() +}
src/c/index.js+0 −2 removed@@ -1,2 +0,0 @@ -module.exports.math = require('./math') -module.exports.stdio = require('./stdio')
src/c/index.ts+5 −0 added@@ -0,0 +1,5 @@ +export * as ctype from './ctype/index.ts' +export * as math from './math/index.ts' +export * as stdio from './stdio/index.ts' +export * as stdlib from './stdlib/index.ts' +export * as string from './string/index.ts'
src/clojure/Math/abs.js+0 −13 removed@@ -1,13 +0,0 @@ -module.exports = function abs(x) { - // discuss at: https://locutus.io/clojure/abs/ - // parity verified: Clojure 1.12 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: abs(-5) - // returns 1: 5 - // example 2: abs(3.14) - // returns 2: 3.14 - // example 3: abs(0) - // returns 3: 0 - - return Math.abs(x) -}
src/clojure/Math/abs.ts+13 −0 added@@ -0,0 +1,13 @@ +export function abs(x: number): number { + // discuss at: https://locutus.io/clojure/abs/ + // parity verified: Clojure 1.12 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: abs(-5) + // returns 1: 5 + // example 2: abs(3.14) + // returns 2: 3.14 + // example 3: abs(0) + // returns 3: 0 + + return Math.abs(x) +}
src/clojure/Math/ceil.js+0 −15 removed@@ -1,15 +0,0 @@ -module.exports = function ceil(x) { - // discuss at: https://locutus.io/clojure/ceil/ - // parity verified: Clojure 1.12 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns the smallest integer >= x - // example 1: ceil(4.2) - // returns 1: 5.0 - // example 2: ceil(-0.5) - // returns 2: -0.0 - // example 3: ceil(3) - // returns 3: 3.0 - - // Clojure's Math/ceil returns a double, so we preserve that behavior - return Math.ceil(x) -}
src/clojure/Math/ceil.ts+15 −0 added@@ -0,0 +1,15 @@ +export function ceil(x: number): number { + // discuss at: https://locutus.io/clojure/ceil/ + // parity verified: Clojure 1.12 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns the smallest integer >= x + // example 1: ceil(4.2) + // returns 1: 5.0 + // example 2: ceil(-0.5) + // returns 2: -0.0 + // example 3: ceil(3) + // returns 3: 3.0 + + // Clojure's Math/ceil returns a double, so we preserve that behavior + return Math.ceil(x) +}
src/clojure/Math/floor.js+0 −14 removed@@ -1,14 +0,0 @@ -module.exports = function floor(x) { - // discuss at: https://locutus.io/clojure/floor/ - // parity verified: Clojure 1.12 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns the largest integer <= x - // example 1: floor(4.7) - // returns 1: 4.0 - // example 2: floor(-0.5) - // returns 2: -1.0 - // example 3: floor(3) - // returns 3: 3.0 - - return Math.floor(x) -}
src/clojure/Math/floor.ts+14 −0 added@@ -0,0 +1,14 @@ +export function floor(x: number): number { + // discuss at: https://locutus.io/clojure/floor/ + // parity verified: Clojure 1.12 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns the largest integer <= x + // example 1: floor(4.7) + // returns 1: 4.0 + // example 2: floor(-0.5) + // returns 2: -1.0 + // example 3: floor(3) + // returns 3: 3.0 + + return Math.floor(x) +}
src/clojure/Math/index.js+0 −3 removed@@ -1,3 +0,0 @@ -module.exports.abs = require('./abs') -module.exports.ceil = require('./ceil') -module.exports.floor = require('./floor')
src/clojure/Math/index.ts+3 −0 added@@ -0,0 +1,3 @@ +export { abs } from './abs.ts' +export { ceil } from './ceil.ts' +export { floor } from './floor.ts'
src/clojure/string/blank.js+0 −17 removed@@ -1,17 +0,0 @@ -module.exports = function blank(s) { - // discuss at: https://locutus.io/clojure/blank/ - // parity verified: Clojure 1.12 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Clojure uses blank?, JS uses blank - // example 1: blank('') - // returns 1: true - // example 2: blank(' ') - // returns 2: true - // example 3: blank('hello') - // returns 3: false - - if (s === null || s === undefined) { - return true - } - return String(s).trim().length === 0 -}
src/clojure/string/blank.ts+17 −0 added@@ -0,0 +1,17 @@ +export function blank(s: string): boolean { + // discuss at: https://locutus.io/clojure/blank/ + // parity verified: Clojure 1.12 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Clojure uses blank?, JS uses blank + // example 1: blank('') + // returns 1: true + // example 2: blank(' ') + // returns 2: true + // example 3: blank('hello') + // returns 3: false + + if (s === null || s === undefined) { + return true + } + return String(s).trim().length === 0 +}
src/clojure/string/index.js+0 −5 removed@@ -1,5 +0,0 @@ -module.exports.blank = require('./blank') -module.exports.lower_case = require('./lower_case') -module.exports.reverse = require('./reverse') -module.exports.trim = require('./trim') -module.exports.upper_case = require('./upper_case')
src/clojure/string/index.ts+5 −0 added@@ -0,0 +1,5 @@ +export { blank } from './blank.ts' +export { lower_case } from './lower_case.ts' +export { reverse } from './reverse.ts' +export { trim } from './trim.ts' +export { upper_case } from './upper_case.ts'
src/clojure/string/lower_case.js+0 −12 removed@@ -1,12 +0,0 @@ -module.exports = function lower_case(s) { - // discuss at: https://locutus.io/clojure/lower_case/ - // parity verified: Clojure 1.12 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Clojure uses lower-case, JS uses lower_case - // example 1: lower_case('HELLO') - // returns 1: 'hello' - // example 2: lower_case('Hello World') - // returns 2: 'hello world' - - return String(s).toLowerCase() -}
src/clojure/string/lower_case.ts+12 −0 added@@ -0,0 +1,12 @@ +export function lower_case(s: string): string { + // discuss at: https://locutus.io/clojure/lower_case/ + // parity verified: Clojure 1.12 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Clojure uses lower-case, JS uses lower_case + // example 1: lower_case('HELLO') + // returns 1: 'hello' + // example 2: lower_case('Hello World') + // returns 2: 'hello world' + + return String(s).toLowerCase() +}
src/clojure/string/reverse.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function reverse(s) { - // discuss at: https://locutus.io/clojure/reverse/ - // parity verified: Clojure 1.12 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: reverse('hello') - // returns 1: 'olleh' - // example 2: reverse('abc') - // returns 2: 'cba' - - return String(s).split('').reverse().join('') -}
src/clojure/string/reverse.ts+11 −0 added@@ -0,0 +1,11 @@ +export function reverse(s: string): string { + // discuss at: https://locutus.io/clojure/reverse/ + // parity verified: Clojure 1.12 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: reverse('hello') + // returns 1: 'olleh' + // example 2: reverse('abc') + // returns 2: 'cba' + + return String(s).split('').reverse().join('') +}
src/clojure/string/trim.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function trim(s) { - // discuss at: https://locutus.io/clojure/trim/ - // parity verified: Clojure 1.12 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: trim(' hello ') - // returns 1: 'hello' - // example 2: trim('abc') - // returns 2: 'abc' - - return String(s).trim() -}
src/clojure/string/trim.ts+11 −0 added@@ -0,0 +1,11 @@ +export function trim(s: string): string { + // discuss at: https://locutus.io/clojure/trim/ + // parity verified: Clojure 1.12 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: trim(' hello ') + // returns 1: 'hello' + // example 2: trim('abc') + // returns 2: 'abc' + + return String(s).trim() +}
src/clojure/string/upper_case.js+0 −12 removed@@ -1,12 +0,0 @@ -module.exports = function upper_case(s) { - // discuss at: https://locutus.io/clojure/upper_case/ - // parity verified: Clojure 1.12 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Clojure uses upper-case, JS uses upper_case - // example 1: upper_case('hello') - // returns 1: 'HELLO' - // example 2: upper_case('Hello World') - // returns 2: 'HELLO WORLD' - - return String(s).toUpperCase() -}
src/clojure/string/upper_case.ts+12 −0 added@@ -0,0 +1,12 @@ +export function upper_case(s: string): string { + // discuss at: https://locutus.io/clojure/upper_case/ + // parity verified: Clojure 1.12 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Clojure uses upper-case, JS uses upper_case + // example 1: upper_case('hello') + // returns 1: 'HELLO' + // example 2: upper_case('Hello World') + // returns 2: 'HELLO WORLD' + + return String(s).toUpperCase() +}
src/c/math/abs.js+0 −16 removed@@ -1,16 +0,0 @@ -module.exports = function abs(mixedNumber) { - // discuss at: https://locutus.io/c/abs/ - // parity verified: C 23 - // original by: Waldo Malqui Silva (https://waldo.malqui.info) - // improved by: Karol Kowalski - // improved by: Kevin van Zonneveld (https://kvz.io) - // improved by: Jonas Raoni Soares Silva (https://www.jsfromhell.com) - // example 1: abs(-5) - // returns 1: 5 - // example 2: abs(-42) - // returns 2: 42 - // example 3: abs(0) - // returns 3: 0 - - return Math.abs(mixedNumber) || 0 -}
src/c/math/abs.ts+16 −0 added@@ -0,0 +1,16 @@ +export function abs(mixedNumber: number): number { + // discuss at: https://locutus.io/c/abs/ + // parity verified: C 23 + // original by: Waldo Malqui Silva (https://waldo.malqui.info) + // improved by: Karol Kowalski + // improved by: Kevin van Zonneveld (https://kvz.io) + // improved by: Jonas Raoni Soares Silva (https://www.jsfromhell.com) + // example 1: abs(-5) + // returns 1: 5 + // example 2: abs(-42) + // returns 2: 42 + // example 3: abs(0) + // returns 3: 0 + + return Math.abs(mixedNumber) || 0 +}
src/c/math/frexp.js+0 −74 removed@@ -1,74 +0,0 @@ -module.exports = function frexp(arg) { - // discuss at: https://locutus.io/c/frexp/ - // original by: Oskar Larsson Högfeldt (https://oskar-lh.name/) - // note 1: Instead of - // note 1: double frexp( double arg, int* exp ); - // note 1: this is built as - // note 1: [double, int] frexp( double arg ); - // note 1: due to the lack of pointers in JavaScript. - // note 1: See code comments for further information. - // example 1: frexp(1) - // returns 1: [0.5, 1] - // example 2: frexp(1.5) - // returns 2: [0.75, 1] - // example 3: frexp(3 * Math.pow(2, 500)) - // returns 3: [0.75, 502] - // example 4: frexp(-4) - // returns 4: [-0.5, 3] - // example 5: frexp(Number.MAX_VALUE) - // returns 5: [0.9999999999999999, 1024] - // example 6: frexp(Number.MIN_VALUE) - // returns 6: [0.5, -1073] - // example 7: frexp(-Infinity) - // returns 7: [-Infinity, 0] - // example 8: frexp(-0) - // returns 8: [-0, 0] - // example 9: frexp(NaN) - // returns 9: [NaN, 0] - - // Potential issue with this implementation: - // the precisions of Math.pow and the ** operator are undefined in the ECMAScript standard, - // however, sane implementations should give the same results for Math.pow(2, <integer>) operations - - // Like frexp of C and std::frexp of C++, - // but returns an array instead of using a pointer argument for passing the exponent result. - // Object.is(n, frexp(n)[0] * 2 ** frexp(n)[1]) for all number values of n except when Math.isFinite(n) && Math.abs(n) > 2**1023 - // Object.is(n, (2 * frexp(n)[0]) * 2 ** (frexp(n)[1] - 1)) for all number values of n - // Object.is(n, frexp(n)[0]) for these values of n: 0, -0, NaN, Infinity, -Infinity - // Math.abs(frexp(n)[0]) is >= 0.5 and < 1.0 for any other number-type value of n - // See https://en.cppreference.com/w/c/numeric/math/frexp for a more detailed description - - arg = Number(arg) - - const result = [arg, 0] - - if (arg !== 0 && Number.isFinite(arg)) { - const absArg = Math.abs(arg) - // Math.log2 was introduced in ES2015, use it when available - const log2 = - Math.log2 || - function log2(n) { - return Math.log(n) * Math.LOG2E - } - let exp = Math.max(-1023, Math.floor(log2(absArg)) + 1) - let x = absArg * Math.pow(2, -exp) - - // These while loops compensate for rounding errors that sometimes occur because of ECMAScript's Math.log2's undefined precision - // and also works around the issue of Math.pow(2, -exp) === Infinity when exp <= -1024 - while (x < 0.5) { - x *= 2 - exp-- - } - while (x >= 1) { - x *= 0.5 - exp++ - } - - if (arg < 0) { - x = -x - } - result[0] = x - result[1] = exp - } - return result -}
src/c/math/frexp.ts+74 −0 added@@ -0,0 +1,74 @@ +export function frexp(arg: number): [number, number] { + // discuss at: https://locutus.io/c/frexp/ + // original by: Oskar Larsson Högfeldt (https://oskar-lh.name/) + // note 1: Instead of + // note 1: double frexp( double arg, int* exp ); + // note 1: this is built as + // note 1: [double, int] frexp( double arg ); + // note 1: due to the lack of pointers in JavaScript. + // note 1: See code comments for further information. + // example 1: frexp(1) + // returns 1: [0.5, 1] + // example 2: frexp(1.5) + // returns 2: [0.75, 1] + // example 3: frexp(3 * Math.pow(2, 500)) + // returns 3: [0.75, 502] + // example 4: frexp(-4) + // returns 4: [-0.5, 3] + // example 5: frexp(Number.MAX_VALUE) + // returns 5: [0.9999999999999999, 1024] + // example 6: frexp(Number.MIN_VALUE) + // returns 6: [0.5, -1073] + // example 7: frexp(-Infinity) + // returns 7: [-Infinity, 0] + // example 8: frexp(-0) + // returns 8: [-0, 0] + // example 9: frexp(NaN) + // returns 9: [NaN, 0] + + // Potential issue with this implementation: + // the precisions of Math.pow and the ** operator are undefined in the ECMAScript standard, + // however, sane implementations should give the same results for Math.pow(2, <integer>) operations + + // Like frexp of C and std::frexp of C++, + // but returns an array instead of using a pointer argument for passing the exponent result. + // Object.is(n, frexp(n)[0] * 2 ** frexp(n)[1]) for all number values of n except when Math.isFinite(n) && Math.abs(n) > 2**1023 + // Object.is(n, (2 * frexp(n)[0]) * 2 ** (frexp(n)[1] - 1)) for all number values of n + // Object.is(n, frexp(n)[0]) for these values of n: 0, -0, NaN, Infinity, -Infinity + // Math.abs(frexp(n)[0]) is >= 0.5 and < 1.0 for any other number-type value of n + // See https://en.cppreference.com/w/c/numeric/math/frexp for a more detailed description + + arg = Number(arg) + + const result: [number, number] = [arg, 0] + + if (arg !== 0 && Number.isFinite(arg)) { + const absArg = Math.abs(arg) + // Math.log2 was introduced in ES2015, use it when available + const log2 = + Math.log2 || + function log2(n) { + return Math.log(n) * Math.LOG2E + } + let exp = Math.max(-1023, Math.floor(log2(absArg)) + 1) + let x = absArg * Math.pow(2, -exp) + + // These while loops compensate for rounding errors that sometimes occur because of ECMAScript's Math.log2's undefined precision + // and also works around the issue of Math.pow(2, -exp) === Infinity when exp <= -1024 + while (x < 0.5) { + x *= 2 + exp-- + } + while (x >= 1) { + x *= 0.5 + exp++ + } + + if (arg < 0) { + x = -x + } + result[0] = x + result[1] = exp + } + return result +}
src/c/math/index.js+0 −2 removed@@ -1,2 +0,0 @@ -module.exports.abs = require('./abs') -module.exports.frexp = require('./frexp')
src/c/math/index.ts+2 −0 added@@ -0,0 +1,2 @@ +export { abs } from './abs.ts' +export { frexp } from './frexp.ts'
src/c/stdio/index.js+0 −1 removed@@ -1 +0,0 @@ -module.exports.sprintf = require('./sprintf')
src/c/stdio/index.ts+1 −0 added@@ -0,0 +1 @@ +export { sprintf } from './sprintf.ts'
src/c/stdio/sprintf.js+0 −132 removed@@ -1,132 +0,0 @@ -function pad(str, minLength, padChar, leftJustify) { - const diff = minLength - str.length - const padStr = padChar.repeat(Math.max(0, diff)) - - return leftJustify ? str + padStr : padStr + str -} - -module.exports = function sprintf(format, ...args) { - // original by: Rafał Kukawski - // bugfixed by: Param Siddharth - // example 1: sprintf('%+10.*d', 5, 1) - // returns 1: ' +00001' - // example 2: sprintf('%s is a %d%% %s %s.', 'Param', 90, 'good', 'boy') - // returns 2: 'Param is a 90% good boy.' - const placeholderRegex = /%(?:(\d+)\$)?([-+#0 ]*)(\*|\d+)?(?:\.(\*|\d*))?([\s\S])/g - - let index = 0 - - return format.replace(placeholderRegex, function (match, param, flags, width, prec, modifier) { - const leftJustify = flags.includes('-') - - // flag '0' is ignored when flag '-' is present - const padChar = leftJustify ? ' ' : flags.split('').reduce((pc, c) => ([' ', '0'].includes(c) ? c : pc), ' ') - - const positiveSign = flags.includes('+') ? '+' : flags.includes(' ') ? ' ' : '' - - const minWidth = width === '*' ? args[index++] : +width || 0 - let precision = prec === '*' ? args[index++] : +prec - - if (param && !+param) { - throw new Error('Param index must be greater than 0') - } - - if (param && +param > args.length) { - throw new Error('Too few arguments') - } - - // compiling with default clang params, mixed positional and non-positional params - // give only a warning - const arg = param ? args[param - 1] : args[index] - - if (modifier !== '%') { - index++ - } - - if (precision === undefined || isNaN(precision)) { - precision = 'eEfFgG'.includes(modifier) ? 6 : modifier === 's' ? String(arg).length : undefined - } - - switch (modifier) { - case '%': - return '%' - case 'd': - case 'i': { - const number = Math.trunc(+arg || 0) - const abs = Math.abs(number) - const prefix = number < 0 ? '-' : positiveSign - - const str = pad(abs.toString(), precision || 0, '0', false) - - if (padChar === '0') { - return prefix + pad(str, minWidth - prefix.length, padChar, leftJustify) - } - - return pad(prefix + str, minWidth, padChar, leftJustify) - } - case 'e': - case 'E': - case 'f': - case 'F': - case 'g': - case 'G': { - const number = +arg - const abs = Math.abs(number) - const prefix = number < 0 ? '-' : positiveSign - - const op = [Number.prototype.toExponential, Number.prototype.toFixed, Number.prototype.toPrecision][ - 'efg'.indexOf(modifier.toLowerCase()) - ] - - const tr = [String.prototype.toLowerCase, String.prototype.toUpperCase]['eEfFgG'.indexOf(modifier) % 2] - - const isSpecial = isNaN(abs) || !isFinite(abs) - - const str = isSpecial ? abs.toString().substr(0, 3) : op.call(abs, precision) - - if (padChar === '0' && !isSpecial) { - return prefix + pad(tr.call(str), minWidth - prefix.length, padChar, leftJustify) - } - - return pad(tr.call(prefix + str), minWidth, isSpecial ? ' ' : padChar, leftJustify) - } - case 'b': - case 'o': - case 'u': - case 'x': - case 'X': { - const number = +arg || 0 - const intVal = Math.trunc(number) + (number < 0 ? 0xffffffff + 1 : 0) - const base = [2, 8, 10, 16, 16]['bouxX'.indexOf(modifier)] - const prefix = intVal && flags.includes('#') ? ['', '0', '', '0x', '0X']['bouxXX'.indexOf(modifier)] : '' - - if (padChar === '0' && prefix) { - return ( - prefix + - pad(pad(intVal.toString(base), precision, '0', false), minWidth - prefix.length, padChar, leftJustify) - ) - } - - return pad(prefix + pad(intVal.toString(base), precision, '0', false), minWidth, padChar, leftJustify) - } - case 'p': - case 'n': { - throw new Error(`'${modifier}' modifier not supported`) - } - case 's': { - return pad(String(arg).substr(0, precision), minWidth, padChar, leftJustify) - } - case 'c': { - // extension, if arg is string, take first char - const chr = typeof arg === 'string' ? arg.charAt(0) : String.fromCharCode(+arg) - return pad(chr, minWidth, padChar, leftJustify) - } - case 'a': - case 'A': - throw new Error(`'${modifier}' modifier not yet implemented`) - default: - // for unknown modifiers, return the modifier char - return modifier - } - }) -}
src/c/stdio/sprintf.ts+157 −0 added@@ -0,0 +1,157 @@ +function pad(str: string, minLength: number, padChar: string, leftJustify: boolean) { + const diff = minLength - str.length + const padStr = padChar.repeat(Math.max(0, diff)) + + return leftJustify ? str + padStr : padStr + str +} + +export function sprintf(format: string, ...args: unknown[]): string { + // original by: Rafał Kukawski + // bugfixed by: Param Siddharth + // example 1: sprintf('%+10.*d', 5, 1) + // returns 1: ' +00001' + // example 2: sprintf('%s is a %d%% %s %s.', 'Param', 90, 'good', 'boy') + // returns 2: 'Param is a 90% good boy.' + const placeholderRegex = /%(?:(\d+)\$)?([-+#0 ]*)(\*|\d+)?(?:\.(\*|\d*))?([\s\S])/g + + let index = 0 + + return format.replace( + placeholderRegex, + function ( + _match: string, + param: string | undefined, + flags: string, + width: string | undefined, + prec: string | undefined, + modifier: string, + ) { + const leftJustify = flags.includes('-') + + // flag '0' is ignored when flag '-' is present + const padChar = leftJustify + ? ' ' + : flags.split('').reduce((pc: string, c: string) => ([' ', '0'].includes(c) ? c : pc), ' ') + + const positiveSign = flags.includes('+') ? '+' : flags.includes(' ') ? ' ' : '' + + const minWidth = width === '*' ? Number(args[index++] ?? 0) : +(width ?? 0) || 0 + let precision: number | undefined = + prec === '*' ? Number(args[index++] ?? 0) : prec === undefined ? Number.NaN : +prec + + const paramIndex = param ? +param : 0 + if (param && !paramIndex) { + throw new Error('Param index must be greater than 0') + } + + if (param && paramIndex > args.length) { + throw new Error('Too few arguments') + } + + // compiling with default clang params, mixed positional and non-positional params + // give only a warning + const arg = param ? args[paramIndex - 1] : args[index] + + if (modifier !== '%') { + index++ + } + + if (precision === undefined || isNaN(precision)) { + precision = 'eEfFgG'.includes(modifier) ? 6 : modifier === 's' ? String(arg).length : undefined + } + + switch (modifier) { + case '%': + return '%' + case 'd': + case 'i': { + const number = Math.trunc(Number(arg) || 0) + const abs = Math.abs(number) + const prefix = number < 0 ? '-' : positiveSign + + const str = pad(abs.toString(), precision || 0, '0', false) + + if (padChar === '0') { + return prefix + pad(str, minWidth - prefix.length, padChar, leftJustify) + } + + return pad(prefix + str, minWidth, padChar, leftJustify) + } + case 'e': + case 'E': + case 'f': + case 'F': + case 'g': + case 'G': { + const number = Number(arg) + const abs = Math.abs(number) + const prefix = number < 0 ? '-' : positiveSign + const operationIndex = 'efg'.indexOf(modifier.toLowerCase()) + const operation = [ + (value: number, fractionDigits?: number) => value.toExponential(fractionDigits), + (value: number, digits?: number) => value.toFixed(digits), + (value: number, precisionDigits?: number) => value.toPrecision(precisionDigits), + ][operationIndex] + const transform = + [(value: string) => value.toLowerCase(), (value: string) => value.toUpperCase()][ + 'eEfFgG'.indexOf(modifier) % 2 + ] ?? ((value: string) => value) + + const isSpecial = isNaN(abs) || !isFinite(abs) + + const str = isSpecial + ? abs.toString().substr(0, 3) + : (operation || ((value: number) => value.toString()))(abs, precision) + + if (padChar === '0' && !isSpecial) { + return prefix + pad(transform(str), minWidth - prefix.length, padChar, leftJustify) + } + + return pad(transform(prefix + str), minWidth, isSpecial ? ' ' : padChar, leftJustify) + } + case 'b': + case 'o': + case 'u': + case 'x': + case 'X': { + const number = Number(arg) || 0 + const intVal = Math.trunc(number) + (number < 0 ? 0xffffffff + 1 : 0) + const base = [2, 8, 10, 16, 16]['bouxX'.indexOf(modifier)] ?? 10 + const prefix = intVal && flags.includes('#') ? ['', '0', '', '0x', '0X']['bouxXX'.indexOf(modifier)] : '' + + if (padChar === '0' && prefix) { + return ( + prefix + + pad( + pad(intVal.toString(base), precision ?? 0, '0', false), + minWidth - prefix.length, + padChar, + leftJustify, + ) + ) + } + + return pad(prefix + pad(intVal.toString(base), precision ?? 0, '0', false), minWidth, padChar, leftJustify) + } + case 'p': + case 'n': { + throw new Error(`'${modifier}' modifier not supported`) + } + case 's': { + return pad(String(arg).substr(0, precision), minWidth, padChar, leftJustify) + } + case 'c': { + // extension, if arg is string, take first char + const chr = typeof arg === 'string' ? arg.charAt(0) : String.fromCharCode(Number(arg)) + return pad(chr, minWidth, padChar, leftJustify) + } + case 'a': + case 'A': + throw new Error(`'${modifier}' modifier not yet implemented`) + default: + // for unknown modifiers, return the modifier char + return modifier + } + }, + ) +}
src/c/stdlib/atof.js+0 −15 removed@@ -1,15 +0,0 @@ -module.exports = function atof(str) { - // discuss at: https://locutus.io/c/stdlib/atof/ - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Converts a string to a floating-point number. - // example 1: atof('3.14') - // returns 1: 3.14 - // example 2: atof(' -2.5e10') - // returns 2: -25000000000 - // example 3: atof('abc') - // returns 3: 0 - - str = (str + '').replace(/^\s+/, '') - const result = parseFloat(str) - return isNaN(result) ? 0 : result -}
src/c/stdlib/atof.ts+15 −0 added@@ -0,0 +1,15 @@ +export function atof(str: string): number { + // discuss at: https://locutus.io/c/stdlib/atof/ + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Converts a string to a floating-point number. + // example 1: atof('3.14') + // returns 1: 3.14 + // example 2: atof(' -2.5e10') + // returns 2: -25000000000 + // example 3: atof('abc') + // returns 3: 0 + + str = (str + '').replace(/^\s+/, '') + const result = parseFloat(str) + return isNaN(result) ? 0 : result +}
src/c/stdlib/atoi.js+0 −17 removed@@ -1,17 +0,0 @@ -module.exports = function atoi(str) { - // discuss at: https://locutus.io/c/stdlib/atoi/ - // parity verified: C 23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Converts a string to an integer. - // note 1: Parses until first non-numeric character. - // example 1: atoi('42') - // returns 1: 42 - // example 2: atoi(' -123abc') - // returns 2: -123 - // example 3: atoi('abc') - // returns 3: 0 - - str = (str + '').replace(/^\s+/, '') - const result = parseInt(str, 10) - return isNaN(result) ? 0 : result -}
src/c/stdlib/atoi.ts+17 −0 added@@ -0,0 +1,17 @@ +export function atoi(str: string): number { + // discuss at: https://locutus.io/c/stdlib/atoi/ + // parity verified: C 23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Converts a string to an integer. + // note 1: Parses until first non-numeric character. + // example 1: atoi('42') + // returns 1: 42 + // example 2: atoi(' -123abc') + // returns 2: -123 + // example 3: atoi('abc') + // returns 3: 0 + + str = (str + '').replace(/^\s+/, '') + const result = parseInt(str, 10) + return isNaN(result) ? 0 : result +}
src/c/stdlib/index.js+0 −2 removed@@ -1,2 +0,0 @@ -module.exports.atof = require('./atof') -module.exports.atoi = require('./atoi')
src/c/stdlib/index.ts+2 −0 added@@ -0,0 +1,2 @@ +export { atof } from './atof.ts' +export { atoi } from './atoi.ts'
src/c/string/index.js+0 −5 removed@@ -1,5 +0,0 @@ -module.exports.strcat = require('./strcat') -module.exports.strchr = require('./strchr') -module.exports.strcmp = require('./strcmp') -module.exports.strlen = require('./strlen') -module.exports.strstr = require('./strstr')
src/c/string/index.ts+5 −0 added@@ -0,0 +1,5 @@ +export { strcat } from './strcat.ts' +export { strchr } from './strchr.ts' +export { strcmp } from './strcmp.ts' +export { strlen } from './strlen.ts' +export { strstr } from './strstr.ts'
src/c/string/strcat.js+0 −12 removed@@ -1,12 +0,0 @@ -module.exports = function strcat(dest, src) { - // discuss at: https://locutus.io/c/string/strcat/ - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Appends src string to dest string. - // note 1: In C this modifies dest, but JS strings are immutable so we return a new string. - // example 1: strcat('Hello, ', 'World!') - // returns 1: 'Hello, World!' - // example 2: strcat('abc', 'def') - // returns 2: 'abcdef' - - return dest + '' + (src + '') -}
src/c/string/strcat.ts+12 −0 added@@ -0,0 +1,12 @@ +export function strcat(dest: string, src: string): string { + // discuss at: https://locutus.io/c/string/strcat/ + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Appends src string to dest string. + // note 1: In C this modifies dest, but JS strings are immutable so we return a new string. + // example 1: strcat('Hello, ', 'World!') + // returns 1: 'Hello, World!' + // example 2: strcat('abc', 'def') + // returns 2: 'abcdef' + + return dest + '' + (src + '') +}
src/c/string/strchr.js+0 −15 removed@@ -1,15 +0,0 @@ -module.exports = function strchr(str, c) { - // discuss at: https://locutus.io/c/string/strchr/ - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns a pointer to the first occurrence of c in str. - // note 1: In JS, returns the substring starting from the first occurrence, or null. - // example 1: strchr('Hello, World!', 'o') - // returns 1: 'o, World!' - // example 2: strchr('Hello', 'x') - // returns 2: null - - str = str + '' - c = (c + '').charAt(0) - const idx = str.indexOf(c) - return idx === -1 ? null : str.slice(idx) -}
src/c/string/strchr.ts+15 −0 added@@ -0,0 +1,15 @@ +export function strchr(str: string, c: string): string | null { + // discuss at: https://locutus.io/c/string/strchr/ + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns a pointer to the first occurrence of c in str. + // note 1: In JS, returns the substring starting from the first occurrence, or null. + // example 1: strchr('Hello, World!', 'o') + // returns 1: 'o, World!' + // example 2: strchr('Hello', 'x') + // returns 2: null + + str = str + '' + c = (c + '').charAt(0) + const idx = str.indexOf(c) + return idx === -1 ? null : str.slice(idx) +}
src/c/string/strcmp.js+0 −21 removed@@ -1,21 +0,0 @@ -module.exports = function strcmp(str1, str2) { - // discuss at: https://locutus.io/c/string/strcmp/ - // parity verified: C 23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Compares two strings lexicographically. - // note 1: Returns < 0 if str1 < str2, 0 if equal, > 0 if str1 > str2. - // example 1: strcmp('abc', 'abc') - // returns 1: 0 - // example 2: strcmp('abc', 'abd') - // returns 2: -1 - // example 3: strcmp('abd', 'abc') - // returns 3: 1 - - str1 = str1 + '' - str2 = str2 + '' - - if (str1 === str2) { - return 0 - } - return str1 < str2 ? -1 : 1 -}
src/c/string/strcmp.ts+21 −0 added@@ -0,0 +1,21 @@ +export function strcmp(str1: string, str2: string): number { + // discuss at: https://locutus.io/c/string/strcmp/ + // parity verified: C 23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Compares two strings lexicographically. + // note 1: Returns < 0 if str1 < str2, 0 if equal, > 0 if str1 > str2. + // example 1: strcmp('abc', 'abc') + // returns 1: 0 + // example 2: strcmp('abc', 'abd') + // returns 2: -1 + // example 3: strcmp('abd', 'abc') + // returns 3: 1 + + str1 = str1 + '' + str2 = str2 + '' + + if (str1 === str2) { + return 0 + } + return str1 < str2 ? -1 : 1 +}
src/c/string/strlen.js+0 −12 removed@@ -1,12 +0,0 @@ -module.exports = function strlen(str) { - // discuss at: https://locutus.io/c/string/strlen/ - // parity verified: C 23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns the length of the string. - // example 1: strlen('hello') - // returns 1: 5 - // example 2: strlen('') - // returns 2: 0 - - return (str + '').length -}
src/c/string/strlen.ts+12 −0 added@@ -0,0 +1,12 @@ +export function strlen(str: string): number { + // discuss at: https://locutus.io/c/string/strlen/ + // parity verified: C 23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns the length of the string. + // example 1: strlen('hello') + // returns 1: 5 + // example 2: strlen('') + // returns 2: 0 + + return (str + '').length +}
src/c/string/strstr.js+0 −15 removed@@ -1,15 +0,0 @@ -module.exports = function strstr(haystack, needle) { - // discuss at: https://locutus.io/c/string/strstr/ - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Finds the first occurrence of needle in haystack. - // note 1: Returns substring starting from the match, or null if not found. - // example 1: strstr('Hello, World!', 'World') - // returns 1: 'World!' - // example 2: strstr('Hello', 'xyz') - // returns 2: null - - haystack = haystack + '' - needle = needle + '' - const idx = haystack.indexOf(needle) - return idx === -1 ? null : haystack.slice(idx) -}
src/c/string/strstr.ts+15 −0 added@@ -0,0 +1,15 @@ +export function strstr(haystack: string, needle: string): string | null { + // discuss at: https://locutus.io/c/string/strstr/ + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Finds the first occurrence of needle in haystack. + // note 1: Returns substring starting from the match, or null if not found. + // example 1: strstr('Hello, World!', 'World') + // returns 1: 'World!' + // example 2: strstr('Hello', 'xyz') + // returns 2: null + + haystack = haystack + '' + needle = needle + '' + const idx = haystack.indexOf(needle) + return idx === -1 ? null : haystack.slice(idx) +}
src/elixir/Float/ceil.js+0 −16 removed@@ -1,16 +0,0 @@ -module.exports = function ceil(x) { - // discuss at: https://locutus.io/elixir/ceil/ - // parity verified: Elixir 1.18 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns the smallest integer >= x - // example 1: ceil(4.2) - // returns 1: 5 - // example 2: ceil(-0.5) - // returns 2: 0 - // example 3: ceil(3) - // returns 3: 3 - - const result = Math.ceil(x) - // Convert -0 to 0 for consistency - return Object.is(result, -0) ? 0 : result -}
src/elixir/Float/ceil.ts+16 −0 added@@ -0,0 +1,16 @@ +export function ceil(x: number): number { + // discuss at: https://locutus.io/elixir/ceil/ + // parity verified: Elixir 1.18 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns the smallest integer >= x + // example 1: ceil(4.2) + // returns 1: 5 + // example 2: ceil(-0.5) + // returns 2: 0 + // example 3: ceil(3) + // returns 3: 3 + + const result = Math.ceil(x) + // Convert -0 to 0 for consistency + return Object.is(result, -0) ? 0 : result +}
src/elixir/Float/floor.js+0 −14 removed@@ -1,14 +0,0 @@ -module.exports = function floor(x) { - // discuss at: https://locutus.io/elixir/floor/ - // parity verified: Elixir 1.18 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns the largest integer <= x - // example 1: floor(4.7) - // returns 1: 4 - // example 2: floor(-0.5) - // returns 2: -1 - // example 3: floor(3) - // returns 3: 3 - - return Math.floor(x) -}
src/elixir/Float/floor.ts+14 −0 added@@ -0,0 +1,14 @@ +export function floor(x: number): number { + // discuss at: https://locutus.io/elixir/floor/ + // parity verified: Elixir 1.18 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns the largest integer <= x + // example 1: floor(4.7) + // returns 1: 4 + // example 2: floor(-0.5) + // returns 2: -1 + // example 3: floor(3) + // returns 3: 3 + + return Math.floor(x) +}
src/elixir/Float/index.js+0 −2 removed@@ -1,2 +0,0 @@ -module.exports.ceil = require('./ceil') -module.exports.floor = require('./floor')
src/elixir/Float/index.ts+2 −0 added@@ -0,0 +1,2 @@ +export { ceil } from './ceil.ts' +export { floor } from './floor.ts'
src/elixir/Kernel/abs.js+0 −13 removed@@ -1,13 +0,0 @@ -module.exports = function abs(x) { - // discuss at: https://locutus.io/elixir/abs/ - // parity verified: Elixir 1.18 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: abs(-5) - // returns 1: 5 - // example 2: abs(3.14) - // returns 2: 3.14 - // example 3: abs(0) - // returns 3: 0 - - return Math.abs(x) -}
src/elixir/Kernel/abs.ts+13 −0 added@@ -0,0 +1,13 @@ +export function abs(x: number): number { + // discuss at: https://locutus.io/elixir/abs/ + // parity verified: Elixir 1.18 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: abs(-5) + // returns 1: 5 + // example 2: abs(3.14) + // returns 2: 3.14 + // example 3: abs(0) + // returns 3: 0 + + return Math.abs(x) +}
src/elixir/Kernel/index.js+0 −1 removed@@ -1 +0,0 @@ -module.exports.abs = require('./abs')
src/elixir/Kernel/index.ts+1 −0 added@@ -0,0 +1 @@ +export { abs } from './abs.ts'
src/elixir/String/downcase.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function downcase(string) { - // discuss at: https://locutus.io/elixir/downcase/ - // parity verified: Elixir 1.18 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: downcase('HELLO') - // returns 1: 'hello' - // example 2: downcase('Hello World') - // returns 2: 'hello world' - - return String(string).toLowerCase() -}
src/elixir/String/downcase.ts+11 −0 added@@ -0,0 +1,11 @@ +export function downcase(string: string): string { + // discuss at: https://locutus.io/elixir/downcase/ + // parity verified: Elixir 1.18 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: downcase('HELLO') + // returns 1: 'hello' + // example 2: downcase('Hello World') + // returns 2: 'hello world' + + return String(string).toLowerCase() +}
src/elixir/String/index.js+0 −4 removed@@ -1,4 +0,0 @@ -module.exports.downcase = require('./downcase') -module.exports.length = require('./length') -module.exports.reverse = require('./reverse') -module.exports.upcase = require('./upcase')
src/elixir/String/index.ts+4 −0 added@@ -0,0 +1,4 @@ +export { downcase } from './downcase.ts' +export { length } from './length.ts' +export { reverse } from './reverse.ts' +export { upcase } from './upcase.ts'
src/elixir/String/length.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function length(string) { - // discuss at: https://locutus.io/elixir/length/ - // parity verified: Elixir 1.18 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: length('hello') - // returns 1: 5 - // example 2: length('') - // returns 2: 0 - - return String(string).length -}
src/elixir/String/length.ts+11 −0 added@@ -0,0 +1,11 @@ +export function length(string: string): number { + // discuss at: https://locutus.io/elixir/length/ + // parity verified: Elixir 1.18 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: length('hello') + // returns 1: 5 + // example 2: length('') + // returns 2: 0 + + return String(string).length +}
src/elixir/String/reverse.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function reverse(string) { - // discuss at: https://locutus.io/elixir/reverse/ - // parity verified: Elixir 1.18 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: reverse('hello') - // returns 1: 'olleh' - // example 2: reverse('abc') - // returns 2: 'cba' - - return String(string).split('').reverse().join('') -}
src/elixir/String/reverse.ts+11 −0 added@@ -0,0 +1,11 @@ +export function reverse(string: string): string { + // discuss at: https://locutus.io/elixir/reverse/ + // parity verified: Elixir 1.18 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: reverse('hello') + // returns 1: 'olleh' + // example 2: reverse('abc') + // returns 2: 'cba' + + return String(string).split('').reverse().join('') +}
src/elixir/String/upcase.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function upcase(string) { - // discuss at: https://locutus.io/elixir/upcase/ - // parity verified: Elixir 1.18 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: upcase('hello') - // returns 1: 'HELLO' - // example 2: upcase('Hello World') - // returns 2: 'HELLO WORLD' - - return String(string).toUpperCase() -}
src/elixir/String/upcase.ts+11 −0 added@@ -0,0 +1,11 @@ +export function upcase(string: string): string { + // discuss at: https://locutus.io/elixir/upcase/ + // parity verified: Elixir 1.18 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: upcase('hello') + // returns 1: 'HELLO' + // example 2: upcase('Hello World') + // returns 2: 'HELLO WORLD' + + return String(string).toUpperCase() +}
src/golang/index.js+0 −1 removed@@ -1 +0,0 @@ -module.exports.strings = require('./strings')
src/golang/index.ts+2 −0 added@@ -0,0 +1,2 @@ +export * as strconv from './strconv/index.ts' +export * as strings from './strings/index.ts'
src/golang/strconv/Atoi.js+0 −26 removed@@ -1,26 +0,0 @@ -module.exports = function Atoi(s) { - // discuss at: https://locutus.io/golang/strconv/Atoi - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Parses a decimal string and returns the integer value. - // note 1: Returns [value, null] on success, [0, error] on failure. - // example 1: Atoi('42')[0] - // returns 1: 42 - // example 2: Atoi('-123')[0] - // returns 2: -123 - // example 3: Atoi('abc')[0] - // returns 3: 0 - - s = (s + '').trim() - - if (!/^-?\d+$/.test(s)) { - return [0, new Error('strconv.Atoi: parsing "' + s + '": invalid syntax')] - } - - const result = parseInt(s, 10) - if (isNaN(result)) { - return [0, new Error('strconv.Atoi: parsing "' + s + '": invalid syntax')] - } - - return [result, null] -}
src/golang/strconv/Atoi.ts+26 −0 added@@ -0,0 +1,26 @@ +export function Atoi(s: unknown): [number, Error | null] { + // discuss at: https://locutus.io/golang/strconv/Atoi + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Parses a decimal string and returns the integer value. + // note 1: Returns [value, null] on success, [0, error] on failure. + // example 1: Atoi('42')[0] + // returns 1: 42 + // example 2: Atoi('-123')[0] + // returns 2: -123 + // example 3: Atoi('abc')[0] + // returns 3: 0 + + const normalized = String(s).trim() + + if (!/^-?\d+$/.test(normalized)) { + return [0, new Error('strconv.Atoi: parsing "' + normalized + '": invalid syntax')] + } + + const result = parseInt(normalized, 10) + if (isNaN(result)) { + return [0, new Error('strconv.Atoi: parsing "' + normalized + '": invalid syntax')] + } + + return [result, null] +}
src/golang/strconv/FormatBool.js+0 −12 removed@@ -1,12 +0,0 @@ -module.exports = function FormatBool(b) { - // discuss at: https://locutus.io/golang/strconv/FormatBool - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns "true" or "false" according to the value of b. - // example 1: FormatBool(true) - // returns 1: 'true' - // example 2: FormatBool(false) - // returns 2: 'false' - - return b ? 'true' : 'false' -}
src/golang/strconv/FormatBool.ts+12 −0 added@@ -0,0 +1,12 @@ +export function FormatBool(b: boolean): string { + // discuss at: https://locutus.io/golang/strconv/FormatBool + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns "true" or "false" according to the value of b. + // example 1: FormatBool(true) + // returns 1: 'true' + // example 2: FormatBool(false) + // returns 2: 'false' + + return b ? 'true' : 'false' +}
src/golang/strconv/FormatInt.js+0 −21 removed@@ -1,21 +0,0 @@ -module.exports = function FormatInt(i, base) { - // discuss at: https://locutus.io/golang/strconv/FormatInt - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns the string representation of i in the given base (2 to 36). - // example 1: FormatInt(42, 10) - // returns 1: '42' - // example 2: FormatInt(255, 16) - // returns 2: 'ff' - // example 3: FormatInt(10, 2) - // returns 3: '1010' - - i = parseInt(i, 10) - base = parseInt(base, 10) || 10 - - if (base < 2 || base > 36) { - throw new Error('strconv: illegal base ' + base) - } - - return i.toString(base) -}
src/golang/strconv/FormatInt.ts+21 −0 added@@ -0,0 +1,21 @@ +export function FormatInt(i: number, base: number): string { + // discuss at: https://locutus.io/golang/strconv/FormatInt + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns the string representation of i in the given base (2 to 36). + // example 1: FormatInt(42, 10) + // returns 1: '42' + // example 2: FormatInt(255, 16) + // returns 2: 'ff' + // example 3: FormatInt(10, 2) + // returns 3: '1010' + + i = Number.parseInt(String(i), 10) + base = Number.parseInt(String(base), 10) || 10 + + if (base < 2 || base > 36) { + throw new Error('strconv: illegal base ' + base) + } + + return i.toString(base) +}
src/golang/strconv/index.js+0 −6 removed@@ -1,6 +0,0 @@ -module.exports.Atoi = require('./Atoi') -module.exports.FormatBool = require('./FormatBool') -module.exports.FormatInt = require('./FormatInt') -module.exports.Itoa = require('./Itoa') -module.exports.ParseBool = require('./ParseBool') -module.exports.ParseInt = require('./ParseInt')
src/golang/strconv/index.ts+6 −0 added@@ -0,0 +1,6 @@ +export { Atoi } from './Atoi.ts' +export { FormatBool } from './FormatBool.ts' +export { FormatInt } from './FormatInt.ts' +export { Itoa } from './Itoa.ts' +export { ParseBool } from './ParseBool.ts' +export { ParseInt } from './ParseInt.ts'
src/golang/strconv/Itoa.js+0 −14 removed@@ -1,14 +0,0 @@ -module.exports = function Itoa(i) { - // discuss at: https://locutus.io/golang/strconv/Itoa - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Converts an integer to its decimal string representation. - // example 1: Itoa(42) - // returns 1: '42' - // example 2: Itoa(-123) - // returns 2: '-123' - // example 3: Itoa(0) - // returns 3: '0' - - return String(parseInt(i, 10)) -}
src/golang/strconv/Itoa.ts+14 −0 added@@ -0,0 +1,14 @@ +export function Itoa(i: number): string { + // discuss at: https://locutus.io/golang/strconv/Itoa + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Converts an integer to its decimal string representation. + // example 1: Itoa(42) + // returns 1: '42' + // example 2: Itoa(-123) + // returns 2: '-123' + // example 3: Itoa(0) + // returns 3: '0' + + return String(Number.parseInt(String(i), 10)) +}
src/golang/strconv/ParseBool.js+0 −35 removed@@ -1,35 +0,0 @@ -module.exports = function ParseBool(str) { - // discuss at: https://locutus.io/golang/strconv/ParseBool - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Parses a boolean string value. - // note 1: Returns [value, null] on success, [false, error] on failure. - // note 1: Accepts: 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False - // example 1: ParseBool('true')[0] - // returns 1: true - // example 2: ParseBool('FALSE')[0] - // returns 2: false - // example 3: ParseBool('1')[0] - // returns 3: true - - str = (str + '').trim() - - switch (str) { - case '1': - case 't': - case 'T': - case 'true': - case 'TRUE': - case 'True': - return [true, null] - case '0': - case 'f': - case 'F': - case 'false': - case 'FALSE': - case 'False': - return [false, null] - default: - return [false, new Error('strconv.ParseBool: parsing "' + str + '": invalid syntax')] - } -}
src/golang/strconv/ParseBool.ts+35 −0 added@@ -0,0 +1,35 @@ +export function ParseBool(str: string): [boolean, Error | null] { + // discuss at: https://locutus.io/golang/strconv/ParseBool + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Parses a boolean string value. + // note 1: Returns [value, null] on success, [false, error] on failure. + // note 1: Accepts: 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False + // example 1: ParseBool('true')[0] + // returns 1: true + // example 2: ParseBool('FALSE')[0] + // returns 2: false + // example 3: ParseBool('1')[0] + // returns 3: true + + str = (str + '').trim() + + switch (str) { + case '1': + case 't': + case 'T': + case 'true': + case 'TRUE': + case 'True': + return [true, null] + case '0': + case 'f': + case 'F': + case 'false': + case 'FALSE': + case 'False': + return [false, null] + default: + return [false, new Error('strconv.ParseBool: parsing "' + str + '": invalid syntax')] + } +}
src/golang/strconv/ParseInt.js+0 −27 removed@@ -1,27 +0,0 @@ -module.exports = function ParseInt(s, base, bitSize) { - // discuss at: https://locutus.io/golang/strconv/ParseInt - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Interprets a string s in the given base (0, 2 to 36). - // note 1: Returns [value, null] on success, [0, error] on failure. - // example 1: ParseInt('42', 10, 64)[0] - // returns 1: 42 - // example 2: ParseInt('ff', 16, 64)[0] - // returns 2: 255 - // example 3: ParseInt('1010', 2, 64)[0] - // returns 3: 10 - - s = (s + '').trim() - base = parseInt(base, 10) || 10 - - if (base < 2 || base > 36) { - return [0, new Error('strconv.ParseInt: invalid base ' + base)] - } - - const result = parseInt(s, base) - if (isNaN(result)) { - return [0, new Error('strconv.ParseInt: parsing "' + s + '": invalid syntax')] - } - - return [result, null] -}
src/golang/strconv/ParseInt.ts+27 −0 added@@ -0,0 +1,27 @@ +export function ParseInt(s: string, base: number, _bitSize: number): [number, Error | null] { + // discuss at: https://locutus.io/golang/strconv/ParseInt + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Interprets a string s in the given base (0, 2 to 36). + // note 1: Returns [value, null] on success, [0, error] on failure. + // example 1: ParseInt('42', 10, 64)[0] + // returns 1: 42 + // example 2: ParseInt('ff', 16, 64)[0] + // returns 2: 255 + // example 3: ParseInt('1010', 2, 64)[0] + // returns 3: 10 + + s = (s + '').trim() + base = Number.parseInt(String(base), 10) || 10 + + if (base < 2 || base > 36) { + return [0, new Error('strconv.ParseInt: invalid base ' + base)] + } + + const result = parseInt(s, base) + if (isNaN(result)) { + return [0, new Error('strconv.ParseInt: parsing "' + s + '": invalid syntax')] + } + + return [result, null] +}
src/golang/strings/Compare.js+0 −19 removed@@ -1,19 +0,0 @@ -module.exports = function Compare(a, b) { - // discuss at: https://locutus.io/golang/strings/Compare/ - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: Compare('a', 'b') - // returns 1: -1 - // example 2: Compare('a', 'a') - // returns 2: 0 - // example 3: Compare('b', 'a') - // returns 3: 1 - - if (a < b) { - return -1 - } - if (a > b) { - return 1 - } - return 0 -}
src/golang/strings/Compare.ts+19 −0 added@@ -0,0 +1,19 @@ +export function Compare(a: string, b: string): number { + // discuss at: https://locutus.io/golang/strings/Compare/ + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: Compare('a', 'b') + // returns 1: -1 + // example 2: Compare('a', 'a') + // returns 2: 0 + // example 3: Compare('b', 'a') + // returns 3: 1 + + if (a < b) { + return -1 + } + if (a > b) { + return 1 + } + return 0 +}
src/golang/strings/ContainsAny.js+0 −18 removed@@ -1,18 +0,0 @@ -module.exports = function ContainsAny(s, chars) { - // discuss at: https://locutus.io/golang/strings/ContainsAny/ - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: ContainsAny('team', 'i') - // returns 1: false - // example 2: ContainsAny('fail', 'ui') - // returns 2: true - // example 3: ContainsAny('ure', 'ui') - // returns 3: true - - for (const char of chars) { - if (s.includes(char)) { - return true - } - } - return false -}
src/golang/strings/ContainsAny.ts+18 −0 added@@ -0,0 +1,18 @@ +export function ContainsAny(s: string, chars: string): boolean | false { + // discuss at: https://locutus.io/golang/strings/ContainsAny/ + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: ContainsAny('team', 'i') + // returns 1: false + // example 2: ContainsAny('fail', 'ui') + // returns 2: true + // example 3: ContainsAny('ure', 'ui') + // returns 3: true + + for (const char of chars) { + if (s.includes(char)) { + return true + } + } + return false +}
src/golang/strings/Contains.js+0 −9 removed@@ -1,9 +0,0 @@ -module.exports = function Contains(s, substr) { - // discuss at: https://locutus.io/golang/strings/Contains - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: Contains('Kevin', 'K') - // returns 1: true - - return (s + '').indexOf(substr) !== -1 -}
src/golang/strings/Contains.ts+9 −0 added@@ -0,0 +1,9 @@ +export function Contains(s: string, substr: string): boolean { + // discuss at: https://locutus.io/golang/strings/Contains + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: Contains('Kevin', 'K') + // returns 1: true + + return (s + '').indexOf(substr) !== -1 +}
src/golang/strings/Count.js+0 −33 removed@@ -1,33 +0,0 @@ -module.exports = function Count(s, sep) { - // discuss at: https://locutus.io/golang/strings/Count - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // input by: GopherJS (https://www.gopherjs.org/) - // example 1: Count("cheese", "e") - // returns 1: 3 - // example 2: Count("five", "") // before & after each rune - // returns 2: 5 - - let pos - let n = 0 - - if (sep.length === 0) { - return s.split(sep).length + 1 - } else if (sep.length > s.length) { - return 0 - } else if (sep.length === s.length) { - if (sep === s) { - return 1 - } - return 0 - } - while (true) { - pos = (s + '').indexOf(sep) - if (pos === -1) { - break - } - n = (n + 1) >> 0 - s = s.substring((pos + sep.length) >> 0) - } - return n -}
src/golang/strings/Count.ts+33 −0 added@@ -0,0 +1,33 @@ +export function Count(s: string, sep: string): number { + // discuss at: https://locutus.io/golang/strings/Count + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // input by: GopherJS (https://www.gopherjs.org/) + // example 1: Count("cheese", "e") + // returns 1: 3 + // example 2: Count("five", "") // before & after each rune + // returns 2: 5 + + let pos + let n = 0 + + if (sep.length === 0) { + return s.split(sep).length + 1 + } else if (sep.length > s.length) { + return 0 + } else if (sep.length === s.length) { + if (sep === s) { + return 1 + } + return 0 + } + while (true) { + pos = (s + '').indexOf(sep) + if (pos === -1) { + break + } + n = (n + 1) >> 0 + s = s.substring((pos + sep.length) >> 0) + } + return n +}
src/golang/strings/EqualFold.js+0 −13 removed@@ -1,13 +0,0 @@ -module.exports = function EqualFold(s, t) { - // discuss at: https://locutus.io/golang/strings/EqualFold/ - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: EqualFold('Go', 'go') - // returns 1: true - // example 2: EqualFold('Hello', 'HELLO') - // returns 2: true - // example 3: EqualFold('Σ', 'σ') - // returns 3: true - - return s.toLowerCase() === t.toLowerCase() -}
src/golang/strings/EqualFold.ts+13 −0 added@@ -0,0 +1,13 @@ +export function EqualFold(s: string, t: string): boolean { + // discuss at: https://locutus.io/golang/strings/EqualFold/ + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: EqualFold('Go', 'go') + // returns 1: true + // example 2: EqualFold('Hello', 'HELLO') + // returns 2: true + // example 3: EqualFold('Σ', 'σ') + // returns 3: true + + return s.toLowerCase() === t.toLowerCase() +}
src/golang/strings/Fields.js+0 −12 removed@@ -1,12 +0,0 @@ -module.exports = function Fields(s) { - // discuss at: https://locutus.io/golang/strings/Fields/ - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: Fields(' foo bar baz ') - // returns 1: ['foo', 'bar', 'baz'] - // example 2: Fields('') - // returns 2: [] - - // Split by whitespace and filter empty strings - return s.split(/\s+/).filter((field) => field !== '') -}
src/golang/strings/Fields.ts+12 −0 added@@ -0,0 +1,12 @@ +export function Fields(s: string): string[] { + // discuss at: https://locutus.io/golang/strings/Fields/ + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: Fields(' foo bar baz ') + // returns 1: ['foo', 'bar', 'baz'] + // example 2: Fields('') + // returns 2: [] + + // Split by whitespace and filter empty strings + return s.split(/\s+/).filter((field) => field !== '') +}
src/golang/strings/HasPrefix.js+0 −15 removed@@ -1,15 +0,0 @@ -module.exports = function HasPrefix(s, prefix) { - // discuss at: https://locutus.io/golang/strings/HasPrefix - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: HasPrefix('Gopher', 'Go') - // returns 1: true - // example 2: HasPrefix('Gopher', 'C') - // returns 2: false - // example 3: HasPrefix('Gopher', '') - // returns 3: true - - s = s + '' - prefix = prefix + '' - return s.slice(0, prefix.length) === prefix -}
src/golang/strings/HasPrefix.ts+15 −0 added@@ -0,0 +1,15 @@ +export function HasPrefix(s: string, prefix: string): boolean { + // discuss at: https://locutus.io/golang/strings/HasPrefix + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: HasPrefix('Gopher', 'Go') + // returns 1: true + // example 2: HasPrefix('Gopher', 'C') + // returns 2: false + // example 3: HasPrefix('Gopher', '') + // returns 3: true + + s = s + '' + prefix = prefix + '' + return s.slice(0, prefix.length) === prefix +}
src/golang/strings/HasSuffix.js+0 −20 removed@@ -1,20 +0,0 @@ -module.exports = function HasSuffix(s, suffix) { - // discuss at: https://locutus.io/golang/strings/HasSuffix - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: HasSuffix('Amigo', 'go') - // returns 1: true - // example 2: HasSuffix('Amigo', 'O') - // returns 2: false - // example 3: HasSuffix('Amigo', 'Amigo') - // returns 3: true - // example 4: HasSuffix('Amigo', '') - // returns 4: true - - s = s + '' - suffix = suffix + '' - if (suffix.length === 0) { - return true - } - return s.slice(-suffix.length) === suffix -}
src/golang/strings/HasSuffix.ts+20 −0 added@@ -0,0 +1,20 @@ +export function HasSuffix(s: string, suffix: string): boolean { + // discuss at: https://locutus.io/golang/strings/HasSuffix + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: HasSuffix('Amigo', 'go') + // returns 1: true + // example 2: HasSuffix('Amigo', 'O') + // returns 2: false + // example 3: HasSuffix('Amigo', 'Amigo') + // returns 3: true + // example 4: HasSuffix('Amigo', '') + // returns 4: true + + s = s + '' + suffix = suffix + '' + if (suffix.length === 0) { + return true + } + return s.slice(-suffix.length) === suffix +}
src/golang/strings/Index2.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function Index(s, sep) { - // discuss at: https://locutus.io/golang/strings/Index - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: Index('Kevin', 'K') - // returns 1: 0 - // example 2: Index('Kevin', 'Z') - // returns 2: -1 - - return (s + '').indexOf(sep) -}
src/golang/strings/Index2.ts+11 −0 added@@ -0,0 +1,11 @@ +export function Index(s: string, sep: string): number { + // discuss at: https://locutus.io/golang/strings/Index + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: Index('Kevin', 'K') + // returns 1: 0 + // example 2: Index('Kevin', 'Z') + // returns 2: -1 + + return (s + '').indexOf(sep) +}
src/golang/strings/IndexAny.js+0 −18 removed@@ -1,18 +0,0 @@ -module.exports = function IndexAny(s, chars) { - // discuss at: https://locutus.io/golang/strings/IndexAny/ - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: IndexAny('chicken', 'aeiouy') - // returns 1: 2 - // example 2: IndexAny('crwth', 'aeiouy') - // returns 2: -1 - - let minIndex = -1 - for (const char of chars) { - const idx = s.indexOf(char) - if (idx !== -1 && (minIndex === -1 || idx < minIndex)) { - minIndex = idx - } - } - return minIndex -}
src/golang/strings/IndexAny.ts+18 −0 added@@ -0,0 +1,18 @@ +export function IndexAny(s: string, chars: string): number { + // discuss at: https://locutus.io/golang/strings/IndexAny/ + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: IndexAny('chicken', 'aeiouy') + // returns 1: 2 + // example 2: IndexAny('crwth', 'aeiouy') + // returns 2: -1 + + let minIndex = -1 + for (const char of chars) { + const idx = s.indexOf(char) + if (idx !== -1 && (minIndex === -1 || idx < minIndex)) { + minIndex = idx + } + } + return minIndex +}
src/golang/strings/index.js+0 −25 removed@@ -1,25 +0,0 @@ -module.exports.Compare = require('./Compare') -module.exports.Contains = require('./Contains') -module.exports.ContainsAny = require('./ContainsAny') -module.exports.Count = require('./Count') -module.exports.EqualFold = require('./EqualFold') -module.exports.Fields = require('./Fields') -module.exports.HasPrefix = require('./HasPrefix') -module.exports.HasSuffix = require('./HasSuffix') -module.exports.Index = require('./Index2') -module.exports.Index = require('./Index2') -module.exports.IndexAny = require('./IndexAny') -module.exports.Join = require('./Join') -module.exports.LastIndex = require('./LastIndex') -module.exports.LastIndexAny = require('./LastIndexAny') -module.exports.Repeat = require('./Repeat') -module.exports.Replace = require('./Replace') -module.exports.Split = require('./Split') -module.exports.ToLower = require('./ToLower') -module.exports.ToUpper = require('./ToUpper') -module.exports.Trim = require('./Trim') -module.exports.TrimLeft = require('./TrimLeft') -module.exports.TrimPrefix = require('./TrimPrefix') -module.exports.TrimRight = require('./TrimRight') -module.exports.TrimSpace = require('./TrimSpace') -module.exports.TrimSuffix = require('./TrimSuffix')
src/golang/strings/Index.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function Index(s, substr) { - // discuss at: https://locutus.io/golang/strings/Index/ - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: Index('chicken', 'ken') - // returns 1: 4 - // example 2: Index('chicken', 'dmr') - // returns 2: -1 - - return s.indexOf(substr) -}
src/golang/strings/index.ts+24 −0 added@@ -0,0 +1,24 @@ +export { Compare } from './Compare.ts' +export { Contains } from './Contains.ts' +export { ContainsAny } from './ContainsAny.ts' +export { Count } from './Count.ts' +export { EqualFold } from './EqualFold.ts' +export { Fields } from './Fields.ts' +export { HasPrefix } from './HasPrefix.ts' +export { HasSuffix } from './HasSuffix.ts' +export { Index } from './Index2.ts' +export { IndexAny } from './IndexAny.ts' +export { Join } from './Join.ts' +export { LastIndex } from './LastIndex.ts' +export { LastIndexAny } from './LastIndexAny.ts' +export { Repeat } from './Repeat.ts' +export { Replace } from './Replace.ts' +export { Split } from './Split.ts' +export { ToLower } from './ToLower.ts' +export { ToUpper } from './ToUpper.ts' +export { Trim } from './Trim.ts' +export { TrimLeft } from './TrimLeft.ts' +export { TrimPrefix } from './TrimPrefix.ts' +export { TrimRight } from './TrimRight.ts' +export { TrimSpace } from './TrimSpace.ts' +export { TrimSuffix } from './TrimSuffix.ts'
src/golang/strings/Join.js+0 −16 removed@@ -1,16 +0,0 @@ -module.exports = function Join(elems, sep) { - // discuss at: https://locutus.io/golang/strings/Join - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Concatenates elements of an array to create a single string - // example 1: Join(['foo', 'bar', 'baz'], ', ') - // returns 1: 'foo, bar, baz' - // example 2: Join(['a', 'b', 'c'], '') - // returns 2: 'abc' - - if (!Array.isArray(elems)) { - return '' - } - - return elems.join(sep + '') -}
src/golang/strings/Join.ts+16 −0 added@@ -0,0 +1,16 @@ +export function Join(elems: unknown[] | unknown, sep: string): string { + // discuss at: https://locutus.io/golang/strings/Join + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Concatenates elements of an array to create a single string + // example 1: Join(['foo', 'bar', 'baz'], ', ') + // returns 1: 'foo, bar, baz' + // example 2: Join(['a', 'b', 'c'], '') + // returns 2: 'abc' + + if (!Array.isArray(elems)) { + return '' + } + + return (elems as unknown[]).join(sep + '') +}
src/golang/strings/LastIndexAny.js+0 −18 removed@@ -1,18 +0,0 @@ -module.exports = function LastIndexAny(s, chars) { - // discuss at: https://locutus.io/golang/strings/LastIndexAny/ - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: LastIndexAny('go gopher', 'go') - // returns 1: 4 - // example 2: LastIndexAny('go gopher', 'rodent') - // returns 2: 8 - - let maxIndex = -1 - for (const char of chars) { - const idx = s.lastIndexOf(char) - if (idx > maxIndex) { - maxIndex = idx - } - } - return maxIndex -}
src/golang/strings/LastIndexAny.ts+18 −0 added@@ -0,0 +1,18 @@ +export function LastIndexAny(s: string, chars: string): number { + // discuss at: https://locutus.io/golang/strings/LastIndexAny/ + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: LastIndexAny('go gopher', 'go') + // returns 1: 4 + // example 2: LastIndexAny('go gopher', 'rodent') + // returns 2: 8 + + let maxIndex = -1 + for (const char of chars) { + const idx = s.lastIndexOf(char) + if (idx > maxIndex) { + maxIndex = idx + } + } + return maxIndex +}
src/golang/strings/LastIndex.js+0 −12 removed@@ -1,12 +0,0 @@ -module.exports = function LastIndex(s, sep) { - // discuss at: https://locutus.io/golang/strings/LastIndex - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // input by: GopherJS (https://www.gopherjs.org/) - // example 1: LastIndex('go gopher', 'go') - // returns 1: 3 - // example 2: LastIndex('go gopher', 'rodent') - // returns 2: -1 - - return parseInt(s.lastIndexOf(sep), 10) >> 0 -}
src/golang/strings/LastIndex.ts+12 −0 added@@ -0,0 +1,12 @@ +export function LastIndex(s: string, sep: string): number { + // discuss at: https://locutus.io/golang/strings/LastIndex + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // input by: GopherJS (https://www.gopherjs.org/) + // example 1: LastIndex('go gopher', 'go') + // returns 1: 3 + // example 2: LastIndex('go gopher', 'rodent') + // returns 2: -1 + + return s.lastIndexOf(sep) +}
src/golang/strings/Repeat.js+0 −19 removed@@ -1,19 +0,0 @@ -module.exports = function Repeat(s, count) { - // discuss at: https://locutus.io/golang/strings/Repeat - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns a new string consisting of count copies of s - // example 1: Repeat('na', 2) - // returns 1: 'nana' - // example 2: Repeat('ba', 3) - // returns 2: 'bababa' - - s = s + '' - count = parseInt(count, 10) - - if (count < 0 || !isFinite(count)) { - throw new Error('strings: negative Repeat count') - } - - return s.repeat(count) -}
src/golang/strings/Repeat.ts+19 −0 added@@ -0,0 +1,19 @@ +export function Repeat(s: string, count: number): string { + // discuss at: https://locutus.io/golang/strings/Repeat + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns a new string consisting of count copies of s + // example 1: Repeat('na', 2) + // returns 1: 'nana' + // example 2: Repeat('ba', 3) + // returns 2: 'bababa' + + s = s + '' + count = Number.parseInt(String(count), 10) + + if (count < 0 || !isFinite(count)) { + throw new Error('strings: negative Repeat count') + } + + return s.repeat(count) +}
src/golang/strings/Replace.js+0 −44 removed@@ -1,44 +0,0 @@ -module.exports = function Replace(s, old, newStr, n) { - // discuss at: https://locutus.io/golang/strings/Replace - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: n is the number of replacements. If n < 0, no limit on replacements. - // example 1: Replace('oink oink oink', 'k', 'ky', 2) - // returns 1: 'oinky oinky oink' - // example 2: Replace('oink oink oink', 'oink', 'moo', -1) - // returns 2: 'moo moo moo' - - s = s + '' - old = old + '' - newStr = newStr + '' - - if (old === '') { - return s - } - - if (n === 0) { - return s - } - - if (n < 0) { - // Replace all occurrences - return s.split(old).join(newStr) - } - - // Replace n occurrences - let result = s - let count = 0 - let pos = 0 - - while (count < n) { - const idx = result.indexOf(old, pos) - if (idx === -1) { - break - } - result = result.slice(0, idx) + newStr + result.slice(idx + old.length) - pos = idx + newStr.length - count++ - } - - return result -}
src/golang/strings/Replace.ts+44 −0 added@@ -0,0 +1,44 @@ +export function Replace(s: string, old: string, newStr: string, n: number): string { + // discuss at: https://locutus.io/golang/strings/Replace + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: n is the number of replacements. If n < 0, no limit on replacements. + // example 1: Replace('oink oink oink', 'k', 'ky', 2) + // returns 1: 'oinky oinky oink' + // example 2: Replace('oink oink oink', 'oink', 'moo', -1) + // returns 2: 'moo moo moo' + + s = s + '' + old = old + '' + newStr = newStr + '' + + if (old === '') { + return s + } + + if (n === 0) { + return s + } + + if (n < 0) { + // Replace all occurrences + return s.split(old).join(newStr) + } + + // Replace n occurrences + let result = s + let count = 0 + let pos = 0 + + while (count < n) { + const idx = result.indexOf(old, pos) + if (idx === -1) { + break + } + result = result.slice(0, idx) + newStr + result.slice(idx + old.length) + pos = idx + newStr.length + count++ + } + + return result +}
src/golang/strings/Split.js+0 −24 removed@@ -1,24 +0,0 @@ -module.exports = function Split(s, sep) { - // discuss at: https://locutus.io/golang/strings/Split - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns an array of substrings split by sep - // example 1: Split('a,b,c', ',') - // returns 1: ['a', 'b', 'c'] - // example 2: Split('a man a plan a canal panama', 'a ') - // returns 2: ['', 'man ', 'plan ', 'canal panama'] - // example 3: Split(' xyz ', '') - // returns 3: [' ', 'x', 'y', 'z', ' '] - // example 4: Split('', 'Bernardo O\'Higgins') - // returns 4: [''] - - s = s + '' - sep = sep + '' - - if (sep === '') { - // Split into individual characters (like Go's behavior) - return s.split('') - } - - return s.split(sep) -}
src/golang/strings/Split.ts+24 −0 added@@ -0,0 +1,24 @@ +export function Split(s: string, sep: string): string[] { + // discuss at: https://locutus.io/golang/strings/Split + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns an array of substrings split by sep + // example 1: Split('a,b,c', ',') + // returns 1: ['a', 'b', 'c'] + // example 2: Split('a man a plan a canal panama', 'a ') + // returns 2: ['', 'man ', 'plan ', 'canal panama'] + // example 3: Split(' xyz ', '') + // returns 3: [' ', 'x', 'y', 'z', ' '] + // example 4: Split('', 'Bernardo O\'Higgins') + // returns 4: [''] + + s = s + '' + sep = sep + '' + + if (sep === '') { + // Split into individual characters (like Go's behavior) + return s.split('') + } + + return s.split(sep) +}
src/golang/strings/ToLower.js+0 −9 removed@@ -1,9 +0,0 @@ -module.exports = function ToLower(s) { - // discuss at: https://locutus.io/golang/strings/ToLower - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: ToLower('Gopher') - // returns 1: 'gopher' - - return (s + '').toLowerCase() -}
src/golang/strings/ToLower.ts+9 −0 added@@ -0,0 +1,9 @@ +export function ToLower(s: string): string { + // discuss at: https://locutus.io/golang/strings/ToLower + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: ToLower('Gopher') + // returns 1: 'gopher' + + return (s + '').toLowerCase() +}
src/golang/strings/ToUpper.js+0 −9 removed@@ -1,9 +0,0 @@ -module.exports = function ToUpper(s) { - // discuss at: https://locutus.io/golang/strings/ToUpper - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: ToUpper('Gopher') - // returns 1: 'GOPHER' - - return (s + '').toUpperCase() -}
src/golang/strings/ToUpper.ts+9 −0 added@@ -0,0 +1,9 @@ +export function ToUpper(s: string): string { + // discuss at: https://locutus.io/golang/strings/ToUpper + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: ToUpper('Gopher') + // returns 1: 'GOPHER' + + return (s + '').toUpperCase() +}
src/golang/strings/Trim.js+0 −19 removed@@ -1,19 +0,0 @@ -module.exports = function Trim(s, cutset) { - // discuss at: https://locutus.io/golang/strings/Trim - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Unlike Go's strings.Trim which removes characters in cutset, - // note 1: this implementation removes exact matches of cutset from both ends - // example 1: Trim('!!!Hello, Gophers!!!', '!') - // returns 1: 'Hello, Gophers' - // example 2: Trim(' Hello ', ' ') - // returns 2: 'Hello' - - s = s + '' - cutset = cutset + '' - - // Build regex to match cutset characters at start and end - const escaped = cutset.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - const regex = new RegExp('^[' + escaped + ']+|[' + escaped + ']+$', 'g') - return s.replace(regex, '') -}
src/golang/strings/TrimLeft.js+0 −15 removed@@ -1,15 +0,0 @@ -module.exports = function TrimLeft(s, cutset) { - // discuss at: https://locutus.io/golang/strings/TrimLeft/ - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: TrimLeft('¡¡¡Hello, Gophers!!!', '!¡') - // returns 1: 'Hello, Gophers!!!' - // example 2: TrimLeft(' hello ', ' ') - // returns 2: 'hello ' - - let start = 0 - while (start < s.length && cutset.includes(s[start])) { - start++ - } - return s.slice(start) -}
src/golang/strings/TrimLeft.ts+15 −0 added@@ -0,0 +1,15 @@ +export function TrimLeft(s: string, cutset: string): string { + // discuss at: https://locutus.io/golang/strings/TrimLeft/ + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: TrimLeft('¡¡¡Hello, Gophers!!!', '!¡') + // returns 1: 'Hello, Gophers!!!' + // example 2: TrimLeft(' hello ', ' ') + // returns 2: 'hello ' + + let start = 0 + while (start < s.length && cutset.includes(s.charAt(start))) { + start++ + } + return s.slice(start) +}
src/golang/strings/TrimPrefix.js+0 −14 removed@@ -1,14 +0,0 @@ -module.exports = function TrimPrefix(s, prefix) { - // discuss at: https://locutus.io/golang/strings/TrimPrefix/ - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: TrimPrefix('Hello, World!', 'Hello, ') - // returns 1: 'World!' - // example 2: TrimPrefix('Hello, World!', 'Goodbye, ') - // returns 2: 'Hello, World!' - - if (s.startsWith(prefix)) { - return s.slice(prefix.length) - } - return s -}
src/golang/strings/TrimPrefix.ts+14 −0 added@@ -0,0 +1,14 @@ +export function TrimPrefix(s: string, prefix: string): string { + // discuss at: https://locutus.io/golang/strings/TrimPrefix/ + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: TrimPrefix('Hello, World!', 'Hello, ') + // returns 1: 'World!' + // example 2: TrimPrefix('Hello, World!', 'Goodbye, ') + // returns 2: 'Hello, World!' + + if (s.startsWith(prefix)) { + return s.slice(prefix.length) + } + return s +}
src/golang/strings/TrimRight.js+0 −15 removed@@ -1,15 +0,0 @@ -module.exports = function TrimRight(s, cutset) { - // discuss at: https://locutus.io/golang/strings/TrimRight/ - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: TrimRight('¡¡¡Hello, Gophers!!!', '!¡') - // returns 1: '¡¡¡Hello, Gophers' - // example 2: TrimRight(' hello ', ' ') - // returns 2: ' hello' - - let end = s.length - while (end > 0 && cutset.includes(s[end - 1])) { - end-- - } - return s.slice(0, end) -}
src/golang/strings/TrimRight.ts+15 −0 added@@ -0,0 +1,15 @@ +export function TrimRight(s: string, cutset: string): string { + // discuss at: https://locutus.io/golang/strings/TrimRight/ + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: TrimRight('¡¡¡Hello, Gophers!!!', '!¡') + // returns 1: '¡¡¡Hello, Gophers' + // example 2: TrimRight(' hello ', ' ') + // returns 2: ' hello' + + let end = s.length + while (end > 0 && cutset.includes(s.charAt(end - 1))) { + end-- + } + return s.slice(0, end) +}
src/golang/strings/TrimSpace.js+0 −10 removed@@ -1,10 +0,0 @@ -module.exports = function TrimSpace(s) { - // discuss at: https://locutus.io/golang/strings/TrimSpace - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Removes leading and trailing white space (as defined by Unicode) - // example 1: TrimSpace(' \t\n Hello, Gophers \n\t\r\n') - // returns 1: 'Hello, Gophers' - - return (s + '').trim() -}
src/golang/strings/TrimSpace.ts+10 −0 added@@ -0,0 +1,10 @@ +export function TrimSpace(s: string): string { + // discuss at: https://locutus.io/golang/strings/TrimSpace + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Removes leading and trailing white space (as defined by Unicode) + // example 1: TrimSpace(' \t\n Hello, Gophers \n\t\r\n') + // returns 1: 'Hello, Gophers' + + return (s + '').trim() +}
src/golang/strings/TrimSuffix.js+0 −14 removed@@ -1,14 +0,0 @@ -module.exports = function TrimSuffix(s, suffix) { - // discuss at: https://locutus.io/golang/strings/TrimSuffix/ - // parity verified: Go 1.23 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: TrimSuffix('Hello, World!', ', World!') - // returns 1: 'Hello' - // example 2: TrimSuffix('Hello, World!', 'Goodbye') - // returns 2: 'Hello, World!' - - if (s.endsWith(suffix)) { - return s.slice(0, s.length - suffix.length) - } - return s -}
src/golang/strings/TrimSuffix.ts+14 −0 added@@ -0,0 +1,14 @@ +export function TrimSuffix(s: string, suffix: string): string { + // discuss at: https://locutus.io/golang/strings/TrimSuffix/ + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: TrimSuffix('Hello, World!', ', World!') + // returns 1: 'Hello' + // example 2: TrimSuffix('Hello, World!', 'Goodbye') + // returns 2: 'Hello, World!' + + if (s.endsWith(suffix)) { + return s.slice(0, s.length - suffix.length) + } + return s +}
src/golang/strings/Trim.ts+19 −0 added@@ -0,0 +1,19 @@ +export function Trim(s: string, cutset: string): string { + // discuss at: https://locutus.io/golang/strings/Trim + // parity verified: Go 1.23 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Unlike Go's strings.Trim which removes characters in cutset, + // note 1: this implementation removes exact matches of cutset from both ends + // example 1: Trim('!!!Hello, Gophers!!!', '!') + // returns 1: 'Hello, Gophers' + // example 2: Trim(' Hello ', ' ') + // returns 2: 'Hello' + + s = s + '' + cutset = cutset + '' + + // Build regex to match cutset characters at start and end + const escaped = cutset.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const regex = new RegExp('^[' + escaped + ']+|[' + escaped + ']+$', 'g') + return s.replace(regex, '') +}
src/index.js+0 −5 removed@@ -1,5 +0,0 @@ -module.exports.c = require('./c') -module.exports.golang = require('./golang') -module.exports.php = require('./php') -module.exports.python = require('./python') -module.exports.ruby = require('./ruby')
src/index.ts+5 −0 added@@ -0,0 +1,5 @@ +export * as c from './c/index.ts' +export * as golang from './golang/index.ts' +export * as php from './php/index.ts' +export * as python from './python/index.ts' +export * as ruby from './ruby/index.ts'
src/julia/Base/abs.js+0 −13 removed@@ -1,13 +0,0 @@ -module.exports = function abs(x) { - // discuss at: https://locutus.io/julia/abs/ - // parity verified: Julia 1.11 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: abs(-5) - // returns 1: 5 - // example 2: abs(3.14) - // returns 2: 3.14 - // example 3: abs(0) - // returns 3: 0 - - return Math.abs(x) -}
src/julia/Base/abs.ts+13 −0 added@@ -0,0 +1,13 @@ +export function abs(x: number): number { + // discuss at: https://locutus.io/julia/abs/ + // parity verified: Julia 1.11 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: abs(-5) + // returns 1: 5 + // example 2: abs(3.14) + // returns 2: 3.14 + // example 3: abs(0) + // returns 3: 0 + + return Math.abs(x) +}
src/julia/Base/ceil.js+0 −16 removed@@ -1,16 +0,0 @@ -module.exports = function ceil(x) { - // discuss at: https://locutus.io/julia/ceil/ - // parity verified: Julia 1.11 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns the smallest integer >= x - // example 1: ceil(4.2) - // returns 1: 5 - // example 2: ceil(-0.5) - // returns 2: 0 - // example 3: ceil(3) - // returns 3: 3 - - const result = Math.ceil(x) - // Convert -0 to 0 for consistency - return Object.is(result, -0) ? 0 : result -}
src/julia/Base/ceil.ts+16 −0 added@@ -0,0 +1,16 @@ +export function ceil(x: number): number { + // discuss at: https://locutus.io/julia/ceil/ + // parity verified: Julia 1.11 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns the smallest integer >= x + // example 1: ceil(4.2) + // returns 1: 5 + // example 2: ceil(-0.5) + // returns 2: 0 + // example 3: ceil(3) + // returns 3: 3 + + const result = Math.ceil(x) + // Convert -0 to 0 for consistency + return Object.is(result, -0) ? 0 : result +}
src/julia/Base/floor.js+0 −14 removed@@ -1,14 +0,0 @@ -module.exports = function floor(x) { - // discuss at: https://locutus.io/julia/floor/ - // parity verified: Julia 1.11 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns the largest integer <= x - // example 1: floor(4.7) - // returns 1: 4 - // example 2: floor(-0.5) - // returns 2: -1 - // example 3: floor(3) - // returns 3: 3 - - return Math.floor(x) -}
src/julia/Base/floor.ts+14 −0 added@@ -0,0 +1,14 @@ +export function floor(x: number): number { + // discuss at: https://locutus.io/julia/floor/ + // parity verified: Julia 1.11 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns the largest integer <= x + // example 1: floor(4.7) + // returns 1: 4 + // example 2: floor(-0.5) + // returns 2: -1 + // example 3: floor(3) + // returns 3: 3 + + return Math.floor(x) +}
src/julia/Base/index.js+0 −5 removed@@ -1,5 +0,0 @@ -module.exports.abs = require('./abs') -module.exports.ceil = require('./ceil') -module.exports.floor = require('./floor') -module.exports.lowercase = require('./lowercase') -module.exports.uppercase = require('./uppercase')
src/julia/Base/index.ts+5 −0 added@@ -0,0 +1,5 @@ +export { abs } from './abs.ts' +export { ceil } from './ceil.ts' +export { floor } from './floor.ts' +export { lowercase } from './lowercase.ts' +export { uppercase } from './uppercase.ts'
src/julia/Base/lowercase.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function lowercase(s) { - // discuss at: https://locutus.io/julia/lowercase/ - // parity verified: Julia 1.11 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: lowercase('HELLO') - // returns 1: 'hello' - // example 2: lowercase('Hello World') - // returns 2: 'hello world' - - return String(s).toLowerCase() -}
src/julia/Base/lowercase.ts+11 −0 added@@ -0,0 +1,11 @@ +export function lowercase(s: string): string { + // discuss at: https://locutus.io/julia/lowercase/ + // parity verified: Julia 1.11 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: lowercase('HELLO') + // returns 1: 'hello' + // example 2: lowercase('Hello World') + // returns 2: 'hello world' + + return String(s).toLowerCase() +}
src/julia/Base/uppercase.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function uppercase(s) { - // discuss at: https://locutus.io/julia/uppercase/ - // parity verified: Julia 1.11 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: uppercase('hello') - // returns 1: 'HELLO' - // example 2: uppercase('Hello World') - // returns 2: 'HELLO WORLD' - - return String(s).toUpperCase() -}
src/julia/Base/uppercase.ts+11 −0 added@@ -0,0 +1,11 @@ +export function uppercase(s: string): string { + // discuss at: https://locutus.io/julia/uppercase/ + // parity verified: Julia 1.11 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: uppercase('hello') + // returns 1: 'HELLO' + // example 2: uppercase('Hello World') + // returns 2: 'HELLO WORLD' + + return String(s).toUpperCase() +}
src/lua/math/abs.js+0 −13 removed@@ -1,13 +0,0 @@ -module.exports = function abs(x) { - // discuss at: https://locutus.io/lua/abs/ - // parity verified: Lua 5.4 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: abs(-5) - // returns 1: 5 - // example 2: abs(3.14) - // returns 2: 3.14 - // example 3: abs(0) - // returns 3: 0 - - return Math.abs(x) -}
src/lua/math/abs.ts+13 −0 added@@ -0,0 +1,13 @@ +export function abs(x: number): number { + // discuss at: https://locutus.io/lua/abs/ + // parity verified: Lua 5.4 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: abs(-5) + // returns 1: 5 + // example 2: abs(3.14) + // returns 2: 3.14 + // example 3: abs(0) + // returns 3: 0 + + return Math.abs(x) +}
src/lua/math/ceil.js+0 −16 removed@@ -1,16 +0,0 @@ -module.exports = function ceil(x) { - // discuss at: https://locutus.io/lua/ceil/ - // parity verified: Lua 5.4 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns the smallest integer >= x - // example 1: ceil(4.2) - // returns 1: 5 - // example 2: ceil(-0.5) - // returns 2: 0 - // example 3: ceil(3) - // returns 3: 3 - - const result = Math.ceil(x) - // Convert -0 to 0 for consistency - return Object.is(result, -0) ? 0 : result -}
src/lua/math/ceil.ts+16 −0 added@@ -0,0 +1,16 @@ +export function ceil(x: number): number { + // discuss at: https://locutus.io/lua/ceil/ + // parity verified: Lua 5.4 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns the smallest integer >= x + // example 1: ceil(4.2) + // returns 1: 5 + // example 2: ceil(-0.5) + // returns 2: 0 + // example 3: ceil(3) + // returns 3: 3 + + const result = Math.ceil(x) + // Convert -0 to 0 for consistency + return Object.is(result, -0) ? 0 : result +}
src/lua/math/cos.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function cos(x) { - // discuss at: https://locutus.io/lua/cos/ - // parity verified: Lua 5.4 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: cos(0) - // returns 1: 1 - // example 2: cos(1) - // returns 2: 0.5403023058681398 - - return Math.cos(x) -}
src/lua/math/cos.ts+11 −0 added@@ -0,0 +1,11 @@ +export function cos(x: number): number { + // discuss at: https://locutus.io/lua/cos/ + // parity verified: Lua 5.4 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: cos(0) + // returns 1: 1 + // example 2: cos(1) + // returns 2: 0.5403023058681398 + + return Math.cos(x) +}
src/lua/math/floor.js+0 −14 removed@@ -1,14 +0,0 @@ -module.exports = function floor(x) { - // discuss at: https://locutus.io/lua/floor/ - // parity verified: Lua 5.4 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns the largest integer <= x - // example 1: floor(4.7) - // returns 1: 4 - // example 2: floor(-0.5) - // returns 2: -1 - // example 3: floor(3) - // returns 3: 3 - - return Math.floor(x) -}
src/lua/math/floor.ts+14 −0 added@@ -0,0 +1,14 @@ +export function floor(x: number): number { + // discuss at: https://locutus.io/lua/floor/ + // parity verified: Lua 5.4 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns the largest integer <= x + // example 1: floor(4.7) + // returns 1: 4 + // example 2: floor(-0.5) + // returns 2: -1 + // example 3: floor(3) + // returns 3: 3 + + return Math.floor(x) +}
src/lua/math/index.js+0 −8 removed@@ -1,8 +0,0 @@ -module.exports.abs = require('./abs') -module.exports.ceil = require('./ceil') -module.exports.cos = require('./cos') -module.exports.floor = require('./floor') -module.exports.max = require('./max') -module.exports.min = require('./min') -module.exports.sin = require('./sin') -module.exports.sqrt = require('./sqrt')
src/lua/math/index.ts+8 −0 added@@ -0,0 +1,8 @@ +export { abs } from './abs.ts' +export { ceil } from './ceil.ts' +export { cos } from './cos.ts' +export { floor } from './floor.ts' +export { max } from './max.ts' +export { min } from './min.ts' +export { sin } from './sin.ts' +export { sqrt } from './sqrt.ts'
src/lua/math/max.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function max(...args) { - // discuss at: https://locutus.io/lua/max/ - // parity verified: Lua 5.4 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: max(1, 5, 3) - // returns 1: 5 - // example 2: max(-1, -5) - // returns 2: -1 - - return Math.max(...args) -}
src/lua/math/max.ts+11 −0 added@@ -0,0 +1,11 @@ +export function max(...args: number[]): number { + // discuss at: https://locutus.io/lua/max/ + // parity verified: Lua 5.4 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: max(1, 5, 3) + // returns 1: 5 + // example 2: max(-1, -5) + // returns 2: -1 + + return Math.max(...args) +}
src/lua/math/min.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function min(...args) { - // discuss at: https://locutus.io/lua/min/ - // parity verified: Lua 5.4 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: min(1, 5, 3) - // returns 1: 1 - // example 2: min(-1, -5) - // returns 2: -5 - - return Math.min(...args) -}
src/lua/math/min.ts+11 −0 added@@ -0,0 +1,11 @@ +export function min(...args: number[]): number { + // discuss at: https://locutus.io/lua/min/ + // parity verified: Lua 5.4 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: min(1, 5, 3) + // returns 1: 1 + // example 2: min(-1, -5) + // returns 2: -5 + + return Math.min(...args) +}
src/lua/math/sin.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function sin(x) { - // discuss at: https://locutus.io/lua/sin/ - // parity verified: Lua 5.4 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: sin(0) - // returns 1: 0 - // example 2: sin(1) - // returns 2: 0.8414709848078965 - - return Math.sin(x) -}
src/lua/math/sin.ts+11 −0 added@@ -0,0 +1,11 @@ +export function sin(x: number): number { + // discuss at: https://locutus.io/lua/sin/ + // parity verified: Lua 5.4 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: sin(0) + // returns 1: 0 + // example 2: sin(1) + // returns 2: 0.8414709848078965 + + return Math.sin(x) +}
src/lua/math/sqrt.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function sqrt(x) { - // discuss at: https://locutus.io/lua/sqrt/ - // parity verified: Lua 5.4 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: sqrt(16) - // returns 1: 4 - // example 2: sqrt(2) - // returns 2: 1.4142135623730951 - - return Math.sqrt(x) -}
src/lua/math/sqrt.ts+11 −0 added@@ -0,0 +1,11 @@ +export function sqrt(x: number): number { + // discuss at: https://locutus.io/lua/sqrt/ + // parity verified: Lua 5.4 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: sqrt(16) + // returns 1: 4 + // example 2: sqrt(2) + // returns 2: 1.4142135623730951 + + return Math.sqrt(x) +}
src/lua/string/index.js+0 −6 removed@@ -1,6 +0,0 @@ -module.exports.len = require('./len') -module.exports.lower = require('./lower') -module.exports.rep = require('./rep') -module.exports.reverse = require('./reverse') -module.exports.sub = require('./sub') -module.exports.upper = require('./upper')
src/lua/string/index.ts+6 −0 added@@ -0,0 +1,6 @@ +export { len } from './len.ts' +export { lower } from './lower.ts' +export { rep } from './rep.ts' +export { reverse } from './reverse.ts' +export { sub } from './sub.ts' +export { upper } from './upper.ts'
src/lua/string/len.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function len(s) { - // discuss at: https://locutus.io/lua/len/ - // parity verified: Lua 5.4 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: len('hello') - // returns 1: 5 - // example 2: len('') - // returns 2: 0 - - return String(s).length -}
src/lua/string/len.ts+11 −0 added@@ -0,0 +1,11 @@ +export function len(s: string): number { + // discuss at: https://locutus.io/lua/len/ + // parity verified: Lua 5.4 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: len('hello') + // returns 1: 5 + // example 2: len('') + // returns 2: 0 + + return String(s).length +}
src/lua/string/lower.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function lower(s) { - // discuss at: https://locutus.io/lua/lower/ - // parity verified: Lua 5.4 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: lower('HELLO') - // returns 1: 'hello' - // example 2: lower('Hello World') - // returns 2: 'hello world' - - return String(s).toLowerCase() -}
src/lua/string/lower.ts+11 −0 added@@ -0,0 +1,11 @@ +export function lower(s: string): string { + // discuss at: https://locutus.io/lua/lower/ + // parity verified: Lua 5.4 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: lower('HELLO') + // returns 1: 'hello' + // example 2: lower('Hello World') + // returns 2: 'hello world' + + return String(s).toLowerCase() +}
src/lua/string/rep.js+0 −14 removed@@ -1,14 +0,0 @@ -module.exports = function rep(s, n) { - // discuss at: https://locutus.io/lua/rep/ - // parity verified: Lua 5.4 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: rep('ab', 3) - // returns 1: 'ababab' - // example 2: rep('x', 5) - // returns 2: 'xxxxx' - - if (n <= 0) { - return '' - } - return String(s).repeat(n) -}
src/lua/string/rep.ts+14 −0 added@@ -0,0 +1,14 @@ +export function rep(s: string, n: number): string { + // discuss at: https://locutus.io/lua/rep/ + // parity verified: Lua 5.4 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: rep('ab', 3) + // returns 1: 'ababab' + // example 2: rep('x', 5) + // returns 2: 'xxxxx' + + if (n <= 0) { + return '' + } + return String(s).repeat(n) +}
src/lua/string/reverse.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function reverse(s) { - // discuss at: https://locutus.io/lua/reverse/ - // parity verified: Lua 5.4 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: reverse('hello') - // returns 1: 'olleh' - // example 2: reverse('abc') - // returns 2: 'cba' - - return String(s).split('').reverse().join('') -}
src/lua/string/reverse.ts+11 −0 added@@ -0,0 +1,11 @@ +export function reverse(s: string): string { + // discuss at: https://locutus.io/lua/reverse/ + // parity verified: Lua 5.4 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: reverse('hello') + // returns 1: 'olleh' + // example 2: reverse('abc') + // returns 2: 'cba' + + return String(s).split('').reverse().join('') +}
src/lua/string/sub.js+0 −33 removed@@ -1,33 +0,0 @@ -module.exports = function sub(s, i, j) { - // discuss at: https://locutus.io/lua/sub/ - // parity verified: Lua 5.4 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Lua uses 1-based indexing, negative indices count from end - // example 1: sub('Hello', 1, 3) - // returns 1: 'Hel' - // example 2: sub('Hello', 2) - // returns 2: 'ello' - // example 3: sub('Hello', -2) - // returns 3: 'lo' - - s = String(s) - const len = s.length - - // Convert Lua 1-based index to JS 0-based - // Negative indices count from end in Lua - let start = i < 0 ? len + i : i - 1 - let end = j === undefined ? len : j < 0 ? len + j + 1 : j - - // Clamp to valid range - if (start < 0) { - start = 0 - } - if (end > len) { - end = len - } - if (start >= end) { - return '' - } - - return s.substring(start, end) -}
src/lua/string/sub.ts+33 −0 added@@ -0,0 +1,33 @@ +export function sub(s: string, i: number, j?: number): string { + // discuss at: https://locutus.io/lua/sub/ + // parity verified: Lua 5.4 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Lua uses 1-based indexing, negative indices count from end + // example 1: sub('Hello', 1, 3) + // returns 1: 'Hel' + // example 2: sub('Hello', 2) + // returns 2: 'ello' + // example 3: sub('Hello', -2) + // returns 3: 'lo' + + s = String(s) + const len = s.length + + // Convert Lua 1-based index to JS 0-based + // Negative indices count from end in Lua + let start = i < 0 ? len + i : i - 1 + let end = j === undefined ? len : j < 0 ? len + j + 1 : j + + // Clamp to valid range + if (start < 0) { + start = 0 + } + if (end > len) { + end = len + } + if (start >= end) { + return '' + } + + return s.substring(start, end) +}
src/lua/string/upper.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function upper(s) { - // discuss at: https://locutus.io/lua/upper/ - // parity verified: Lua 5.4 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: upper('hello') - // returns 1: 'HELLO' - // example 2: upper('Hello World') - // returns 2: 'HELLO WORLD' - - return String(s).toUpperCase() -}
src/lua/string/upper.ts+11 −0 added@@ -0,0 +1,11 @@ +export function upper(s: string): string { + // discuss at: https://locutus.io/lua/upper/ + // parity verified: Lua 5.4 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: upper('hello') + // returns 1: 'HELLO' + // example 2: upper('Hello World') + // returns 2: 'HELLO WORLD' + + return String(s).toUpperCase() +}
src/perl/core/index.js+0 −5 removed@@ -1,5 +0,0 @@ -module.exports.lc = require('./lc') -module.exports.length = require('./length') -module.exports.reverse = require('./reverse') -module.exports.substr = require('./substr') -module.exports.uc = require('./uc')
src/perl/core/index.ts+5 −0 added@@ -0,0 +1,5 @@ +export { lc } from './lc.ts' +export { length } from './length.ts' +export { reverse } from './reverse.ts' +export { substr } from './substr.ts' +export { uc } from './uc.ts'
src/perl/core/lc.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function lc(str) { - // discuss at: https://locutus.io/perl/lc/ - // parity verified: Perl 5.40 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: lc('HELLO') - // returns 1: 'hello' - // example 2: lc('Hello World') - // returns 2: 'hello world' - - return String(str).toLowerCase() -}
src/perl/core/lc.ts+11 −0 added@@ -0,0 +1,11 @@ +export function lc(str: string): string { + // discuss at: https://locutus.io/perl/lc/ + // parity verified: Perl 5.40 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: lc('HELLO') + // returns 1: 'hello' + // example 2: lc('Hello World') + // returns 2: 'hello world' + + return String(str).toLowerCase() +}
src/perl/core/length.js+0 −16 removed@@ -1,16 +0,0 @@ -module.exports = function length(str) { - // discuss at: https://locutus.io/perl/length/ - // parity verified: Perl 5.40 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: length('hello') - // returns 1: 5 - // example 2: length('') - // returns 2: 0 - // example 3: length('hello world') - // returns 3: 11 - - if (str === undefined || str === null) { - return undefined - } - return String(str).length -}
src/perl/core/length.ts+16 −0 added@@ -0,0 +1,16 @@ +export function length(str: string | null | undefined): number | undefined { + // discuss at: https://locutus.io/perl/length/ + // parity verified: Perl 5.40 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: length('hello') + // returns 1: 5 + // example 2: length('') + // returns 2: 0 + // example 3: length('hello world') + // returns 3: 11 + + if (str === undefined || str === null) { + return undefined + } + return String(str).length +}
src/perl/core/reverse.js+0 −12 removed@@ -1,12 +0,0 @@ -module.exports = function reverse(str) { - // discuss at: https://locutus.io/perl/reverse/ - // parity verified: Perl 5.40 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: In scalar context, reverses string characters - // example 1: reverse('hello') - // returns 1: 'olleh' - // example 2: reverse('abc') - // returns 2: 'cba' - - return String(str).split('').reverse().join('') -}
src/perl/core/reverse.ts+12 −0 added@@ -0,0 +1,12 @@ +export function reverse(str: string): string { + // discuss at: https://locutus.io/perl/reverse/ + // parity verified: Perl 5.40 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: In scalar context, reverses string characters + // example 1: reverse('hello') + // returns 1: 'olleh' + // example 2: reverse('abc') + // returns 2: 'cba' + + return String(str).split('').reverse().join('') +}
src/perl/core/substr.js+0 −44 removed@@ -1,44 +0,0 @@ -module.exports = function substr(str, offset, length) { - // discuss at: https://locutus.io/perl/substr/ - // parity verified: Perl 5.40 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Perl's offset is 0-based, negative counts from end - // example 1: substr('hello', 0, 3) - // returns 1: 'hel' - // example 2: substr('hello', 1) - // returns 2: 'ello' - // example 3: substr('hello', -2) - // returns 3: 'lo' - - str = String(str) - const len = str.length - - // Handle negative offset (count from end) - let start = offset < 0 ? len + offset : offset - - // Clamp start to valid range - if (start < 0) { - start = 0 - } - if (start > len) { - return '' - } - - // If length is undefined, return rest of string - if (length === undefined) { - return str.substring(start) - } - - // Handle negative length (leave that many chars at end) - let end - if (length < 0) { - end = len + length - if (end < start) { - return '' - } - } else { - end = start + length - } - - return str.substring(start, end) -}
src/perl/core/substr.ts+44 −0 added@@ -0,0 +1,44 @@ +export function substr(str: string, offset: number, length?: number): string { + // discuss at: https://locutus.io/perl/substr/ + // parity verified: Perl 5.40 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Perl's offset is 0-based, negative counts from end + // example 1: substr('hello', 0, 3) + // returns 1: 'hel' + // example 2: substr('hello', 1) + // returns 2: 'ello' + // example 3: substr('hello', -2) + // returns 3: 'lo' + + str = String(str) + const len = str.length + + // Handle negative offset (count from end) + let start = offset < 0 ? len + offset : offset + + // Clamp start to valid range + if (start < 0) { + start = 0 + } + if (start > len) { + return '' + } + + // If length is undefined, return rest of string + if (length === undefined) { + return str.substring(start) + } + + // Handle negative length (leave that many chars at end) + let end + if (length < 0) { + end = len + length + if (end < start) { + return '' + } + } else { + end = start + length + } + + return str.substring(start, end) +}
src/perl/core/uc.js+0 −11 removed@@ -1,11 +0,0 @@ -module.exports = function uc(str) { - // discuss at: https://locutus.io/perl/uc/ - // parity verified: Perl 5.40 - // original by: Kevin van Zonneveld (https://kvz.io) - // example 1: uc('hello') - // returns 1: 'HELLO' - // example 2: uc('Hello World') - // returns 2: 'HELLO WORLD' - - return String(str).toUpperCase() -}
src/perl/core/uc.ts+11 −0 added@@ -0,0 +1,11 @@ +export function uc(str: string): string { + // discuss at: https://locutus.io/perl/uc/ + // parity verified: Perl 5.40 + // original by: Kevin van Zonneveld (https://kvz.io) + // example 1: uc('hello') + // returns 1: 'HELLO' + // example 2: uc('Hello World') + // returns 2: 'HELLO WORLD' + + return String(str).toUpperCase() +}
src/perl/POSIX/ceil.js+0 −16 removed@@ -1,16 +0,0 @@ -module.exports = function ceil(x) { - // discuss at: https://locutus.io/perl/ceil/ - // parity verified: Perl 5.40 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns the smallest integer >= x - // example 1: ceil(4.2) - // returns 1: 5 - // example 2: ceil(-0.5) - // returns 2: 0 - // example 3: ceil(3) - // returns 3: 3 - - const result = Math.ceil(x) - // Convert -0 to 0 for consistency - return Object.is(result, -0) ? 0 : result -}
src/perl/POSIX/ceil.ts+16 −0 added@@ -0,0 +1,16 @@ +export function ceil(x: number): number { + // discuss at: https://locutus.io/perl/ceil/ + // parity verified: Perl 5.40 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns the smallest integer >= x + // example 1: ceil(4.2) + // returns 1: 5 + // example 2: ceil(-0.5) + // returns 2: 0 + // example 3: ceil(3) + // returns 3: 3 + + const result = Math.ceil(x) + // Convert -0 to 0 for consistency + return Object.is(result, -0) ? 0 : result +}
src/perl/POSIX/floor.js+0 −14 removed@@ -1,14 +0,0 @@ -module.exports = function floor(x) { - // discuss at: https://locutus.io/perl/floor/ - // parity verified: Perl 5.40 - // original by: Kevin van Zonneveld (https://kvz.io) - // note 1: Returns the largest integer <= x - // example 1: floor(4.7) - // returns 1: 4 - // example 2: floor(-0.5) - // returns 2: -1 - // example 3: floor(3) - // returns 3: 3 - - return Math.floor(x) -}
src/perl/POSIX/floor.ts+14 −0 added@@ -0,0 +1,14 @@ +export function floor(x: number): number { + // discuss at: https://locutus.io/perl/floor/ + // parity verified: Perl 5.40 + // original by: Kevin van Zonneveld (https://kvz.io) + // note 1: Returns the largest integer <= x + // example 1: floor(4.7) + // returns 1: 4 + // example 2: floor(-0.5) + // returns 2: -1 + // example 3: floor(3) + // returns 3: 3 + + return Math.floor(x) +}
src/perl/POSIX/index.js+0 −2 removed@@ -1,2 +0,0 @@ -module.exports.ceil = require('./ceil') -module.exports.floor = require('./floor')
src/perl/POSIX/index.ts+2 −0 added@@ -0,0 +1,2 @@ +export { ceil } from './ceil.ts' +export { floor } from './floor.ts'
src/php/array/array_change_key_case.js+0 −36 removed@@ -1,36 +0,0 @@ -module.exports = function array_change_key_case(array, cs) { - // discuss at: https://locutus.io/php/array_change_key_case/ - // original by: Ates Goral (https://magnetiq.com) - // improved by: marrtins - // improved by: Brett Zamir (https://brett-zamir.me) - // example 1: array_change_key_case(42) - // returns 1: false - // example 2: array_change_key_case([ 3, 5 ]) - // returns 2: [3, 5] - // example 3: array_change_key_case({ FuBaR: 42 }) - // returns 3: {"fubar": 42} - // example 4: array_change_key_case({ FuBaR: 42 }, 'CASE_LOWER') - // returns 4: {"fubar": 42} - // example 5: array_change_key_case({ FuBaR: 42 }, 'CASE_UPPER') - // returns 5: {"FUBAR": 42} - // example 6: array_change_key_case({ FuBaR: 42 }, 2) - // returns 6: {"FUBAR": 42} - - let caseFnc - let key - const tmpArr = {} - - if (Array.isArray(array)) { - return array - } - - if (array && typeof array === 'object') { - caseFnc = !cs || cs === 'CASE_LOWER' ? 'toLowerCase' : 'toUpperCase' - for (key in array) { - tmpArr[key[caseFnc]()] = array[key] - } - return tmpArr - } - - return false -}
src/php/array/array_change_key_case.ts+53 −0 added@@ -0,0 +1,53 @@ +import { type PhpArrayLike, type PhpAssoc, type PhpRuntimeValue, toPhpArrayObject } from '../_helpers/_phpTypes.ts' + +type ChangeValue = PhpRuntimeValue +type ArrayChangeInput<TValue extends ChangeValue> = number | PhpArrayLike<TValue> | null +type ChangeKeyCaseMode = 0 | 1 | 2 | 'CASE_LOWER' | 'CASE_UPPER' + +export function array_change_key_case(array: number | null, cs?: ChangeKeyCaseMode): false +export function array_change_key_case<TValue extends ChangeValue>(array: TValue[], cs?: ChangeKeyCaseMode): TValue[] +export function array_change_key_case<TValue extends ChangeValue>( + array: PhpAssoc<TValue>, + cs?: ChangeKeyCaseMode, +): PhpAssoc<TValue> +export function array_change_key_case<TValue extends ChangeValue>( + array: ArrayChangeInput<TValue>, + cs?: ChangeKeyCaseMode, +): PhpArrayLike<TValue> | false { + // discuss at: https://locutus.io/php/array_change_key_case/ + // original by: Ates Goral (https://magnetiq.com) + // improved by: marrtins + // improved by: Brett Zamir (https://brett-zamir.me) + // example 1: array_change_key_case(42) + // returns 1: false + // example 2: array_change_key_case([ 3, 5 ]) + // returns 2: [3, 5] + // example 3: array_change_key_case({ FuBaR: 42 }) + // returns 3: {"fubar": 42} + // example 4: array_change_key_case({ FuBaR: 42 }, 'CASE_LOWER') + // returns 4: {"fubar": 42} + // example 5: array_change_key_case({ FuBaR: 42 }, 'CASE_UPPER') + // returns 5: {"FUBAR": 42} + // example 6: array_change_key_case({ FuBaR: 42 }, 2) + // returns 6: {"FUBAR": 42} + + let result: PhpArrayLike<TValue> | false + + if (Array.isArray(array)) { + result = array + } else if (!array || typeof array !== 'object') { + result = false + } else { + const caseFunction: 'toLowerCase' | 'toUpperCase' = + cs === undefined || cs === 0 || cs === 'CASE_LOWER' ? 'toLowerCase' : 'toUpperCase' + const source = toPhpArrayObject<TValue>(array) + const transformed: PhpAssoc<TValue> = {} + + for (const [key, value] of Object.entries(source)) { + transformed[key[caseFunction]()] = value + } + result = transformed + } + + return result +}
src/php/array/array_chunk.js+0 −60 removed@@ -1,60 +0,0 @@ -module.exports = function array_chunk(input, size, preserveKeys) { - // discuss at: https://locutus.io/php/array_chunk/ - // original by: Carlos R. L. Rodrigues (https://www.jsfromhell.com) - // improved by: Brett Zamir (https://brett-zamir.me) - // note 1: Important note: Per the ECMAScript specification, - // note 1: objects may not always iterate in a predictable order - // example 1: array_chunk(['Kevin', 'van', 'Zonneveld'], 2) - // returns 1: [['Kevin', 'van'], ['Zonneveld']] - // example 2: array_chunk(['Kevin', 'van', 'Zonneveld'], 2, true) - // returns 2: [{0:'Kevin', 1:'van'}, {2: 'Zonneveld'}] - // example 3: array_chunk({1:'Kevin', 2:'van', 3:'Zonneveld'}, 2) - // returns 3: [['Kevin', 'van'], ['Zonneveld']] - // example 4: array_chunk({1:'Kevin', 2:'van', 3:'Zonneveld'}, 2, true) - // returns 4: [{1: 'Kevin', 2: 'van'}, {3: 'Zonneveld'}] - - let x - let p = '' - let i = 0 - let c = -1 - const l = input.length || 0 - const n = [] - - if (size < 1) { - return null - } - - if (Array.isArray(input)) { - if (preserveKeys) { - while (i < l) { - ;(x = i % size) ? (n[c][i] = input[i]) : (n[++c] = {}) - n[c][i] = input[i] - i++ - } - } else { - while (i < l) { - ;(x = i % size) ? (n[c][x] = input[i]) : (n[++c] = [input[i]]) - i++ - } - } - } else { - if (preserveKeys) { - for (p in input) { - if (input.hasOwnProperty(p)) { - ;(x = i % size) ? (n[c][p] = input[p]) : (n[++c] = {}) - n[c][p] = input[p] - i++ - } - } - } else { - for (p in input) { - if (input.hasOwnProperty(p)) { - ;(x = i % size) ? (n[c][x] = input[p]) : (n[++c] = [input[p]]) - i++ - } - } - } - } - - return n -}
src/php/array/array_chunk.ts+77 −0 added@@ -0,0 +1,77 @@ +import type { PhpAssoc } from '../_helpers/_phpTypes.ts' + +type ArrayChunkInput<T> = T[] | PhpAssoc<T> +type ArrayChunkOutput<T> = T[] | PhpAssoc<T> + +export function array_chunk<T>( + input: ArrayChunkInput<T>, + size: number, + preserveKeys?: boolean, +): ArrayChunkOutput<T>[] | null { + // discuss at: https://locutus.io/php/array_chunk/ + // original by: Carlos R. L. Rodrigues (https://www.jsfromhell.com) + // improved by: Brett Zamir (https://brett-zamir.me) + // note 1: Important note: Per the ECMAScript specification, + // note 1: objects may not always iterate in a predictable order + // example 1: array_chunk(['Kevin', 'van', 'Zonneveld'], 2) + // returns 1: [['Kevin', 'van'], ['Zonneveld']] + // example 2: array_chunk(['Kevin', 'van', 'Zonneveld'], 2, true) + // returns 2: [{0:'Kevin', 1:'van'}, {2: 'Zonneveld'}] + // example 3: array_chunk({1:'Kevin', 2:'van', 3:'Zonneveld'}, 2) + // returns 3: [['Kevin', 'van'], ['Zonneveld']] + // example 4: array_chunk({1:'Kevin', 2:'van', 3:'Zonneveld'}, 2, true) + // returns 4: [{1: 'Kevin', 2: 'van'}, {3: 'Zonneveld'}] + + if (size < 1) { + return null + } + + const keepKeys = Boolean(preserveKeys) + + if (Array.isArray(input)) { + if (keepKeys) { + const chunks: PhpAssoc<T>[] = [] + for (const [i, value] of input.entries()) { + const chunkIndex = Math.floor(i / size) + if (!chunks[chunkIndex]) { + chunks[chunkIndex] = {} + } + chunks[chunkIndex][String(i)] = value + } + return chunks + } else { + const chunks: T[][] = [] + for (const [i, value] of input.entries()) { + const chunkIndex = Math.floor(i / size) + if (!chunks[chunkIndex]) { + chunks[chunkIndex] = [] + } + chunks[chunkIndex].push(value) + } + return chunks + } + } else { + const inputEntries = Object.entries(input) + if (keepKeys) { + const chunks: PhpAssoc<T>[] = [] + for (const [i, [key, value]] of inputEntries.entries()) { + const chunkIndex = Math.floor(i / size) + if (!chunks[chunkIndex]) { + chunks[chunkIndex] = {} + } + chunks[chunkIndex][key] = value + } + return chunks + } else { + const chunks: T[][] = [] + for (const [i, [, value]] of inputEntries.entries()) { + const chunkIndex = Math.floor(i / size) + if (!chunks[chunkIndex]) { + chunks[chunkIndex] = [] + } + chunks[chunkIndex].push(value) + } + return chunks + } + } +}
src/php/array/array_column.js+0 −41 removed@@ -1,41 +0,0 @@ -module.exports = function array_column(input, ColumnKey, IndexKey = null) { - // discuss at: https://locutus.io/php/array_column/ - // original by: Enzo Dañobeytía - // example 1: array_column([{name: 'Alex', value: 1}, {name: 'Elvis', value: 2}, {name: 'Michael', value: 3}], 'name') - // returns 1: {0: "Alex", 1: "Elvis", 2: "Michael"} - // example 2: array_column({0: {name: 'Alex', value: 1}, 1: {name: 'Elvis', value: 2}, 2: {name: 'Michael', value: 3}}, 'name') - // returns 2: {0: "Alex", 1: "Elvis", 2: "Michael"} - // example 3: array_column([{name: 'Alex', value: 1}, {name: 'Elvis', value: 2}, {name: 'Michael', value: 3}], 'name', 'value') - // returns 3: {1: "Alex", 2: "Elvis", 3: "Michael"} - // example 4: array_column([{name: 'Alex', value: 1}, {name: 'Elvis', value: 2}, {name: 'Michael', value: 3}], null, 'value') - // returns 4: {1: {name: 'Alex', value: 1}, 2: {name: 'Elvis', value: 2}, 3: {name: 'Michael', value: 3}} - - if (input !== null && (typeof input === 'object' || Array.isArray(input))) { - const newarray = [] - if (typeof input === 'object') { - const temparray = [] - for (const key of Object.keys(input)) { - temparray.push(input[key]) - } - input = temparray - } - if (Array.isArray(input)) { - for (const key of input.keys()) { - if (IndexKey && input[key][IndexKey]) { - if (ColumnKey) { - newarray[input[key][IndexKey]] = input[key][ColumnKey] - } else { - newarray[input[key][IndexKey]] = input[key] - } - } else { - if (ColumnKey) { - newarray.push(input[key][ColumnKey]) - } else { - newarray.push(input[key]) - } - } - } - } - return Object.assign({}, newarray) - } -}
src/php/array/array_column.ts+51 −0 added@@ -0,0 +1,51 @@ +import type { PhpAssoc } from '../_helpers/_phpTypes.ts' +import { toPhpArrayObject } from '../_helpers/_phpTypes.ts' + +type ColumnRow<TValue> = PhpAssoc<TValue> +type ColumnInput<TValue> = ColumnRow<TValue>[] | PhpAssoc<ColumnRow<TValue>> +type ColumnOutput<TValue> = PhpAssoc<TValue | ColumnRow<TValue> | undefined> + +export function array_column<TValue>( + input: ColumnInput<TValue>, + columnKey: string | number | null, + indexKey?: string | number | null, +): ColumnOutput<TValue> +export function array_column<TValue>( + input: ColumnInput<TValue> | null | undefined, + columnKey: string | number | null, + indexKey?: string | number | null, +): ColumnOutput<TValue> | undefined { + // discuss at: https://locutus.io/php/array_column/ + // original by: Enzo Dañobeytía + // example 1: array_column([{name: 'Alex', value: 1}, {name: 'Elvis', value: 2}, {name: 'Michael', value: 3}], 'name') + // returns 1: {0: "Alex", 1: "Elvis", 2: "Michael"} + // example 2: array_column({0: {name: 'Alex', value: 1}, 1: {name: 'Elvis', value: 2}, 2: {name: 'Michael', value: 3}}, 'name') + // returns 2: {0: "Alex", 1: "Elvis", 2: "Michael"} + // example 3: array_column([{name: 'Alex', value: 1}, {name: 'Elvis', value: 2}, {name: 'Michael', value: 3}], 'name', 'value') + // returns 3: {1: "Alex", 2: "Elvis", 3: "Michael"} + // example 4: array_column([{name: 'Alex', value: 1}, {name: 'Elvis', value: 2}, {name: 'Michael', value: 3}], null, 'value') + // returns 4: {1: {name: 'Alex', value: 1}, 2: {name: 'Elvis', value: 2}, 3: {name: 'Michael', value: 3}} + + if (input === null || typeof input !== 'object') { + return undefined + } + + const normalizedInput = Array.isArray(input) ? input : Object.values(toPhpArrayObject<ColumnRow<TValue>>(input)) + const result: ColumnOutput<TValue> = {} + let fallbackIndex = 0 + + for (const rowValue of normalizedInput) { + const row = toPhpArrayObject<TValue>(rowValue) + const indexCandidate = indexKey === null ? undefined : row[String(indexKey)] + + const value = columnKey === null ? rowValue : row[String(columnKey)] + if (indexCandidate !== undefined && indexCandidate !== null) { + result[String(indexCandidate)] = value + } else { + result[String(fallbackIndex)] = value + fallbackIndex += 1 + } + } + + return result +}
src/php/array/array_combine.js+0 −40 removed@@ -1,40 +0,0 @@ -module.exports = function array_combine(keys, values) { - // discuss at: https://locutus.io/php/array_combine/ - // original by: Kevin van Zonneveld (https://kvz.io) - // improved by: Brett Zamir (https://brett-zamir.me) - // example 1: array_combine([0,1,2], ['kevin','van','zonneveld']) - // returns 1: {0: 'kevin', 1: 'van', 2: 'zonneveld'} - - const newArray = {} - let i = 0 - - // input sanitation - // Only accept arrays or array-like objects - // Require arrays to have a count - if (typeof keys !== 'object') { - return false - } - if (typeof values !== 'object') { - return false - } - if (typeof keys.length !== 'number') { - return false - } - if (typeof values.length !== 'number') { - return false - } - if (!keys.length) { - return false - } - - // number of elements does not match - if (keys.length !== values.length) { - return false - } - - for (i = 0; i < keys.length; i++) { - newArray[keys[i]] = values[i] - } - - return newArray -}
src/php/array/array_combine.ts+49 −0 added@@ -0,0 +1,49 @@ +import type { PhpAssoc } from '../_helpers/_phpTypes.ts' + +export function array_combine<TKey extends string | number, TValue>( + keys: TKey[], + values: TValue[], +): PhpAssoc<TValue> | false { + // discuss at: https://locutus.io/php/array_combine/ + // original by: Kevin van Zonneveld (https://kvz.io) + // improved by: Brett Zamir (https://brett-zamir.me) + // example 1: array_combine([0,1,2], ['kevin','van','zonneveld']) + // returns 1: {0: 'kevin', 1: 'van', 2: 'zonneveld'} + + const newArray: PhpAssoc<TValue> = {} + let i = 0 + + // input sanitation + // Only accept arrays or array-like objects + // Require arrays to have a count + if (typeof keys !== 'object') { + return false + } + if (typeof values !== 'object') { + return false + } + if (typeof keys.length !== 'number') { + return false + } + if (typeof values.length !== 'number') { + return false + } + if (!keys.length) { + return false + } + + // number of elements does not match + if (keys.length !== values.length) { + return false + } + + for (i = 0; i < keys.length; i++) { + const value = values[i] + if (typeof value === 'undefined') { + return false + } + newArray[String(keys[i])] = value + } + + return newArray +}
src/php/array/array_count_values.js+0 −57 removed@@ -1,57 +0,0 @@ -module.exports = function array_count_values(array) { - // discuss at: https://locutus.io/php/array_count_values/ - // parity verified: PHP 8.3 - // original by: Ates Goral (https://magnetiq.com) - // improved by: Michael White (https://getsprink.com) - // improved by: Kevin van Zonneveld (https://kvz.io) - // input by: sankai - // input by: Shingo - // bugfixed by: Brett Zamir (https://brett-zamir.me) - // example 1: array_count_values([ 3, 5, 3, "foo", "bar", "foo" ]) - // returns 1: {3:2, 5:1, "foo":2, "bar":1} - // example 2: array_count_values({ p1: 3, p2: 5, p3: 3, p4: "foo", p5: "bar", p6: "foo" }) - // returns 2: {3:2, 5:1, "foo":2, "bar":1} - // example 3: array_count_values([ true, 4.2, 42, "fubar" ]) - // returns 3: {42:1, "fubar":1} - - const tmpArr = {} - let key = '' - let t = '' - - const _getType = function (obj) { - // Objects are php associative arrays. - let t = typeof obj - t = t.toLowerCase() - if (t === 'object') { - t = 'array' - } - return t - } - - const _countValue = function (tmpArr, value) { - if (typeof value === 'number') { - if (Math.floor(value) !== value) { - return - } - } else if (typeof value !== 'string') { - return - } - - if (value in tmpArr && tmpArr.hasOwnProperty(value)) { - ++tmpArr[value] - } else { - tmpArr[value] = 1 - } - } - - t = _getType(array) - if (t === 'array') { - for (key in array) { - if (array.hasOwnProperty(key)) { - _countValue.call(this, tmpArr, array[key]) - } - } - } - - return tmpArr -}
src/php/array/array_count_values.ts+61 −0 added@@ -0,0 +1,61 @@ +import { + isObjectLike, + type PhpArrayLike, + type PhpAssoc, + type PhpRuntimeValue, + toPhpArrayObject, +} from '../_helpers/_phpTypes.ts' + +type CountValue = PhpRuntimeValue + +export function array_count_values(array: PhpArrayLike<CountValue>): PhpAssoc<number> { + // discuss at: https://locutus.io/php/array_count_values/ + // parity verified: PHP 8.3 + // original by: Ates Goral (https://magnetiq.com) + // improved by: Michael White (https://getsprink.com) + // improved by: Kevin van Zonneveld (https://kvz.io) + // input by: sankai + // input by: Shingo + // bugfixed by: Brett Zamir (https://brett-zamir.me) + // example 1: array_count_values([ 3, 5, 3, "foo", "bar", "foo" ]) + // returns 1: {3:2, 5:1, "foo":2, "bar":1} + // example 2: array_count_values({ p1: 3, p2: 5, p3: 3, p4: "foo", p5: "bar", p6: "foo" }) + // returns 2: {3:2, 5:1, "foo":2, "bar":1} + // example 3: array_count_values([ true, 4.2, 42, "fubar" ]) + // returns 3: {42:1, "fubar":1} + + const tmpArr: PhpAssoc<number> = {} + + const _countValue = function (target: PhpAssoc<number>, value: CountValue): void { + let normalized = '' + if (typeof value === 'number') { + if (Math.floor(value) !== value) { + return + } + normalized = String(value) + } else if (typeof value !== 'string') { + return + } else { + normalized = value + } + + if (normalized in target && Object.prototype.hasOwnProperty.call(target, normalized)) { + target[normalized] = (target[normalized] ?? 0) + 1 + } else { + target[normalized] = 1 + } + } + + if (!isObjectLike(array)) { + return tmpArr + } + + const source = toPhpArrayObject<CountValue>(array) + for (const key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + _countValue(tmpArr, source[key]) + } + } + + return tmpArr +}
src/php/array/array_diff_assoc.js+0 −30 removed@@ -1,30 +0,0 @@ -module.exports = function array_diff_assoc(arr1) { - // discuss at: https://locutus.io/php/array_diff_assoc/ - // original by: Kevin van Zonneveld (https://kvz.io) - // bugfixed by: 0m3r - // revised by: Brett Zamir (https://brett-zamir.me) - // example 1: array_diff_assoc({0: 'Kevin', 1: 'van', 2: 'Zonneveld'}, {0: 'Kevin', 4: 'van', 5: 'Zonneveld'}) - // returns 1: {1: 'van', 2: 'Zonneveld'} - - const retArr = {} - const argl = arguments.length - let k1 = '' - let i = 1 - let k = '' - let arr = {} - - arr1keys: for (k1 in arr1) { - for (i = 1; i < argl; i++) { - arr = arguments[i] - for (k in arr) { - if (arr[k] === arr1[k1] && k === k1) { - // If it reaches here, it was found in at least one array, so try next value - continue arr1keys - } - } - retArr[k1] = arr1[k1] - } - } - - return retArr -}
src/php/array/array_diff_assoc.ts+34 −0 added@@ -0,0 +1,34 @@ +import { entriesOfPhpAssoc, type PhpArrayLike, type PhpAssoc, toPhpArrayObject } from '../_helpers/_phpTypes.ts' + +export function array_diff_assoc<TValue>( + arr1: PhpArrayLike<TValue>, + ...arrays: Array<PhpArrayLike<TValue>> +): PhpAssoc<TValue> { + // discuss at: https://locutus.io/php/array_diff_assoc/ + // original by: Kevin van Zonneveld (https://kvz.io) + // bugfixed by: 0m3r + // revised by: Brett Zamir (https://brett-zamir.me) + // example 1: array_diff_assoc({0: 'Kevin', 1: 'van', 2: 'Zonneveld'}, {0: 'Kevin', 4: 'van', 5: 'Zonneveld'}) + // returns 1: {1: 'van', 2: 'Zonneveld'} + + const retArr: PhpAssoc<TValue> = {} + + if (arrays.length < 1) { + return retArr + } + + const arr1Object = toPhpArrayObject<TValue>(arr1) + arr1keys: for (const [k1, arr1Value] of entriesOfPhpAssoc(arr1Object)) { + for (const nextArray of arrays) { + const arr = toPhpArrayObject<TValue>(nextArray) + for (const [k, value] of entriesOfPhpAssoc(arr)) { + if (value === arr1Value && k === k1) { + continue arr1keys + } + } + } + retArr[k1] = arr1Value + } + + return retArr +}
src/php/array/array_diff.js+0 −30 removed@@ -1,30 +0,0 @@ -module.exports = function array_diff(arr1) { - // discuss at: https://locutus.io/php/array_diff/ - // original by: Kevin van Zonneveld (https://kvz.io) - // improved by: Sanjoy Roy - // revised by: Brett Zamir (https://brett-zamir.me) - // example 1: array_diff(['Kevin', 'van', 'Zonneveld'], ['van', 'Zonneveld']) - // returns 1: {0:'Kevin'} - - const retArr = {} - const argl = arguments.length - let k1 = '' - let i = 1 - let k = '' - let arr = {} - - arr1keys: for (k1 in arr1) { - for (i = 1; i < argl; i++) { - arr = arguments[i] - for (k in arr) { - if (arr[k] === arr1[k1]) { - // If it reaches here, it was found in at least one array, so try next value - continue arr1keys - } - } - retArr[k1] = arr1[k1] - } - } - - return retArr -}
src/php/array/array_diff_key.js+0 −32 removed@@ -1,32 +0,0 @@ -module.exports = function array_diff_key(arr1) { - // discuss at: https://locutus.io/php/array_diff_key/ - // original by: Ates Goral (https://magnetiq.com) - // revised by: Brett Zamir (https://brett-zamir.me) - // input by: Everlasto - // example 1: array_diff_key({red: 1, green: 2, blue: 3, white: 4}, {red: 5}) - // returns 1: {"green":2, "blue":3, "white":4} - // example 2: array_diff_key({red: 1, green: 2, blue: 3, white: 4}, {red: 5}, {red: 5}) - // returns 2: {"green":2, "blue":3, "white":4} - - const argl = arguments.length - const retArr = {} - let k1 = '' - let i = 1 - let k = '' - let arr = {} - - arr1keys: for (k1 in arr1) { - for (i = 1; i < argl; i++) { - arr = arguments[i] - for (k in arr) { - if (k === k1) { - // If it reaches here, it was found in at least one array, so try next value - continue arr1keys - } - } - retArr[k1] = arr1[k1] - } - } - - return retArr -}
src/php/array/array_diff_key.ts+36 −0 added@@ -0,0 +1,36 @@ +import { entriesOfPhpAssoc, type PhpArrayLike, type PhpAssoc, toPhpArrayObject } from '../_helpers/_phpTypes.ts' + +export function array_diff_key<TValue>( + arr1: PhpArrayLike<TValue>, + ...arrays: Array<PhpArrayLike<TValue>> +): PhpAssoc<TValue> { + // discuss at: https://locutus.io/php/array_diff_key/ + // original by: Ates Goral (https://magnetiq.com) + // revised by: Brett Zamir (https://brett-zamir.me) + // input by: Everlasto + // example 1: array_diff_key({red: 1, green: 2, blue: 3, white: 4}, {red: 5}) + // returns 1: {"green":2, "blue":3, "white":4} + // example 2: array_diff_key({red: 1, green: 2, blue: 3, white: 4}, {red: 5}, {red: 5}) + // returns 2: {"green":2, "blue":3, "white":4} + + const retArr: PhpAssoc<TValue> = {} + + if (arrays.length < 1) { + return retArr + } + + const arr1Object = toPhpArrayObject<TValue>(arr1) + arr1keys: for (const [k1, arr1Value] of entriesOfPhpAssoc(arr1Object)) { + for (const nextArray of arrays) { + const arr = toPhpArrayObject<TValue>(nextArray) + for (const [k] of entriesOfPhpAssoc(arr)) { + if (k === k1) { + continue arr1keys + } + } + } + retArr[k1] = arr1Value + } + + return retArr +}
src/php/array/array_diff.ts+34 −0 added@@ -0,0 +1,34 @@ +import { entriesOfPhpAssoc, type PhpArrayLike, type PhpAssoc, toPhpArrayObject } from '../_helpers/_phpTypes.ts' + +export function array_diff<TValue>( + arr1: PhpArrayLike<TValue>, + ...arrays: Array<PhpArrayLike<TValue>> +): PhpAssoc<TValue> { + // discuss at: https://locutus.io/php/array_diff/ + // original by: Kevin van Zonneveld (https://kvz.io) + // improved by: Sanjoy Roy + // revised by: Brett Zamir (https://brett-zamir.me) + // example 1: array_diff(['Kevin', 'van', 'Zonneveld'], ['van', 'Zonneveld']) + // returns 1: {0:'Kevin'} + + const retArr: PhpAssoc<TValue> = {} + + if (arrays.length < 1) { + return retArr + } + + const arr1Object = toPhpArrayObject<TValue>(arr1) + arr1keys: for (const [k1, arr1Value] of entriesOfPhpAssoc(arr1Object)) { + for (const nextArray of arrays) { + const arr = toPhpArrayObject<TValue>(nextArray) + for (const [, value] of entriesOfPhpAssoc(arr)) { + if (value === arr1Value) { + continue arr1keys + } + } + } + retArr[k1] = arr1Value + } + + return retArr +}
src/php/array/array_diff_uassoc.js+0 −35 removed@@ -1,35 +0,0 @@ -module.exports = function array_diff_uassoc(arr1) { - // discuss at: https://locutus.io/php/array_diff_uassoc/ - // original by: Brett Zamir (https://brett-zamir.me) - // example 1: var $array1 = {a: 'green', b: 'brown', c: 'blue', 0: 'red'} - // example 1: var $array2 = {a: 'GREEN', B: 'brown', 0: 'yellow', 1: 'red'} - // example 1: array_diff_uassoc($array1, $array2, function (key1, key2) { return (key1 === key2 ? 0 : (key1 > key2 ? 1 : -1)) }) - // returns 1: {a: 'green', b: 'brown', c: 'blue', 0: 'red'} - - const retArr = {} - const arglm1 = arguments.length - 1 - let cb = arguments[arglm1] - let arr = {} - let i = 1 - let k1 = '' - let k = '' - - const $global = typeof window !== 'undefined' ? window : global - - cb = typeof cb === 'string' ? $global[cb] : Array.isArray(cb) ? $global[cb[0]][cb[1]] : cb - - arr1keys: for (k1 in arr1) { - for (i = 1; i < arglm1; i++) { - arr = arguments[i] - for (k in arr) { - if (arr[k] === arr1[k1] && cb(k, k1) === 0) { - // If it reaches here, it was found in at least one array, so try next value - continue arr1keys - } - } - retArr[k1] = arr1[k1] - } - } - - return retArr -}
src/php/array/array_diff_uassoc.ts+44 −0 added@@ -0,0 +1,44 @@ +import { resolveNumericComparator } from '../_helpers/_callbackResolver.ts' +import { + entriesOfPhpAssoc, + isPhpCallableDescriptor, + type PhpAssoc, + type PhpKeyComparatorDescriptor, + toPhpArrayObject, +} from '../_helpers/_phpTypes.ts' + +type DiffArray<T> = PhpAssoc<T> | T[] + +export function array_diff_uassoc<T>( + arr1: PhpAssoc<T>, + ...arraysAndCallback: [arr2: DiffArray<T>, ...rest: Array<DiffArray<T>>, callback: PhpKeyComparatorDescriptor] +): PhpAssoc<T> { + // discuss at: https://locutus.io/php/array_diff_uassoc/ + // original by: Brett Zamir (https://brett-zamir.me) + // example 1: var $array1 = {a: 'green', b: 'brown', c: 'blue', 0: 'red'} + // example 1: var $array2 = {a: 'GREEN', B: 'brown', 0: 'yellow', 1: 'red'} + // example 1: array_diff_uassoc($array1, $array2, function (key1, key2) { return (key1 === key2 ? 0 : (key1 > key2 ? 1 : -1)) }) + // returns 1: {a: 'green', b: 'brown', c: 'blue', 0: 'red'} + + const retArr: PhpAssoc<T> = {} + const callback = arraysAndCallback[arraysAndCallback.length - 1] + if (typeof callback === 'undefined' || !isPhpCallableDescriptor<[string, string]>(callback)) { + throw new Error('array_diff_uassoc(): Invalid callback') + } + const arrays = arraysAndCallback.slice(0, -1).map((value) => toPhpArrayObject<T>(value)) + const keyComparator = resolveNumericComparator<string, string>(callback, 'array_diff_uassoc(): Invalid callback') + + arr1keys: for (const [k1, arr1Value] of entriesOfPhpAssoc(arr1)) { + for (const arr of arrays) { + for (const [k, arrValue] of entriesOfPhpAssoc(arr)) { + if (arrValue === arr1Value && keyComparator(k, k1) === 0) { + // If it reaches here, it was found in at least one array, so try next value + continue arr1keys + } + } + } + retArr[k1] = arr1Value + } + + return retArr +}
src/php/array/array_diff_ukey.js+0 −36 removed@@ -1,36 +0,0 @@ -module.exports = function array_diff_ukey(arr1) { - // discuss at: https://locutus.io/php/array_diff_ukey/ - // original by: Brett Zamir (https://brett-zamir.me) - // example 1: var $array1 = {blue: 1, red: 2, green: 3, purple: 4} - // example 1: var $array2 = {green: 5, blue: 6, yellow: 7, cyan: 8} - // example 1: array_diff_ukey($array1, $array2, function (key1, key2){ return (key1 === key2 ? 0 : (key1 > key2 ? 1 : -1)); }) - // returns 1: {red: 2, purple: 4} - - const retArr = {} - const arglm1 = arguments.length - 1 - // var arglm2 = arglm1 - 1 - let cb = arguments[arglm1] - let k1 = '' - let i = 1 - let arr = {} - let k = '' - - const $global = typeof window !== 'undefined' ? window : global - - cb = typeof cb === 'string' ? $global[cb] : Array.isArray(cb) ? $global[cb[0]][cb[1]] : cb - - arr1keys: for (k1 in arr1) { - for (i = 1; i < arglm1; i++) { - arr = arguments[i] - for (k in arr) { - if (cb(k, k1) === 0) { - // If it reaches here, it was found in at least one array, so try next value - continue arr1keys - } - } - retArr[k1] = arr1[k1] - } - } - - return retArr -}
src/php/array/array_diff_ukey.ts+44 −0 added@@ -0,0 +1,44 @@ +import { resolveNumericComparator } from '../_helpers/_callbackResolver.ts' +import { + entriesOfPhpAssoc, + isPhpCallableDescriptor, + type PhpAssoc, + type PhpKeyComparatorDescriptor, + toPhpArrayObject, +} from '../_helpers/_phpTypes.ts' + +type DiffArray<T> = PhpAssoc<T> | T[] + +export function array_diff_ukey<T>( + arr1: PhpAssoc<T>, + ...arraysAndCallback: [arr2: DiffArray<T>, ...rest: Array<DiffArray<T>>, callback: PhpKeyComparatorDescriptor] +): PhpAssoc<T> { + // discuss at: https://locutus.io/php/array_diff_ukey/ + // original by: Brett Zamir (https://brett-zamir.me) + // example 1: var $array1 = {blue: 1, red: 2, green: 3, purple: 4} + // example 1: var $array2 = {green: 5, blue: 6, yellow: 7, cyan: 8} + // example 1: array_diff_ukey($array1, $array2, function (key1, key2){ return (key1 === key2 ? 0 : (key1 > key2 ? 1 : -1)); }) + // returns 1: {red: 2, purple: 4} + + const retArr: PhpAssoc<T> = {} + const callback = arraysAndCallback[arraysAndCallback.length - 1] + if (typeof callback === 'undefined' || !isPhpCallableDescriptor<[string, string]>(callback)) { + throw new Error('array_diff_ukey(): Invalid callback') + } + const arrays = arraysAndCallback.slice(0, -1).map((value) => toPhpArrayObject<T>(value)) + const keyComparator = resolveNumericComparator<string, string>(callback, 'array_diff_ukey(): Invalid callback') + + arr1keys: for (const [k1, arr1Value] of entriesOfPhpAssoc(arr1)) { + for (const arr of arrays) { + for (const [k] of entriesOfPhpAssoc(arr)) { + if (keyComparator(k, k1) === 0) { + // If it reaches here, it was found in at least one array, so try next value + continue arr1keys + } + } + } + retArr[k1] = arr1Value + } + + return retArr +}
src/php/array/array_fill.js+0 −19 removed@@ -1,19 +0,0 @@ -module.exports = function array_fill(startIndex, num, mixedVal) { - // discuss at: https://locutus.io/php/array_fill/ - // parity verified: PHP 8.3 - // original by: Kevin van Zonneveld (https://kvz.io) - // improved by: Waldo Malqui Silva (https://waldo.malqui.info) - // example 1: array_fill(5, 6, 'banana') - // returns 1: { 5: 'banana', 6: 'banana', 7: 'banana', 8: 'banana', 9: 'banana', 10: 'banana' } - - let key - const tmpArr = {} - - if (!isNaN(startIndex) && !isNaN(num)) { - for (key = 0; key < num; key++) { - tmpArr[key + startIndex] = mixedVal - } - } - - return tmpArr -}
src/php/array/array_fill_keys.js+0 −17 removed@@ -1,17 +0,0 @@ -module.exports = function array_fill_keys(keys, value) { - // discuss at: https://locutus.io/php/array_fill_keys/ - // original by: Brett Zamir (https://brett-zamir.me) - // bugfixed by: Brett Zamir (https://brett-zamir.me) - // example 1: var $keys = {'a': 'foo', 2: 5, 3: 10, 4: 'bar'} - // example 1: array_fill_keys($keys, 'banana') - // returns 1: {"foo": "banana", 5: "banana", 10: "banana", "bar": "banana"} - - const retObj = {} - let key = '' - - for (key in keys) { - retObj[keys[key]] = value - } - - return retObj -}
src/php/array/array_fill_keys.ts+28 −0 added@@ -0,0 +1,28 @@ +import { + type PhpArrayLike, + type PhpAssoc, + type PhpKey, + type StringLike, + toPhpArrayObject, +} from '../_helpers/_phpTypes.ts' + +export function array_fill_keys<TKey extends PhpKey | StringLike, TValue>( + keys: PhpArrayLike<TKey>, + value: TValue, +): PhpAssoc<TValue> { + // discuss at: https://locutus.io/php/array_fill_keys/ + // original by: Brett Zamir (https://brett-zamir.me) + // bugfixed by: Brett Zamir (https://brett-zamir.me) + // example 1: var $keys = {'a': 'foo', 2: 5, 3: 10, 4: 'bar'} + // example 1: array_fill_keys($keys, 'banana') + // returns 1: {"foo": "banana", 5: "banana", 10: "banana", "bar": "banana"} + + const retObj: PhpAssoc<TValue> = {} + const keyedValues = toPhpArrayObject<TKey>(keys) + + for (const key in keyedValues) { + retObj[String(keyedValues[key])] = value + } + + return retObj +}
src/php/array/array_fill.ts+19 −0 added@@ -0,0 +1,19 @@ +import type { PhpAssoc } from '../_helpers/_phpTypes.ts' + +export function array_fill<TValue>(startIndex: number, num: number, mixedVal: TValue): PhpAssoc<TValue> { + // discuss at: https://locutus.io/php/array_fill/ + // parity verified: PHP 8.3 + // original by: Kevin van Zonneveld (https://kvz.io) + // improved by: Waldo Malqui Silva (https://waldo.malqui.info) + // example 1: array_fill(5, 6, 'banana') + // returns 1: { 5: 'banana', 6: 'banana', 7: 'banana', 8: 'banana', 9: 'banana', 10: 'banana' } + + const tmpArr: PhpAssoc<TValue> = {} + if (!isNaN(startIndex) && !isNaN(num)) { + for (let key = 0; key < num; key++) { + tmpArr[String(key + startIndex)] = mixedVal + } + } + + return tmpArr +}
src/php/array/array_filter.js+0 −37 removed@@ -1,37 +0,0 @@ -module.exports = function array_filter(arr, func) { - // discuss at: https://locutus.io/php/array_filter/ - // original by: Brett Zamir (https://brett-zamir.me) - // input by: max4ever - // improved by: Brett Zamir (https://brett-zamir.me) - // note 1: Takes a function as an argument, not a function's name - // example 1: var odd = function (num) {return (num & 1);} - // example 1: array_filter({"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, odd) - // returns 1: {"a": 1, "c": 3, "e": 5} - // example 2: var even = function (num) {return (!(num & 1));} - // example 2: array_filter([6, 7, 8, 9, 10, 11, 12], even) - // returns 2: [ 6, , 8, , 10, , 12 ] - // example 3: array_filter({"a": 1, "b": false, "c": -1, "d": 0, "e": null, "f":'', "g":undefined}) - // returns 3: {"a":1, "c":-1} - - let retObj = {} - let k - - func = - func || - function (v) { - return v - } - - // @todo: Issue #73 - if (Array.isArray(arr)) { - retObj = [] - } - - for (k in arr) { - if (func(arr[k])) { - retObj[k] = arr[k] - } - } - - return retObj -}
src/php/_helpers/_arrayPointers.ts+82 −0 added@@ -0,0 +1,82 @@ +import { ensurePhpRuntimeState } from './_phpRuntimeState.ts' +import { entriesOfPhpAssoc, type PhpArrayLike, type PhpInput, type PhpList } from './_phpTypes.ts' + +interface PointerState { + cursor: number + setCursor: (nextCursor: number) => void +} + +const findPointerIndex = (pointers: PhpList<PhpInput>, target: PhpInput): number => { + for (let index = 0; index < pointers.length; index += 1) { + if (pointers[index] === target) { + return index + } + } + + return -1 +} + +export function getPointerState<T>(target: PhpArrayLike<T>, initialize = true): PointerState | null { + const runtime = ensurePhpRuntimeState() + const pointers = runtime.pointers + const pointerTarget: PhpInput = target + + let index = findPointerIndex(pointers, pointerTarget) + if (index === -1) { + if (!initialize) { + return null + } + pointers.push(pointerTarget, 0) + index = pointers.length - 2 + } + + const cursorValue = pointers[index + 1] + const cursor = typeof cursorValue === 'number' ? cursorValue : 0 + + return { + cursor, + setCursor: (nextCursor: number) => { + pointers[index + 1] = nextCursor + }, + } +} + +export function getArrayLikeLength<T>(target: PhpArrayLike<T>): number { + if (Array.isArray(target)) { + return target.length + } + + let count = 0 + for (const _key in target) { + count += 1 + } + return count +} + +export function getEntryAtCursor<T>(target: PhpArrayLike<T>, cursor: number): [string | number, T] | null { + if (cursor < 0) { + return null + } + + if (Array.isArray(target)) { + if (cursor >= target.length) { + return null + } + + const value = target[cursor] + if (typeof value === 'undefined') { + return null + } + return [cursor, value] + } + + let index = 0 + for (const [key, value] of entriesOfPhpAssoc(target)) { + if (index === cursor) { + return [key, value] + } + index += 1 + } + + return null +}
src/php/_helpers/_bc.js+0 −1322 removed@@ -1,1322 +0,0 @@ -// SPDX-License-Identifier: LGPL-2.1-or-later -// NOTE: This file is LGPL licensed, not MIT like the rest of locutus. -// See LICENSE file for details. - -module.exports = function _bc() { - // discuss at: https://locutus.io/php/_helpers/_bc - // original by: lmeyrick (https://sourceforge.net/projects/bcmath-js/) - // improved by: Brett Zamir (https://brett-zamir.me) - // example 1: var $bc = _bc() - // example 1: var $result = $bc.PLUS - // returns 1: '+' - - /** - * BC Math Library for Javascript (LGPL-2.1) - * Ported from the PHP5 bcmath extension source code, - * which uses the Libbcmath package... - * Copyright (C) 1991, 1992, 1993, 1994, 1997 Free Software Foundation, Inc. - * Copyright (C) 2000 Philip A. Nelson - * The Free Software Foundation, Inc. - * 59 Temple Place, Suite 330 - * Boston, MA 02111-1307 USA. - * e-mail: philnelson@acm.org - * us-mail: Philip A. Nelson - * Computer Science Department, 9062 - * Western Washington University - * Bellingham, WA 98226-9062 - * - * bcmath-js homepage: https://sourceforge.net/projects/bcmath-js/ - * - * This code is covered under the LGPL-2.1 license. - */ - - /** - * Binary Calculator (BC) Arbitrary Precision Mathematics Lib v0.10 (LGPL) - * Copy of Libbcmath included in PHP5 src - * - * Note: this is just the shared library file and does not include the php-style functions. - * use bcmath{-min}.js for functions like bcadd, bcsub etc. - * - * Feel free to use how-ever you want, just email any bug-fixes/improvements - * to the sourceforge project: - * - * - * Ported from the PHP5 bcmath extension source code, - * which uses the Libbcmath package... - * Copyright (C) 1991, 1992, 1993, 1994, 1997 Free Software Foundation, Inc. - * Copyright (C) 2000 Philip A. Nelson - * The Free Software Foundation, Inc. - * 59 Temple Place, Suite 330 - * Boston, MA 02111-1307 USA. - * e-mail: philnelson@acm.org - * us-mail: Philip A. Nelson - * Computer Science Department, 9062 - * Western Washington University - * Bellingham, WA 98226-9062 - */ - - const Libbcmath = { - PLUS: '+', - MINUS: '-', - BASE: 10, - // must be 10 (for now) - scale: 0, - // default scale - /** - * Basic number structure - */ - bc_num: function () { - this.n_sign = null // sign - this.n_len = null // (int) The number of digits before the decimal point. - this.n_scale = null // (int) The number of digits after the decimal point. - // this.n_refs = null; // (int) The number of pointers to this number. - // this.n_text = null; // ?? Linked list for available list. - this.n_value = null // array as value, where 1.23 = [1,2,3] - this.toString = function () { - let r - let tmp - tmp = this.n_value.join('') - - // add minus sign (if applicable) then add the integer part - r = (this.n_sign === Libbcmath.PLUS ? '' : this.n_sign) + tmp.substr(0, this.n_len) - - // if decimal places, add a . and the decimal part - if (this.n_scale > 0) { - r += '.' + tmp.substr(this.n_len, this.n_scale) - } - return r - } - }, - - /** - * Base add function - * - // Here is the full add routine that takes care of negative numbers. - // N1 is added to N2 and the result placed into RESULT. SCALE_MIN - // is the minimum scale for the result. - * - * @param {bc_num} n1 - * @param {bc_num} n2 - * @param {int} scaleMin - * @return bc_num - */ - bc_add: function (n1, n2, scaleMin) { - let sum - let cmpRes - let resScale - - if (n1.n_sign === n2.n_sign) { - sum = Libbcmath._bc_do_add(n1, n2, scaleMin) - sum.n_sign = n1.n_sign - } else { - // subtraction must be done. - cmpRes = Libbcmath._bc_do_compare(n1, n2, false, false) // Compare magnitudes. - switch (cmpRes) { - case -1: - // n1 is less than n2, subtract n1 from n2. - sum = Libbcmath._bc_do_sub(n2, n1, scaleMin) - sum.n_sign = n2.n_sign - break - - case 0: - // They are equal! return zero with the correct scale! - resScale = Libbcmath.MAX(scaleMin, Libbcmath.MAX(n1.n_scale, n2.n_scale)) - sum = Libbcmath.bc_new_num(1, resScale) - Libbcmath.memset(sum.n_value, 0, 0, resScale + 1) - break - - case 1: - // n2 is less than n1, subtract n2 from n1. - sum = Libbcmath._bc_do_sub(n1, n2, scaleMin) - sum.n_sign = n1.n_sign - } - } - return sum - }, - - /** - * This is the "user callable" routine to compare numbers N1 and N2. - * @param {bc_num} n1 - * @param {bc_num} n2 - * @return int -1, 0, 1 (n1 < n2, ===, n1 > n2) - */ - bc_compare: function (n1, n2) { - return Libbcmath._bc_do_compare(n1, n2, true, false) - }, - - _one_mult: function (num, nPtr, size, digit, result, rPtr) { - let carry - let value // int - let nptr - let rptr // int pointers - if (digit === 0) { - Libbcmath.memset(result, 0, 0, size) // memset (result, 0, size); - } else { - if (digit === 1) { - Libbcmath.memcpy(result, rPtr, num, nPtr, size) // memcpy (result, num, size); - } else { - // Initialize - nptr = nPtr + size - 1 // nptr = (unsigned char *) (num+size-1); - rptr = rPtr + size - 1 // rptr = (unsigned char *) (result+size-1); - carry = 0 - - while (size-- > 0) { - value = num[nptr--] * digit + carry // value = *nptr-- * digit + carry; - result[rptr--] = value % Libbcmath.BASE // @CHECK cint //*rptr-- = value % BASE; - carry = Math.floor(value / Libbcmath.BASE) // @CHECK cint //carry = value / BASE; - } - - if (carry !== 0) { - result[rptr] = carry - } - } - } - }, - - bc_divide: function (n1, n2, scale) { - // var quot // bc_num return - let qval // bc_num - let num1 - let num2 // string - let ptr1 - let ptr2 - let n2ptr - let qptr // int pointers - let scale1 - let val // int - let len1 - let len2 - let scale2 - let qdigits - let extra - let count // int - let qdig - let qguess - let borrow - let carry // int - let mval // string - let zero // char - let norm // int - // var ptrs // return object from one_mul - // Test for divide by zero. (return failure) - if (Libbcmath.bc_is_zero(n2)) { - return -1 - } - - // Test for zero divide by anything (return zero) - if (Libbcmath.bc_is_zero(n1)) { - return Libbcmath.bc_new_num(1, scale) - } - - /* Test for n1 equals n2 (return 1 as n1 nor n2 are zero) - if (Libbcmath.bc_compare(n1, n2, Libbcmath.MAX(n1.n_scale, n2.n_scale)) === 0) { - quot=Libbcmath.bc_new_num(1, scale); - quot.n_value[0] = 1; - return quot; - } - */ - - // Test for divide by 1. If it is we must truncate. - // @todo: check where scale > 0 too.. can't see why not - // (ie bc_is_zero - add bc_is_one function) - if (n2.n_scale === 0) { - if (n2.n_len === 1 && n2.n_value[0] === 1) { - qval = Libbcmath.bc_new_num(n1.n_len, scale) // qval = bc_new_num (n1->n_len, scale); - qval.n_sign = n1.n_sign === n2.n_sign ? Libbcmath.PLUS : Libbcmath.MINUS - // memset (&qval->n_value[n1->n_len],0,scale): - Libbcmath.memset(qval.n_value, n1.n_len, 0, scale) - // memcpy (qval->n_value, n1->n_value, n1->n_len + MIN(n1->n_scale,scale)): - Libbcmath.memcpy(qval.n_value, 0, n1.n_value, 0, n1.n_len + Libbcmath.MIN(n1.n_scale, scale)) - // can we return here? not in c src, but can't see why-not. - // return qval; - } - } - - /* Set up the divide. Move the decimal point on n1 by n2's scale. - Remember, zeros on the end of num2 are wasted effort for dividing. */ - scale2 = n2.n_scale // scale2 = n2->n_scale; - n2ptr = n2.n_len + scale2 - 1 // n2ptr = (unsigned char *) n2.n_value+n2.n_len+scale2-1; - while (scale2 > 0 && n2.n_value[n2ptr--] === 0) { - scale2-- - } - - len1 = n1.n_len + scale2 - scale1 = n1.n_scale - scale2 - if (scale1 < scale) { - extra = scale - scale1 - } else { - extra = 0 - } - - // num1 = (unsigned char *) safe_emalloc (1, n1.n_len+n1.n_scale, extra+2): - num1 = Libbcmath.safe_emalloc(1, n1.n_len + n1.n_scale, extra + 2) - if (num1 === null) { - Libbcmath.bc_out_of_memory() - } - // memset (num1, 0, n1->n_len+n1->n_scale+extra+2): - Libbcmath.memset(num1, 0, 0, n1.n_len + n1.n_scale + extra + 2) - // memcpy (num1+1, n1.n_value, n1.n_len+n1.n_scale): - Libbcmath.memcpy(num1, 1, n1.n_value, 0, n1.n_len + n1.n_scale) - // len2 = n2->n_len + scale2: - len2 = n2.n_len + scale2 - // num2 = (unsigned char *) safe_emalloc (1, len2, 1): - num2 = Libbcmath.safe_emalloc(1, len2, 1) - if (num2 === null) { - Libbcmath.bc_out_of_memory() - } - // memcpy (num2, n2.n_value, len2): - Libbcmath.memcpy(num2, 0, n2.n_value, 0, len2) - // *(num2+len2) = 0: - num2[len2] = 0 - // n2ptr = num2: - n2ptr = 0 - // while (*n2ptr === 0): - while (num2[n2ptr] === 0) { - n2ptr++ - len2-- - } - - // Calculate the number of quotient digits. - if (len2 > len1 + scale) { - qdigits = scale + 1 - zero = true - } else { - zero = false - if (len2 > len1) { - qdigits = scale + 1 // One for the zero integer part. - } else { - qdigits = len1 - len2 + scale + 1 - } - } - - // Allocate and zero the storage for the quotient. - // qval = bc_new_num (qdigits-scale,scale); - qval = Libbcmath.bc_new_num(qdigits - scale, scale) - // memset (qval->n_value, 0, qdigits); - Libbcmath.memset(qval.n_value, 0, 0, qdigits) - // Allocate storage for the temporary storage mval. - // mval = (unsigned char *) safe_emalloc (1, len2, 1); - mval = Libbcmath.safe_emalloc(1, len2, 1) - if (mval === null) { - Libbcmath.bc_out_of_memory() - } - - // Now for the full divide algorithm. - if (!zero) { - // Normalize - // norm = Libbcmath.cint(10 / (Libbcmath.cint(n2.n_value[n2ptr]) + 1)); - // norm = 10 / ((int)*n2ptr + 1) - norm = Math.floor(10 / (n2.n_value[n2ptr] + 1)) // norm = 10 / ((int)*n2ptr + 1); - if (norm !== 1) { - // Libbcmath._one_mult(num1, len1+scale1+extra+1, norm, num1); - Libbcmath._one_mult(num1, 0, len1 + scale1 + extra + 1, norm, num1, 0) - // Libbcmath._one_mult(n2ptr, len2, norm, n2ptr); - Libbcmath._one_mult(n2.n_value, n2ptr, len2, norm, n2.n_value, n2ptr) - // @todo: Check: Is the pointer affected by the call? if so, - // maybe need to adjust points on return? - } - - // Initialize divide loop. - qdig = 0 - if (len2 > len1) { - qptr = len2 - len1 // qptr = (unsigned char *) qval.n_value+len2-len1; - } else { - qptr = 0 // qptr = (unsigned char *) qval.n_value; - } - - // Loop - while (qdig <= len1 + scale - len2) { - // Calculate the quotient digit guess. - if (n2.n_value[n2ptr] === num1[qdig]) { - qguess = 9 - } else { - qguess = Math.floor((num1[qdig] * 10 + num1[qdig + 1]) / n2.n_value[n2ptr]) - } - // Test qguess. - - if ( - n2.n_value[n2ptr + 1] * qguess > - (num1[qdig] * 10 + num1[qdig + 1] - n2.n_value[n2ptr] * qguess) * 10 + num1[qdig + 2] - ) { - qguess-- - // And again. - if ( - n2.n_value[n2ptr + 1] * qguess > - (num1[qdig] * 10 + num1[qdig + 1] - n2.n_value[n2ptr] * qguess) * 10 + num1[qdig + 2] - ) { - qguess-- - } - } - - // Multiply and subtract. - borrow = 0 - if (qguess !== 0) { - mval[0] = 0 //* mval = 0; // @CHECK is this to fix ptr2 < 0? - // _one_mult (n2ptr, len2, qguess, mval+1); // @CHECK - Libbcmath._one_mult(n2.n_value, n2ptr, len2, qguess, mval, 1) - ptr1 = qdig + len2 // (unsigned char *) num1+qdig+len2; - ptr2 = len2 // (unsigned char *) mval+len2; - // @todo: CHECK: Does a negative pointer return null? - // ptr2 can be < 0 here as ptr1 = len2, thus count < len2+1 will always fail ? - for (count = 0; count < len2 + 1; count++) { - if (ptr2 < 0) { - // val = Libbcmath.cint(num1[ptr1]) - 0 - borrow; - // val = (int) *ptr1 - (int) *ptr2-- - borrow; - val = num1[ptr1] - 0 - borrow // val = (int) *ptr1 - (int) *ptr2-- - borrow; - } else { - // val = Libbcmath.cint(num1[ptr1]) - Libbcmath.cint(mval[ptr2--]) - borrow; - // val = (int) *ptr1 - (int) *ptr2-- - borrow; - // val = (int) *ptr1 - (int) *ptr2-- - borrow; - val = num1[ptr1] - mval[ptr2--] - borrow - } - if (val < 0) { - val += 10 - borrow = 1 - } else { - borrow = 0 - } - num1[ptr1--] = val - } - } - - // Test for negative result. - if (borrow === 1) { - qguess-- - ptr1 = qdig + len2 // (unsigned char *) num1+qdig+len2; - ptr2 = len2 - 1 // (unsigned char *) n2ptr+len2-1; - carry = 0 - for (count = 0; count < len2; count++) { - if (ptr2 < 0) { - // val = Libbcmath.cint(num1[ptr1]) + 0 + carry; - // val = (int) *ptr1 + (int) *ptr2-- + carry; - // val = (int) *ptr1 + (int) *ptr2-- + carry; - val = num1[ptr1] + 0 + carry - } else { - // val = Libbcmath.cint(num1[ptr1]) + Libbcmath.cint(n2.n_value[ptr2--]) + carry; - // val = (int) *ptr1 + (int) *ptr2-- + carry; - // val = (int) *ptr1 + (int) *ptr2-- + carry; - val = num1[ptr1] + n2.n_value[ptr2--] + carry - } - if (val > 9) { - val -= 10 - carry = 1 - } else { - carry = 0 - } - num1[ptr1--] = val //* ptr1-- = val; - } - if (carry === 1) { - // num1[ptr1] = Libbcmath.cint((num1[ptr1] + 1) % 10); - // *ptr1 = (*ptr1 + 1) % 10; // @CHECK - // *ptr1 = (*ptr1 + 1) % 10; // @CHECK - num1[ptr1] = (num1[ptr1] + 1) % 10 - } - } - - // We now know the quotient digit. - qval.n_value[qptr++] = qguess //* qptr++ = qguess; - qdig++ - } - } - - // Clean up and return the number. - qval.n_sign = n1.n_sign === n2.n_sign ? Libbcmath.PLUS : Libbcmath.MINUS - if (Libbcmath.bc_is_zero(qval)) { - qval.n_sign = Libbcmath.PLUS - } - Libbcmath._bc_rm_leading_zeros(qval) - - return qval - - // return 0; // Everything is OK. - }, - - MUL_BASE_DIGITS: 80, - MUL_SMALL_DIGITS: 80 / 4, - // #define MUL_SMALL_DIGITS mul_base_digits/4 - - /* The multiply routine. N2 times N1 is put int PROD with the scale of - the result being MIN(N2 scale+N1 scale, MAX (SCALE, N2 scale, N1 scale)). - */ - /** - * @param n1 bc_num - * @param n2 bc_num - * @param scale [int] optional - */ - bc_multiply: function (n1, n2, scale) { - let pval // bc_num - let len1 - let len2 // int - let fullScale - let prodScale // int - // Initialize things. - len1 = n1.n_len + n1.n_scale - len2 = n2.n_len + n2.n_scale - fullScale = n1.n_scale + n2.n_scale - prodScale = Libbcmath.MIN(fullScale, Libbcmath.MAX(scale, Libbcmath.MAX(n1.n_scale, n2.n_scale))) - - // pval = Libbcmath.bc_init_num(); // allow pass by ref - // Do the multiply - pval = Libbcmath._bc_rec_mul(n1, len1, n2, len2, fullScale) - - // Assign to prod and clean up the number. - pval.n_sign = n1.n_sign === n2.n_sign ? Libbcmath.PLUS : Libbcmath.MINUS - // pval.n_value = pval.nPtr; - pval.n_len = len2 + len1 + 1 - fullScale - pval.n_scale = prodScale - Libbcmath._bc_rm_leading_zeros(pval) - if (Libbcmath.bc_is_zero(pval)) { - pval.n_sign = Libbcmath.PLUS - } - // bc_free_num (prod); - return pval - }, - - new_sub_num: function (length, scale, value, ptr = 0) { - const temp = new Libbcmath.bc_num() - temp.n_sign = Libbcmath.PLUS - temp.n_len = length - temp.n_scale = scale - temp.n_value = Libbcmath.safe_emalloc(1, length + scale, 0) - Libbcmath.memcpy(temp.n_value, 0, value, ptr, length + scale) - return temp - }, - - _bc_simp_mul: function (n1, n1len, n2, n2len, fullScale) { - let prod // bc_num - let n1ptr - let n2ptr - let pvptr // char *n1ptr, *n2ptr, *pvptr; - let n1end - let n2end // char *n1end, *n2end; // To the end of n1 and n2. - let indx - let sum - let prodlen // int indx, sum, prodlen; - prodlen = n1len + n2len + 1 - - prod = Libbcmath.bc_new_num(prodlen, 0) - - n1end = n1len - 1 // (char *) (n1->n_value + n1len - 1); - n2end = n2len - 1 // (char *) (n2->n_value + n2len - 1); - pvptr = prodlen - 1 // (char *) ((*prod)->n_value + prodlen - 1); - sum = 0 - - // Here is the loop... - for (indx = 0; indx < prodlen - 1; indx++) { - // (char *) (n1end - MAX(0, indx-n2len+1)); - n1ptr = n1end - Libbcmath.MAX(0, indx - n2len + 1) - // (char *) (n2end - MIN(indx, n2len-1)); - n2ptr = n2end - Libbcmath.MIN(indx, n2len - 1) - while (n1ptr >= 0 && n2ptr <= n2end) { - // sum += *n1ptr-- * *n2ptr++; - sum += n1.n_value[n1ptr--] * n2.n_value[n2ptr++] - } - //* pvptr-- = sum % BASE; - prod.n_value[pvptr--] = Math.floor(sum % Libbcmath.BASE) - sum = Math.floor(sum / Libbcmath.BASE) // sum = sum / BASE; - } - prod.n_value[pvptr] = sum //* pvptr = sum; - return prod - }, - - /* A special adder/subtractor for the recursive divide and conquer - multiply algorithm. Note: if sub is called, accum must - be larger that what is being subtracted. Also, accum and val - must have n_scale = 0. (e.g. they must look like integers. *) */ - _bc_shift_addsub: function (accum, val, shift, sub) { - let accp - let valp // signed char *accp, *valp; - let count - let carry // int count, carry; - count = val.n_len - if (val.n_value[0] === 0) { - count-- - } - - // assert (accum->n_len+accum->n_scale >= shift+count); - if (accum.n_len + accum.n_scale < shift + count) { - throw new Error('len + scale < shift + count') // ?? I think that's what assert does :) - } - - // Set up pointers and others - // (signed char *)(accum->n_value + accum->n_len + accum->n_scale - shift - 1); - accp = accum.n_len + accum.n_scale - shift - 1 - valp = val.n_len - 1 // (signed char *)(val->n_value + val->n_len - 1); - carry = 0 - if (sub) { - // Subtraction, carry is really borrow. - while (count--) { - accum.n_value[accp] -= val.n_value[valp--] + carry //* accp -= *valp-- + carry; - if (accum.n_value[accp] < 0) { - // if (*accp < 0) - carry = 1 - accum.n_value[accp--] += Libbcmath.BASE //* accp-- += BASE; - } else { - carry = 0 - accp-- - } - } - while (carry) { - accum.n_value[accp] -= carry //* accp -= carry; - if (accum.n_value[accp] < 0) { - // if (*accp < 0) - accum.n_value[accp--] += Libbcmath.BASE // *accp-- += BASE; - } else { - carry = 0 - } - } - } else { - // Addition - while (count--) { - accum.n_value[accp] += val.n_value[valp--] + carry //* accp += *valp-- + carry; - if (accum.n_value[accp] > Libbcmath.BASE - 1) { - // if (*accp > (BASE-1)) - carry = 1 - accum.n_value[accp--] -= Libbcmath.BASE //* accp-- -= BASE; - } else { - carry = 0 - accp-- - } - } - while (carry) { - accum.n_value[accp] += carry //* accp += carry; - if (accum.n_value[accp] > Libbcmath.BASE - 1) { - // if (*accp > (BASE-1)) - accum.n_value[accp--] -= Libbcmath.BASE //* accp-- -= BASE; - } else { - carry = 0 - } - } - } - return true // accum is the pass-by-reference return - }, - - /* Recursive divide and conquer multiply algorithm. - based on - Let u = u0 + u1*(b^n) - Let v = v0 + v1*(b^n) - Then uv = (B^2n+B^n)*u1*v1 + B^n*(u1-u0)*(v0-v1) + (B^n+1)*u0*v0 - - B is the base of storage, number of digits in u1,u0 close to equal. - */ - _bc_rec_mul: function (u, ulen, v, vlen, fullScale) { - let prod // @return - let u0 - let u1 - let v0 - let v1 // bc_num - // var u0len, - // var v0len // int - let m1 - let m2 - let m3 - let d1 - let d2 // bc_num - let n - let prodlen - let m1zero // int - let d1len - let d2len // int - // Base case? - if ( - ulen + vlen < Libbcmath.MUL_BASE_DIGITS || - ulen < Libbcmath.MUL_SMALL_DIGITS || - vlen < Libbcmath.MUL_SMALL_DIGITS - ) { - return Libbcmath._bc_simp_mul(u, ulen, v, vlen, fullScale) - } - - // Calculate n -- the u and v split point in digits. - n = Math.floor((Libbcmath.MAX(ulen, vlen) + 1) / 2) - - // Split u and v. - if (ulen < n) { - u1 = Libbcmath.bc_init_num() // u1 = bc_copy_num (BCG(_zero_)); - u0 = Libbcmath.new_sub_num(ulen, 0, u.n_value) - } else { - u1 = Libbcmath.new_sub_num(ulen - n, 0, u.n_value) - u0 = Libbcmath.new_sub_num(n, 0, u.n_value, ulen - n) - } - if (vlen < n) { - v1 = Libbcmath.bc_init_num() // bc_copy_num (BCG(_zero_)); - v0 = Libbcmath.new_sub_num(vlen, 0, v.n_value) - } else { - v1 = Libbcmath.new_sub_num(vlen - n, 0, v.n_value) - v0 = Libbcmath.new_sub_num(n, 0, v.n_value, vlen - n) - } - Libbcmath._bc_rm_leading_zeros(u1) - Libbcmath._bc_rm_leading_zeros(u0) - // var u0len = u0.n_len - Libbcmath._bc_rm_leading_zeros(v1) - Libbcmath._bc_rm_leading_zeros(v0) - // var v0len = v0.n_len - - m1zero = Libbcmath.bc_is_zero(u1) || Libbcmath.bc_is_zero(v1) - - // Calculate sub results ... - d1 = Libbcmath.bc_init_num() // needed? - d2 = Libbcmath.bc_init_num() // needed? - d1 = Libbcmath.bc_sub(u1, u0, 0) - d1len = d1.n_len - - d2 = Libbcmath.bc_sub(v0, v1, 0) - d2len = d2.n_len - - // Do recursive multiplies and shifted adds. - if (m1zero) { - m1 = Libbcmath.bc_init_num() // bc_copy_num (BCG(_zero_)); - } else { - // m1 = Libbcmath.bc_init_num(); //allow pass-by-ref - m1 = Libbcmath._bc_rec_mul(u1, u1.n_len, v1, v1.n_len, 0) - } - if (Libbcmath.bc_is_zero(d1) || Libbcmath.bc_is_zero(d2)) { - m2 = Libbcmath.bc_init_num() // bc_copy_num (BCG(_zero_)); - } else { - // m2 = Libbcmath.bc_init_num(); //allow pass-by-ref - m2 = Libbcmath._bc_rec_mul(d1, d1len, d2, d2len, 0) - } - - if (Libbcmath.bc_is_zero(u0) || Libbcmath.bc_is_zero(v0)) { - m3 = Libbcmath.bc_init_num() // bc_copy_num (BCG(_zero_)); - } else { - // m3 = Libbcmath.bc_init_num(); //allow pass-by-ref - m3 = Libbcmath._bc_rec_mul(u0, u0.n_len, v0, v0.n_len, 0) - } - - // Initialize product - prodlen = ulen + vlen + 1 - prod = Libbcmath.bc_new_num(prodlen, 0) - - if (!m1zero) { - Libbcmath._bc_shift_addsub(prod, m1, 2 * n, 0) - Libbcmath._bc_shift_addsub(prod, m1, n, 0) - } - Libbcmath._bc_shift_addsub(prod, m3, n, 0) - Libbcmath._bc_shift_addsub(prod, m3, 0, 0) - Libbcmath._bc_shift_addsub(prod, m2, n, d1.n_sign !== d2.n_sign) - - return prod - // Now clean up! - // bc_free_num (&u1); - // bc_free_num (&u0); - // bc_free_num (&v1); - // bc_free_num (&m1); - // bc_free_num (&v0); - // bc_free_num (&m2); - // bc_free_num (&m3); - // bc_free_num (&d1); - // bc_free_num (&d2); - }, - - /** - * - * @param {bc_num} n1 - * @param {bc_num} n2 - * @param {boolean} useSign - * @param {boolean} ignoreLast - * @return -1, 0, 1 (see bc_compare) - */ - _bc_do_compare: function (n1, n2, useSign, ignoreLast) { - let n1ptr - let n2ptr // int - let count // int - // First, compare signs. - if (useSign && n1.n_sign !== n2.n_sign) { - if (n1.n_sign === Libbcmath.PLUS) { - return 1 // Positive N1 > Negative N2 - } else { - return -1 // Negative N1 < Positive N1 - } - } - - // Now compare the magnitude. - if (n1.n_len !== n2.n_len) { - if (n1.n_len > n2.n_len) { - // Magnitude of n1 > n2. - if (!useSign || n1.n_sign === Libbcmath.PLUS) { - return 1 - } else { - return -1 - } - } else { - // Magnitude of n1 < n2. - if (!useSign || n1.n_sign === Libbcmath.PLUS) { - return -1 - } else { - return 1 - } - } - } - - /* If we get here, they have the same number of integer digits. - check the integer part and the equal length part of the fraction. */ - count = n1.n_len + Math.min(n1.n_scale, n2.n_scale) - n1ptr = 0 - n2ptr = 0 - - while (count > 0 && n1.n_value[n1ptr] === n2.n_value[n2ptr]) { - n1ptr++ - n2ptr++ - count-- - } - - if (ignoreLast && count === 1 && n1.n_scale === n2.n_scale) { - return 0 - } - - if (count !== 0) { - if (n1.n_value[n1ptr] > n2.n_value[n2ptr]) { - // Magnitude of n1 > n2. - if (!useSign || n1.n_sign === Libbcmath.PLUS) { - return 1 - } else { - return -1 - } - } else { - // Magnitude of n1 < n2. - if (!useSign || n1.n_sign === Libbcmath.PLUS) { - return -1 - } else { - return 1 - } - } - } - - // They are equal up to the last part of the equal part of the fraction. - if (n1.n_scale !== n2.n_scale) { - if (n1.n_scale > n2.n_scale) { - for (count = n1.n_scale - n2.n_scale; count > 0; count--) { - if (n1.n_value[n1ptr++] !== 0) { - // Magnitude of n1 > n2. - if (!useSign || n1.n_sign === Libbcmath.PLUS) { - return 1 - } else { - return -1 - } - } - } - } else { - for (count = n2.n_scale - n1.n_scale; count > 0; count--) { - if (n2.n_value[n2ptr++] !== 0) { - // Magnitude of n1 < n2. - if (!useSign || n1.n_sign === Libbcmath.PLUS) { - return -1 - } else { - return 1 - } - } - } - } - } - - // They must be equal! - return 0 - }, - - /* Here is the full subtract routine that takes care of negative numbers. - N2 is subtracted from N1 and the result placed in RESULT. SCALE_MIN - is the minimum scale for the result. */ - bc_sub: function (n1, n2, scaleMin) { - let diff // bc_num - let cmpRes - let resScale // int - if (n1.n_sign !== n2.n_sign) { - diff = Libbcmath._bc_do_add(n1, n2, scaleMin) - diff.n_sign = n1.n_sign - } else { - // subtraction must be done. - // Compare magnitudes. - cmpRes = Libbcmath._bc_do_compare(n1, n2, false, false) - switch (cmpRes) { - case -1: - // n1 is less than n2, subtract n1 from n2. - diff = Libbcmath._bc_do_sub(n2, n1, scaleMin) - diff.n_sign = n2.n_sign === Libbcmath.PLUS ? Libbcmath.MINUS : Libbcmath.PLUS - break - case 0: - // They are equal! return zero! - resScale = Libbcmath.MAX(scaleMin, Libbcmath.MAX(n1.n_scale, n2.n_scale)) - diff = Libbcmath.bc_new_num(1, resScale) - Libbcmath.memset(diff.n_value, 0, 0, resScale + 1) - break - case 1: - // n2 is less than n1, subtract n2 from n1. - diff = Libbcmath._bc_do_sub(n1, n2, scaleMin) - diff.n_sign = n1.n_sign - break - } - } - - // Clean up and return. - // bc_free_num (result); - //* result = diff; - return diff - }, - - _bc_do_add: function (n1, n2, scaleMin) { - let sum // bc_num - let sumScale - let sumDigits // int - let n1ptr - let n2ptr - let sumptr // int - let carry - let n1bytes - let n2bytes // int - let tmp // int - - // Prepare sum. - sumScale = Libbcmath.MAX(n1.n_scale, n2.n_scale) - sumDigits = Libbcmath.MAX(n1.n_len, n2.n_len) + 1 - sum = Libbcmath.bc_new_num(sumDigits, Libbcmath.MAX(sumScale, scaleMin)) - - // Start with the fraction part. Initialize the pointers. - n1bytes = n1.n_scale - n2bytes = n2.n_scale - n1ptr = n1.n_len + n1bytes - 1 - n2ptr = n2.n_len + n2bytes - 1 - sumptr = sumScale + sumDigits - 1 - - // Add the fraction part. First copy the longer fraction - // (ie when adding 1.2345 to 1 we know .2345 is correct already) . - if (n1bytes !== n2bytes) { - if (n1bytes > n2bytes) { - // n1 has more dp then n2 - while (n1bytes > n2bytes) { - sum.n_value[sumptr--] = n1.n_value[n1ptr--] - // *sumptr-- = *n1ptr--; - n1bytes-- - } - } else { - // n2 has more dp then n1 - while (n2bytes > n1bytes) { - sum.n_value[sumptr--] = n2.n_value[n2ptr--] - // *sumptr-- = *n2ptr--; - n2bytes-- - } - } - } - - // Now add the remaining fraction part and equal size integer parts. - n1bytes += n1.n_len - n2bytes += n2.n_len - carry = 0 - while (n1bytes > 0 && n2bytes > 0) { - // add the two numbers together - tmp = n1.n_value[n1ptr--] + n2.n_value[n2ptr--] + carry - // *sumptr = *n1ptr-- + *n2ptr-- + carry; - // check if they are >= 10 (impossible to be more then 18) - if (tmp >= Libbcmath.BASE) { - carry = 1 - tmp -= Libbcmath.BASE // yep, subtract 10, add a carry - } else { - carry = 0 - } - sum.n_value[sumptr] = tmp - sumptr-- - n1bytes-- - n2bytes-- - } - - // Now add carry the [rest of the] longer integer part. - if (n1bytes === 0) { - // n2 is a bigger number then n1 - while (n2bytes-- > 0) { - tmp = n2.n_value[n2ptr--] + carry - // *sumptr = *n2ptr-- + carry; - if (tmp >= Libbcmath.BASE) { - carry = 1 - tmp -= Libbcmath.BASE - } else { - carry = 0 - } - sum.n_value[sumptr--] = tmp - } - } else { - // n1 is bigger then n2.. - while (n1bytes-- > 0) { - tmp = n1.n_value[n1ptr--] + carry - // *sumptr = *n1ptr-- + carry; - if (tmp >= Libbcmath.BASE) { - carry = 1 - tmp -= Libbcmath.BASE - } else { - carry = 0 - } - sum.n_value[sumptr--] = tmp - } - } - - // Set final carry. - if (carry === 1) { - sum.n_value[sumptr] += 1 - // *sumptr += 1; - } - - // Adjust sum and return. - Libbcmath._bc_rm_leading_zeros(sum) - return sum - }, - - /** - * Perform a subtraction - * - * Perform subtraction: N2 is subtracted from N1 and the value is - * returned. The signs of N1 and N2 are ignored. Also, N1 is - * assumed to be larger than N2. SCALE_MIN is the minimum scale - * of the result. - * - * Basic school maths says to subtract 2 numbers.. - * 1. make them the same length, the decimal places, and the integer part - * 2. start from the right and subtract the two numbers from each other - * 3. if the sum of the 2 numbers < 0, carry -1 to the next set and add 10 - * (ie 18 > carry 1 becomes 8). thus 0.9 + 0.9 = 1.8 - * - * @param {bc_num} n1 - * @param {bc_num} n2 - * @param {int} scaleMin - * @return bc_num - */ - _bc_do_sub: function (n1, n2, scaleMin) { - let diff // bc_num - let diffScale - let diffLen // int - let minScale - let minLen // int - let n1ptr - let n2ptr - let diffptr // int - let borrow - let count - let val // int - // Allocate temporary storage. - diffLen = Libbcmath.MAX(n1.n_len, n2.n_len) - diffScale = Libbcmath.MAX(n1.n_scale, n2.n_scale) - minLen = Libbcmath.MIN(n1.n_len, n2.n_len) - minScale = Libbcmath.MIN(n1.n_scale, n2.n_scale) - diff = Libbcmath.bc_new_num(diffLen, Libbcmath.MAX(diffScale, scaleMin)) - - /* Not needed? - // Zero extra digits made by scaleMin. - if (scaleMin > diffScale) { - diffptr = (char *) (diff->n_value + diffLen + diffScale); - for (count = scaleMin - diffScale; count > 0; count--) { - *diffptr++ = 0; - } - } - */ - - // Initialize the subtract. - n1ptr = n1.n_len + n1.n_scale - 1 - n2ptr = n2.n_len + n2.n_scale - 1 - diffptr = diffLen + diffScale - 1 - - // Subtract the numbers. - borrow = 0 - - // Take care of the longer scaled number. - if (n1.n_scale !== minScale) { - // n1 has the longer scale - for (count = n1.n_scale - minScale; count > 0; count--) { - diff.n_value[diffptr--] = n1.n_value[n1ptr--] - // *diffptr-- = *n1ptr--; - } - } else { - // n2 has the longer scale - for (count = n2.n_scale - minScale; count > 0; count--) { - val = 0 - n2.n_value[n2ptr--] - borrow - // val = - *n2ptr-- - borrow; - if (val < 0) { - val += Libbcmath.BASE - borrow = 1 - } else { - borrow = 0 - } - diff.n_value[diffptr--] = val - //* diffptr-- = val; - } - } - - // Now do the equal length scale and integer parts. - for (count = 0; count < minLen + minScale; count++) { - val = n1.n_value[n1ptr--] - n2.n_value[n2ptr--] - borrow - // val = *n1ptr-- - *n2ptr-- - borrow; - if (val < 0) { - val += Libbcmath.BASE - borrow = 1 - } else { - borrow = 0 - } - diff.n_value[diffptr--] = val - //* diffptr-- = val; - } - - // If n1 has more digits then n2, we now do that subtract. - if (diffLen !== minLen) { - for (count = diffLen - minLen; count > 0; count--) { - val = n1.n_value[n1ptr--] - borrow - // val = *n1ptr-- - borrow; - if (val < 0) { - val += Libbcmath.BASE - borrow = 1 - } else { - borrow = 0 - } - diff.n_value[diffptr--] = val - } - } - - // Clean up and return. - Libbcmath._bc_rm_leading_zeros(diff) - return diff - }, - - /** - * - * @param {int} length - * @param {int} scale - * @return bc_num - */ - bc_new_num: function (length, scale) { - let temp // bc_num - temp = new Libbcmath.bc_num() - temp.n_sign = Libbcmath.PLUS - temp.n_len = length - temp.n_scale = scale - temp.n_value = Libbcmath.safe_emalloc(1, length + scale, 0) - Libbcmath.memset(temp.n_value, 0, 0, length + scale) - return temp - }, - - safe_emalloc: function (size, len, extra) { - return new Array(size * len + extra) - }, - - /** - * Create a new number - */ - bc_init_num: function () { - return new Libbcmath.bc_new_num(1, 0) - }, - - _bc_rm_leading_zeros: function (num) { - // We can move n_value to point to the first non zero digit! - while (num.n_value[0] === 0 && num.n_len > 1) { - num.n_value.shift() - num.n_len-- - } - }, - - /** - * Convert to bc_num detecting scale - */ - php_str2num: function (str) { - let p - p = str.indexOf('.') - if (p === -1) { - return Libbcmath.bc_str2num(str, 0) - } else { - return Libbcmath.bc_str2num(str, str.length - p) - } - }, - - CH_VAL: function (c) { - return c - '0' // ?? - }, - - BCD_CHAR: function (d) { - return d + '0' // ?? - }, - - isdigit: function (c) { - return isNaN(parseInt(c, 10)) - }, - - bc_str2num: function (strIn, scale) { - let str - let num - let ptr - let digits - let strscale - let zeroInt - let nptr - // remove any non-expected characters - // Check for valid number and count digits. - - str = strIn.split('') // convert to array - ptr = 0 // str - digits = 0 - strscale = 0 - zeroInt = false - if (str[ptr] === '+' || str[ptr] === '-') { - ptr++ // Sign - } - while (str[ptr] === '0') { - ptr++ // Skip leading zeros. - } - // while (Libbcmath.isdigit(str[ptr])) { - while (str[ptr] % 1 === 0) { - // Libbcmath.isdigit(str[ptr])) { - ptr++ - digits++ // digits - } - - if (str[ptr] === '.') { - ptr++ // decimal point - } - // while (Libbcmath.isdigit(str[ptr])) { - while (str[ptr] % 1 === 0) { - // Libbcmath.isdigit(str[ptr])) { - ptr++ - strscale++ // digits - } - - if (str[ptr] || digits + strscale === 0) { - // invalid number, return 0 - return Libbcmath.bc_init_num() - //* num = bc_copy_num (BCG(_zero_)); - } - - // Adjust numbers and allocate storage and initialize fields. - strscale = Libbcmath.MIN(strscale, scale) - if (digits === 0) { - zeroInt = true - digits = 1 - } - - num = Libbcmath.bc_new_num(digits, strscale) - - // Build the whole number. - ptr = 0 // str - if (str[ptr] === '-') { - num.n_sign = Libbcmath.MINUS - // (*num)->n_sign = MINUS; - ptr++ - } else { - num.n_sign = Libbcmath.PLUS - // (*num)->n_sign = PLUS; - if (str[ptr] === '+') { - ptr++ - } - } - while (str[ptr] === '0') { - ptr++ // Skip leading zeros. - } - - nptr = 0 // (*num)->n_value; - if (zeroInt) { - num.n_value[nptr++] = 0 - digits = 0 - } - for (; digits > 0; digits--) { - num.n_value[nptr++] = Libbcmath.CH_VAL(str[ptr++]) - //* nptr++ = CH_VAL(*ptr++); - } - - // Build the fractional part. - if (strscale > 0) { - ptr++ // skip the decimal point! - for (; strscale > 0; strscale--) { - num.n_value[nptr++] = Libbcmath.CH_VAL(str[ptr++]) - } - } - - return num - }, - - cint: function (v) { - if (typeof v === 'undefined') { - v = 0 - } - let x = parseInt(v, 10) - if (isNaN(x)) { - x = 0 - } - return x - }, - - /** - * Basic min function - * @param {int} a - * @param {int} b - */ - MIN: function (a, b) { - return a > b ? b : a - }, - - /** - * Basic max function - * @param {int} a - * @param {int} b - */ - MAX: function (a, b) { - return a > b ? a : b - }, - - /** - * Basic odd function - * @param {int} a - */ - ODD: function (a) { - return a & 1 - }, - - /** - * replicate c function - * @param {array} r return (by reference) - * @param {int} ptr - * @param {string} chr char to fill - * @param {int} len length to fill - */ - memset: function (r, ptr, chr, len) { - let i - for (i = 0; i < len; i++) { - r[ptr + i] = chr - } - }, - - /** - * Replacement c function - * Obviously can't work like c does, so we've added an "offset" - * param so you could do memcpy(dest+1, src, len) as memcpy(dest, 1, src, len) - * Also only works on arrays - */ - memcpy: function (dest, ptr, src, srcptr, len) { - let i - for (i = 0; i < len; i++) { - dest[ptr + i] = src[srcptr + i] - } - return true - }, - - /** - * Determine if the number specified is zero or not - * @param {bc_num} num number to check - * @return boolean true when zero, false when not zero. - */ - bc_is_zero: function (num) { - let count // int - let nptr // int - // Quick check. - // if (num === BCG(_zero_)) return TRUE; - // Initialize - count = num.n_len + num.n_scale - nptr = 0 // num->n_value; - // The check - while (count > 0 && num.n_value[nptr++] === 0) { - count-- - } - - if (count !== 0) { - return false - } else { - return true - } - }, - - bc_out_of_memory: function () { - throw new Error('(BC) Out of memory') - }, - } - return Libbcmath -}
src/php/_helpers/_bc.ts+1344 −0 added@@ -0,0 +1,1344 @@ +import type { PhpInput } from './_phpTypes.ts' +// SPDX-License-Identifier: LGPL-2.1-or-later +// NOTE: This file is LGPL licensed, not MIT like the rest of locutus. +// See LICENSE file for details. + + +function createBcLibrary() { + // original by: lmeyrick (https://sourceforge.net/projects/bcmath-js/) + // improved by: Brett Zamir (https://brett-zamir.me) + + /** + * BC Math Library for Javascript (LGPL-2.1) + * Ported from the PHP5 bcmath extension source code, + * which uses the Libbcmath package... + * Copyright (C) 1991, 1992, 1993, 1994, 1997 Free Software Foundation, Inc. + * Copyright (C) 2000 Philip A. Nelson + * The Free Software Foundation, Inc. + * 59 Temple Place, Suite 330 + * Boston, MA 02111-1307 USA. + * e-mail: philnelson@acm.org + * us-mail: Philip A. Nelson + * Computer Science Department, 9062 + * Western Washington University + * Bellingham, WA 98226-9062 + * + * bcmath-js homepage: https://sourceforge.net/projects/bcmath-js/ + * + * This code is covered under the LGPL-2.1 license. + */ + + /** + * Binary Calculator (BC) Arbitrary Precision Mathematics Lib v0.10 (LGPL) + * Copy of Libbcmath included in PHP5 src + * + * Note: this is just the shared library file and does not include the php-style functions. + * use bcmath{-min}.js for functions like bcadd, bcsub etc. + * + * Feel free to use how-ever you want, just email any bug-fixes/improvements + * to the sourceforge project: + * + * + * Ported from the PHP5 bcmath extension source code, + * which uses the Libbcmath package... + * Copyright (C) 1991, 1992, 1993, 1994, 1997 Free Software Foundation, Inc. + * Copyright (C) 2000 Philip A. Nelson + * The Free Software Foundation, Inc. + * 59 Temple Place, Suite 330 + * Boston, MA 02111-1307 USA. + * e-mail: philnelson@acm.org + * us-mail: Philip A. Nelson + * Computer Science Department, 9062 + * Western Washington University + * Bellingham, WA 98226-9062 + */ + + type BcNum = { + n_sign: string + n_len: number + n_scale: number + n_value: number[] + toString: () => string + } + const digitAt = (value: number[], index: number): number => value[index] ?? 0 + + const Libbcmath = { + PLUS: '+', + MINUS: '-', + BASE: 10, + // must be 10 (for now) + scale: 0, + // default scale + /** + * Basic number structure + */ + bc_num: function (): BcNum { + const value: BcNum = { + n_sign: Libbcmath.PLUS, // sign + n_len: 0, // (int) The number of digits before the decimal point. + n_scale: 0, // (int) The number of digits after the decimal point. + // this.n_refs = null; // (int) The number of pointers to this number. + // this.n_text = null; // ?? Linked list for available list. + n_value: [], // array as value, where 1.23 = [1,2,3] + toString: function () { + let r + let tmp + tmp = value.n_value.join('') + + // add minus sign (if applicable) then add the integer part + r = (value.n_sign === Libbcmath.PLUS ? '' : value.n_sign) + tmp.substr(0, value.n_len) + + // if decimal places, add a . and the decimal part + if (value.n_scale > 0) { + r += '.' + tmp.substr(value.n_len, value.n_scale) + } + return r + }, + } + return value + }, + + /** + * Base add function + * + // Here is the full add routine that takes care of negative numbers. + // N1 is added to N2 and the result placed into RESULT. SCALE_MIN + // is the minimum scale for the result. + * + * @param {bc_num} n1 + * @param {bc_num} n2 + * @param {int} scaleMin + * @return bc_num + */ + bc_add: function (n1: BcNum, n2: BcNum, scaleMin: number): BcNum { + let sum: BcNum = Libbcmath.bc_init_num() + let cmpRes + let resScale + + if (n1.n_sign === n2.n_sign) { + sum = Libbcmath._bc_do_add(n1, n2, scaleMin) + sum.n_sign = n1.n_sign + } else { + // subtraction must be done. + cmpRes = Libbcmath._bc_do_compare(n1, n2, false, false) // Compare magnitudes. + switch (cmpRes) { + case -1: + // n1 is less than n2, subtract n1 from n2. + sum = Libbcmath._bc_do_sub(n2, n1, scaleMin) + sum.n_sign = n2.n_sign + break + + case 0: + // They are equal! return zero with the correct scale! + resScale = Libbcmath.MAX(scaleMin, Libbcmath.MAX(n1.n_scale, n2.n_scale)) + sum = Libbcmath.bc_new_num(1, resScale) + Libbcmath.memset(sum.n_value, 0, 0, resScale + 1) + break + + case 1: + // n2 is less than n1, subtract n2 from n1. + sum = Libbcmath._bc_do_sub(n1, n2, scaleMin) + sum.n_sign = n1.n_sign + } + } + return sum + }, + + /** + * This is the "user callable" routine to compare numbers N1 and N2. + * @param {bc_num} n1 + * @param {bc_num} n2 + * @return int -1, 0, 1 (n1 < n2, ===, n1 > n2) + */ + bc_compare: function (n1: BcNum, n2: BcNum): -1 | 0 | 1 { + return Libbcmath._bc_do_compare(n1, n2, true, false) + }, + + _one_mult: function (num: number[], nPtr: number, size: number, digit: number, result: number[], rPtr: number) { + let carry + let value // int + let nptr + let rptr // int pointers + if (digit === 0) { + Libbcmath.memset(result, 0, 0, size) // memset (result, 0, size); + } else { + if (digit === 1) { + Libbcmath.memcpy(result, rPtr, num, nPtr, size) // memcpy (result, num, size); + } else { + // Initialize + nptr = nPtr + size - 1 // nptr = (unsigned char *) (num+size-1); + rptr = rPtr + size - 1 // rptr = (unsigned char *) (result+size-1); + carry = 0 + + while (size-- > 0) { + value = digitAt(num, nptr--) * digit + carry // value = *nptr-- * digit + carry; + result[rptr--] = value % Libbcmath.BASE // @CHECK cint //*rptr-- = value % BASE; + carry = Math.floor(value / Libbcmath.BASE) // @CHECK cint //carry = value / BASE; + } + + if (carry !== 0) { + result[rptr] = carry + } + } + } + }, + + bc_divide: function (n1: BcNum, n2: BcNum, scale: number): BcNum | -1 { + // var quot // bc_num return + let qval // bc_num + let num1: number[] + let num2: number[] // string + let ptr1 + let ptr2 + let n2ptr + let qptr // int pointers + let scale1 + let val // int + let len1 + let len2 + let scale2 + let qdigits + let extra + let count // int + let qdig + let qguess + let borrow + let carry // int + let mval: number[] // string + let zero // char + let norm // int + // var ptrs // return object from one_mul + // Test for divide by zero. (return failure) + if (Libbcmath.bc_is_zero(n2)) { + return -1 + } + + // Test for zero divide by anything (return zero) + if (Libbcmath.bc_is_zero(n1)) { + return Libbcmath.bc_new_num(1, scale) + } + + /* Test for n1 equals n2 (return 1 as n1 nor n2 are zero) + if (Libbcmath.bc_compare(n1, n2, Libbcmath.MAX(n1.n_scale, n2.n_scale)) === 0) { + quot=Libbcmath.bc_new_num(1, scale); + quot.n_value[0] = 1; + return quot; + } + */ + + // Test for divide by 1. If it is we must truncate. + // @todo: check where scale > 0 too.. can't see why not + // (ie bc_is_zero - add bc_is_one function) + if (n2.n_scale === 0) { + if (n2.n_len === 1 && n2.n_value[0] === 1) { + qval = Libbcmath.bc_new_num(n1.n_len, scale) // qval = bc_new_num (n1->n_len, scale); + qval.n_sign = n1.n_sign === n2.n_sign ? Libbcmath.PLUS : Libbcmath.MINUS + // memset (&qval->n_value[n1->n_len],0,scale): + Libbcmath.memset(qval.n_value, n1.n_len, 0, scale) + // memcpy (qval->n_value, n1->n_value, n1->n_len + MIN(n1->n_scale,scale)): + Libbcmath.memcpy(qval.n_value, 0, n1.n_value, 0, n1.n_len + Libbcmath.MIN(n1.n_scale, scale)) + // can we return here? not in c src, but can't see why-not. + // return qval; + } + } + + /* Set up the divide. Move the decimal point on n1 by n2's scale. + Remember, zeros on the end of num2 are wasted effort for dividing. */ + scale2 = n2.n_scale // scale2 = n2->n_scale; + n2ptr = n2.n_len + scale2 - 1 // n2ptr = (unsigned char *) n2.n_value+n2.n_len+scale2-1; + while (scale2 > 0 && n2.n_value[n2ptr--] === 0) { + scale2-- + } + + len1 = n1.n_len + scale2 + scale1 = n1.n_scale - scale2 + if (scale1 < scale) { + extra = scale - scale1 + } else { + extra = 0 + } + + // num1 = (unsigned char *) safe_emalloc (1, n1.n_len+n1.n_scale, extra+2): + num1 = Libbcmath.safe_emalloc(1, n1.n_len + n1.n_scale, extra + 2) + if (num1 === null) { + Libbcmath.bc_out_of_memory() + } + // memset (num1, 0, n1->n_len+n1->n_scale+extra+2): + Libbcmath.memset(num1, 0, 0, n1.n_len + n1.n_scale + extra + 2) + // memcpy (num1+1, n1.n_value, n1.n_len+n1.n_scale): + Libbcmath.memcpy(num1, 1, n1.n_value, 0, n1.n_len + n1.n_scale) + // len2 = n2->n_len + scale2: + len2 = n2.n_len + scale2 + // num2 = (unsigned char *) safe_emalloc (1, len2, 1): + num2 = Libbcmath.safe_emalloc(1, len2, 1) + if (num2 === null) { + Libbcmath.bc_out_of_memory() + } + // memcpy (num2, n2.n_value, len2): + Libbcmath.memcpy(num2, 0, n2.n_value, 0, len2) + // *(num2+len2) = 0: + num2[len2] = 0 + // n2ptr = num2: + n2ptr = 0 + // while (*n2ptr === 0): + while (num2[n2ptr] === 0) { + n2ptr++ + len2-- + } + + // Calculate the number of quotient digits. + if (len2 > len1 + scale) { + qdigits = scale + 1 + zero = true + } else { + zero = false + if (len2 > len1) { + qdigits = scale + 1 // One for the zero integer part. + } else { + qdigits = len1 - len2 + scale + 1 + } + } + + // Allocate and zero the storage for the quotient. + // qval = bc_new_num (qdigits-scale,scale); + qval = Libbcmath.bc_new_num(qdigits - scale, scale) + // memset (qval->n_value, 0, qdigits); + Libbcmath.memset(qval.n_value, 0, 0, qdigits) + // Allocate storage for the temporary storage mval. + // mval = (unsigned char *) safe_emalloc (1, len2, 1); + mval = Libbcmath.safe_emalloc(1, len2, 1) + if (mval === null) { + Libbcmath.bc_out_of_memory() + } + + // Now for the full divide algorithm. + if (!zero) { + // Normalize + // norm = Libbcmath.cint(10 / (Libbcmath.cint(n2.n_value[n2ptr]) + 1)); + // norm = 10 / ((int)*n2ptr + 1) + norm = Math.floor(10 / (digitAt(n2.n_value, n2ptr) + 1)) // norm = 10 / ((int)*n2ptr + 1); + if (norm !== 1) { + // Libbcmath._one_mult(num1, len1+scale1+extra+1, norm, num1); + Libbcmath._one_mult(num1, 0, len1 + scale1 + extra + 1, norm, num1, 0) + // Libbcmath._one_mult(n2ptr, len2, norm, n2ptr); + Libbcmath._one_mult(n2.n_value, n2ptr, len2, norm, n2.n_value, n2ptr) + // @todo: Check: Is the pointer affected by the call? if so, + // maybe need to adjust points on return? + } + + // Initialize divide loop. + qdig = 0 + if (len2 > len1) { + qptr = len2 - len1 // qptr = (unsigned char *) qval.n_value+len2-len1; + } else { + qptr = 0 // qptr = (unsigned char *) qval.n_value; + } + + // Loop + while (qdig <= len1 + scale - len2) { + // Calculate the quotient digit guess. + if (n2.n_value[n2ptr] === num1[qdig]) { + qguess = 9 + } else { + qguess = Math.floor((digitAt(num1, qdig) * 10 + digitAt(num1, qdig + 1)) / digitAt(n2.n_value, n2ptr)) + } + // Test qguess. + + if ( + digitAt(n2.n_value, n2ptr + 1) * qguess > + (digitAt(num1, qdig) * 10 + digitAt(num1, qdig + 1) - digitAt(n2.n_value, n2ptr) * qguess) * 10 + + digitAt(num1, qdig + 2) + ) { + qguess-- + // And again. + if ( + digitAt(n2.n_value, n2ptr + 1) * qguess > + (digitAt(num1, qdig) * 10 + digitAt(num1, qdig + 1) - digitAt(n2.n_value, n2ptr) * qguess) * 10 + + digitAt(num1, qdig + 2) + ) { + qguess-- + } + } + + // Multiply and subtract. + borrow = 0 + if (qguess !== 0) { + mval[0] = 0 //* mval = 0; // @CHECK is this to fix ptr2 < 0? + // _one_mult (n2ptr, len2, qguess, mval+1); // @CHECK + Libbcmath._one_mult(n2.n_value, n2ptr, len2, qguess, mval, 1) + ptr1 = qdig + len2 // (unsigned char *) num1+qdig+len2; + ptr2 = len2 // (unsigned char *) mval+len2; + // @todo: CHECK: Does a negative pointer return null? + // ptr2 can be < 0 here as ptr1 = len2, thus count < len2+1 will always fail ? + for (count = 0; count < len2 + 1; count++) { + if (ptr2 < 0) { + // val = Libbcmath.cint(num1[ptr1]) - 0 - borrow; + // val = (int) *ptr1 - (int) *ptr2-- - borrow; + val = digitAt(num1, ptr1) - 0 - borrow // val = (int) *ptr1 - (int) *ptr2-- - borrow; + } else { + // val = Libbcmath.cint(num1[ptr1]) - Libbcmath.cint(mval[ptr2--]) - borrow; + // val = (int) *ptr1 - (int) *ptr2-- - borrow; + // val = (int) *ptr1 - (int) *ptr2-- - borrow; + val = digitAt(num1, ptr1) - digitAt(mval, ptr2--) - borrow + } + if (val < 0) { + val += 10 + borrow = 1 + } else { + borrow = 0 + } + num1[ptr1--] = val + } + } + + // Test for negative result. + if (borrow === 1) { + qguess-- + ptr1 = qdig + len2 // (unsigned char *) num1+qdig+len2; + ptr2 = len2 - 1 // (unsigned char *) n2ptr+len2-1; + carry = 0 + for (count = 0; count < len2; count++) { + if (ptr2 < 0) { + // val = Libbcmath.cint(num1[ptr1]) + 0 + carry; + // val = (int) *ptr1 + (int) *ptr2-- + carry; + // val = (int) *ptr1 + (int) *ptr2-- + carry; + val = digitAt(num1, ptr1) + 0 + carry + } else { + // val = Libbcmath.cint(num1[ptr1]) + Libbcmath.cint(n2.n_value[ptr2--]) + carry; + // val = (int) *ptr1 + (int) *ptr2-- + carry; + // val = (int) *ptr1 + (int) *ptr2-- + carry; + val = digitAt(num1, ptr1) + digitAt(n2.n_value, ptr2--) + carry + } + if (val > 9) { + val -= 10 + carry = 1 + } else { + carry = 0 + } + num1[ptr1--] = val //* ptr1-- = val; + } + if (carry === 1) { + // num1[ptr1] = Libbcmath.cint((num1[ptr1] + 1) % 10); + // *ptr1 = (*ptr1 + 1) % 10; // @CHECK + // *ptr1 = (*ptr1 + 1) % 10; // @CHECK + num1[ptr1] = (digitAt(num1, ptr1) + 1) % 10 + } + } + + // We now know the quotient digit. + qval.n_value[qptr++] = qguess //* qptr++ = qguess; + qdig++ + } + } + + // Clean up and return the number. + qval.n_sign = n1.n_sign === n2.n_sign ? Libbcmath.PLUS : Libbcmath.MINUS + if (Libbcmath.bc_is_zero(qval)) { + qval.n_sign = Libbcmath.PLUS + } + Libbcmath._bc_rm_leading_zeros(qval) + + return qval + + // return 0; // Everything is OK. + }, + + MUL_BASE_DIGITS: 80, + MUL_SMALL_DIGITS: 80 / 4, + // #define MUL_SMALL_DIGITS mul_base_digits/4 + + /* The multiply routine. N2 times N1 is put int PROD with the scale of + the result being MIN(N2 scale+N1 scale, MAX (SCALE, N2 scale, N1 scale)). + */ + /** + * @param n1 bc_num + * @param n2 bc_num + * @param scale [int] optional + */ + bc_multiply: function (n1: BcNum, n2: BcNum, scale: number): BcNum { + let pval: BcNum // bc_num + let len1 + let len2 // int + let fullScale + let prodScale // int + // Initialize things. + len1 = n1.n_len + n1.n_scale + len2 = n2.n_len + n2.n_scale + fullScale = n1.n_scale + n2.n_scale + prodScale = Libbcmath.MIN(fullScale, Libbcmath.MAX(scale, Libbcmath.MAX(n1.n_scale, n2.n_scale))) + + // pval = Libbcmath.bc_init_num(); // allow pass by ref + // Do the multiply + pval = Libbcmath._bc_rec_mul(n1, len1, n2, len2, fullScale) + + // Assign to prod and clean up the number. + pval.n_sign = n1.n_sign === n2.n_sign ? Libbcmath.PLUS : Libbcmath.MINUS + // pval.n_value = pval.nPtr; + pval.n_len = len2 + len1 + 1 - fullScale + pval.n_scale = prodScale + Libbcmath._bc_rm_leading_zeros(pval) + if (Libbcmath.bc_is_zero(pval)) { + pval.n_sign = Libbcmath.PLUS + } + // bc_free_num (prod); + return pval + }, + + new_sub_num: function (length: number, scale: number, value: number[], ptr = 0): BcNum { + const temp = Libbcmath.bc_num() + temp.n_sign = Libbcmath.PLUS + temp.n_len = length + temp.n_scale = scale + temp.n_value = Libbcmath.safe_emalloc(1, length + scale, 0) + Libbcmath.memcpy(temp.n_value, 0, value, ptr, length + scale) + return temp + }, + + _bc_simp_mul: function (n1: BcNum, n1len: number, n2: BcNum, n2len: number, fullScale: number): BcNum { + let prod: BcNum // bc_num + let n1ptr + let n2ptr + let pvptr // char *n1ptr, *n2ptr, *pvptr; + let n1end + let n2end // char *n1end, *n2end; // To the end of n1 and n2. + let indx + let sum + let prodlen // int indx, sum, prodlen; + prodlen = n1len + n2len + 1 + + prod = Libbcmath.bc_new_num(prodlen, 0) + + n1end = n1len - 1 // (char *) (n1->n_value + n1len - 1); + n2end = n2len - 1 // (char *) (n2->n_value + n2len - 1); + pvptr = prodlen - 1 // (char *) ((*prod)->n_value + prodlen - 1); + sum = 0 + + // Here is the loop... + for (indx = 0; indx < prodlen - 1; indx++) { + // (char *) (n1end - MAX(0, indx-n2len+1)); + n1ptr = n1end - Libbcmath.MAX(0, indx - n2len + 1) + // (char *) (n2end - MIN(indx, n2len-1)); + n2ptr = n2end - Libbcmath.MIN(indx, n2len - 1) + while (n1ptr >= 0 && n2ptr <= n2end) { + // sum += *n1ptr-- * *n2ptr++; + sum += digitAt(n1.n_value, n1ptr--) * digitAt(n2.n_value, n2ptr++) + } + //* pvptr-- = sum % BASE; + prod.n_value[pvptr--] = Math.floor(sum % Libbcmath.BASE) + sum = Math.floor(sum / Libbcmath.BASE) // sum = sum / BASE; + } + prod.n_value[pvptr] = sum //* pvptr = sum; + return prod + }, + + /* A special adder/subtractor for the recursive divide and conquer + multiply algorithm. Note: if sub is called, accum must + be larger that what is being subtracted. Also, accum and val + must have n_scale = 0. (e.g. they must look like integers. *) */ + _bc_shift_addsub: function (accum: BcNum, val: BcNum, shift: number, sub: number | boolean) { + let accp + let valp // signed char *accp, *valp; + let count + let carry // int count, carry; + count = val.n_len + if (val.n_value[0] === 0) { + count-- + } + + // assert (accum->n_len+accum->n_scale >= shift+count); + if (accum.n_len + accum.n_scale < shift + count) { + throw new Error('len + scale < shift + count') // ?? I think that's what assert does :) + } + + // Set up pointers and others + // (signed char *)(accum->n_value + accum->n_len + accum->n_scale - shift - 1); + accp = accum.n_len + accum.n_scale - shift - 1 + valp = val.n_len - 1 // (signed char *)(val->n_value + val->n_len - 1); + carry = 0 + if (sub) { + // Subtraction, carry is really borrow. + while (count--) { + accum.n_value[accp] = digitAt(accum.n_value, accp) - digitAt(val.n_value, valp--) - carry //* accp -= *valp-- + carry; + if (digitAt(accum.n_value, accp) < 0) { + // if (*accp < 0) + carry = 1 + accum.n_value[accp] = digitAt(accum.n_value, accp) + Libbcmath.BASE //* accp += BASE; + accp-- + } else { + carry = 0 + accp-- + } + } + while (carry) { + accum.n_value[accp] = digitAt(accum.n_value, accp) - carry //* accp -= carry; + if (digitAt(accum.n_value, accp) < 0) { + // if (*accp < 0) + accum.n_value[accp] = digitAt(accum.n_value, accp) + Libbcmath.BASE // *accp += BASE; + accp-- + } else { + carry = 0 + } + } + } else { + // Addition + while (count--) { + accum.n_value[accp] = digitAt(accum.n_value, accp) + digitAt(val.n_value, valp--) + carry //* accp += *valp-- + carry; + if (digitAt(accum.n_value, accp) > Libbcmath.BASE - 1) { + // if (*accp > (BASE-1)) + carry = 1 + accum.n_value[accp] = digitAt(accum.n_value, accp) - Libbcmath.BASE //* accp -= BASE; + accp-- + } else { + carry = 0 + accp-- + } + } + while (carry) { + accum.n_value[accp] = digitAt(accum.n_value, accp) + carry //* accp += carry; + if (digitAt(accum.n_value, accp) > Libbcmath.BASE - 1) { + // if (*accp > (BASE-1)) + accum.n_value[accp] = digitAt(accum.n_value, accp) - Libbcmath.BASE //* accp -= BASE; + accp-- + } else { + carry = 0 + } + } + } + return true // accum is the pass-by-reference return + }, + + /* Recursive divide and conquer multiply algorithm. + based on + Let u = u0 + u1*(b^n) + Let v = v0 + v1*(b^n) + Then uv = (B^2n+B^n)*u1*v1 + B^n*(u1-u0)*(v0-v1) + (B^n+1)*u0*v0 + + B is the base of storage, number of digits in u1,u0 close to equal. + */ + _bc_rec_mul: function (u: BcNum, ulen: number, v: BcNum, vlen: number, fullScale: number): BcNum { + let prod: BcNum // @return + let u0: BcNum + let u1: BcNum + let v0: BcNum + let v1: BcNum // bc_num + // var u0len, + // var v0len // int + let m1: BcNum = Libbcmath.bc_init_num() + let m2: BcNum = Libbcmath.bc_init_num() + let m3: BcNum = Libbcmath.bc_init_num() + let d1: BcNum = Libbcmath.bc_init_num() + let d2: BcNum = Libbcmath.bc_init_num() // bc_num + let n + let prodlen + let m1zero // int + let d1len + let d2len // int + // Base case? + if ( + ulen + vlen < Libbcmath.MUL_BASE_DIGITS || + ulen < Libbcmath.MUL_SMALL_DIGITS || + vlen < Libbcmath.MUL_SMALL_DIGITS + ) { + return Libbcmath._bc_simp_mul(u, ulen, v, vlen, fullScale) + } + + // Calculate n -- the u and v split point in digits. + n = Math.floor((Libbcmath.MAX(ulen, vlen) + 1) / 2) + + // Split u and v. + if (ulen < n) { + u1 = Libbcmath.bc_init_num() // u1 = bc_copy_num (BCG(_zero_)); + u0 = Libbcmath.new_sub_num(ulen, 0, u.n_value) + } else { + u1 = Libbcmath.new_sub_num(ulen - n, 0, u.n_value) + u0 = Libbcmath.new_sub_num(n, 0, u.n_value, ulen - n) + } + if (vlen < n) { + v1 = Libbcmath.bc_init_num() // bc_copy_num (BCG(_zero_)); + v0 = Libbcmath.new_sub_num(vlen, 0, v.n_value) + } else { + v1 = Libbcmath.new_sub_num(vlen - n, 0, v.n_value) + v0 = Libbcmath.new_sub_num(n, 0, v.n_value, vlen - n) + } + Libbcmath._bc_rm_leading_zeros(u1) + Libbcmath._bc_rm_leading_zeros(u0) + // var u0len = u0.n_len + Libbcmath._bc_rm_leading_zeros(v1) + Libbcmath._bc_rm_leading_zeros(v0) + // var v0len = v0.n_len + + m1zero = Libbcmath.bc_is_zero(u1) || Libbcmath.bc_is_zero(v1) + + // Calculate sub results ... + d1 = Libbcmath.bc_init_num() // needed? + d2 = Libbcmath.bc_init_num() // needed? + d1 = Libbcmath.bc_sub(u1, u0, 0) + d1len = d1.n_len + + d2 = Libbcmath.bc_sub(v0, v1, 0) + d2len = d2.n_len + + // Do recursive multiplies and shifted adds. + if (m1zero) { + m1 = Libbcmath.bc_init_num() // bc_copy_num (BCG(_zero_)); + } else { + // m1 = Libbcmath.bc_init_num(); //allow pass-by-ref + m1 = Libbcmath._bc_rec_mul(u1, u1.n_len, v1, v1.n_len, 0) + } + if (Libbcmath.bc_is_zero(d1) || Libbcmath.bc_is_zero(d2)) { + m2 = Libbcmath.bc_init_num() // bc_copy_num (BCG(_zero_)); + } else { + // m2 = Libbcmath.bc_init_num(); //allow pass-by-ref + m2 = Libbcmath._bc_rec_mul(d1, d1len, d2, d2len, 0) + } + + if (Libbcmath.bc_is_zero(u0) || Libbcmath.bc_is_zero(v0)) { + m3 = Libbcmath.bc_init_num() // bc_copy_num (BCG(_zero_)); + } else { + // m3 = Libbcmath.bc_init_num(); //allow pass-by-ref + m3 = Libbcmath._bc_rec_mul(u0, u0.n_len, v0, v0.n_len, 0) + } + + // Initialize product + prodlen = ulen + vlen + 1 + prod = Libbcmath.bc_new_num(prodlen, 0) + + if (!m1zero) { + Libbcmath._bc_shift_addsub(prod, m1, 2 * n, 0) + Libbcmath._bc_shift_addsub(prod, m1, n, 0) + } + Libbcmath._bc_shift_addsub(prod, m3, n, 0) + Libbcmath._bc_shift_addsub(prod, m3, 0, 0) + Libbcmath._bc_shift_addsub(prod, m2, n, d1.n_sign !== d2.n_sign) + + return prod + // Now clean up! + // bc_free_num (&u1); + // bc_free_num (&u0); + // bc_free_num (&v1); + // bc_free_num (&m1); + // bc_free_num (&v0); + // bc_free_num (&m2); + // bc_free_num (&m3); + // bc_free_num (&d1); + // bc_free_num (&d2); + }, + + /** + * + * @param {bc_num} n1 + * @param {bc_num} n2 + * @param {boolean} useSign + * @param {boolean} ignoreLast + * @return -1, 0, 1 (see bc_compare) + */ + _bc_do_compare: function (n1: BcNum, n2: BcNum, useSign: boolean, ignoreLast: boolean): -1 | 0 | 1 { + let n1ptr + let n2ptr // int + let count // int + // First, compare signs. + if (useSign && n1.n_sign !== n2.n_sign) { + if (n1.n_sign === Libbcmath.PLUS) { + return 1 // Positive N1 > Negative N2 + } else { + return -1 // Negative N1 < Positive N1 + } + } + + // Now compare the magnitude. + if (n1.n_len !== n2.n_len) { + if (n1.n_len > n2.n_len) { + // Magnitude of n1 > n2. + if (!useSign || n1.n_sign === Libbcmath.PLUS) { + return 1 + } else { + return -1 + } + } else { + // Magnitude of n1 < n2. + if (!useSign || n1.n_sign === Libbcmath.PLUS) { + return -1 + } else { + return 1 + } + } + } + + /* If we get here, they have the same number of integer digits. + check the integer part and the equal length part of the fraction. */ + count = n1.n_len + Math.min(n1.n_scale, n2.n_scale) + n1ptr = 0 + n2ptr = 0 + + while (count > 0 && n1.n_value[n1ptr] === n2.n_value[n2ptr]) { + n1ptr++ + n2ptr++ + count-- + } + + if (ignoreLast && count === 1 && n1.n_scale === n2.n_scale) { + return 0 + } + + if (count !== 0) { + if (digitAt(n1.n_value, n1ptr) > digitAt(n2.n_value, n2ptr)) { + // Magnitude of n1 > n2. + if (!useSign || n1.n_sign === Libbcmath.PLUS) { + return 1 + } else { + return -1 + } + } else { + // Magnitude of n1 < n2. + if (!useSign || n1.n_sign === Libbcmath.PLUS) { + return -1 + } else { + return 1 + } + } + } + + // They are equal up to the last part of the equal part of the fraction. + if (n1.n_scale !== n2.n_scale) { + if (n1.n_scale > n2.n_scale) { + for (count = n1.n_scale - n2.n_scale; count > 0; count--) { + if (digitAt(n1.n_value, n1ptr++) !== 0) { + // Magnitude of n1 > n2. + if (!useSign || n1.n_sign === Libbcmath.PLUS) { + return 1 + } else { + return -1 + } + } + } + } else { + for (count = n2.n_scale - n1.n_scale; count > 0; count--) { + if (digitAt(n2.n_value, n2ptr++) !== 0) { + // Magnitude of n1 < n2. + if (!useSign || n1.n_sign === Libbcmath.PLUS) { + return -1 + } else { + return 1 + } + } + } + } + } + + // They must be equal! + return 0 + }, + + /* Here is the full subtract routine that takes care of negative numbers. + N2 is subtracted from N1 and the result placed in RESULT. SCALE_MIN + is the minimum scale for the result. */ + bc_sub: function (n1: BcNum, n2: BcNum, scaleMin: number): BcNum { + let diff: BcNum = Libbcmath.bc_init_num() // bc_num + let cmpRes + let resScale // int + if (n1.n_sign !== n2.n_sign) { + diff = Libbcmath._bc_do_add(n1, n2, scaleMin) + diff.n_sign = n1.n_sign + } else { + // subtraction must be done. + // Compare magnitudes. + cmpRes = Libbcmath._bc_do_compare(n1, n2, false, false) + switch (cmpRes) { + case -1: + // n1 is less than n2, subtract n1 from n2. + diff = Libbcmath._bc_do_sub(n2, n1, scaleMin) + diff.n_sign = n2.n_sign === Libbcmath.PLUS ? Libbcmath.MINUS : Libbcmath.PLUS + break + case 0: + // They are equal! return zero! + resScale = Libbcmath.MAX(scaleMin, Libbcmath.MAX(n1.n_scale, n2.n_scale)) + diff = Libbcmath.bc_new_num(1, resScale) + Libbcmath.memset(diff.n_value, 0, 0, resScale + 1) + break + case 1: + // n2 is less than n1, subtract n2 from n1. + diff = Libbcmath._bc_do_sub(n1, n2, scaleMin) + diff.n_sign = n1.n_sign + break + } + } + + // Clean up and return. + // bc_free_num (result); + //* result = diff; + return diff + }, + + _bc_do_add: function (n1: BcNum, n2: BcNum, scaleMin: number): BcNum { + let sum: BcNum // bc_num + let sumScale + let sumDigits // int + let n1ptr + let n2ptr + let sumptr // int + let carry + let n1bytes + let n2bytes // int + let tmp // int + + // Prepare sum. + sumScale = Libbcmath.MAX(n1.n_scale, n2.n_scale) + sumDigits = Libbcmath.MAX(n1.n_len, n2.n_len) + 1 + sum = Libbcmath.bc_new_num(sumDigits, Libbcmath.MAX(sumScale, scaleMin)) + + // Start with the fraction part. Initialize the pointers. + n1bytes = n1.n_scale + n2bytes = n2.n_scale + n1ptr = n1.n_len + n1bytes - 1 + n2ptr = n2.n_len + n2bytes - 1 + sumptr = sumScale + sumDigits - 1 + + // Add the fraction part. First copy the longer fraction + // (ie when adding 1.2345 to 1 we know .2345 is correct already) . + if (n1bytes !== n2bytes) { + if (n1bytes > n2bytes) { + // n1 has more dp then n2 + while (n1bytes > n2bytes) { + sum.n_value[sumptr--] = digitAt(n1.n_value, n1ptr--) + // *sumptr-- = *n1ptr--; + n1bytes-- + } + } else { + // n2 has more dp then n1 + while (n2bytes > n1bytes) { + sum.n_value[sumptr--] = digitAt(n2.n_value, n2ptr--) + // *sumptr-- = *n2ptr--; + n2bytes-- + } + } + } + + // Now add the remaining fraction part and equal size integer parts. + n1bytes += n1.n_len + n2bytes += n2.n_len + carry = 0 + while (n1bytes > 0 && n2bytes > 0) { + // add the two numbers together + tmp = digitAt(n1.n_value, n1ptr--) + digitAt(n2.n_value, n2ptr--) + carry + // *sumptr = *n1ptr-- + *n2ptr-- + carry; + // check if they are >= 10 (impossible to be more then 18) + if (tmp >= Libbcmath.BASE) { + carry = 1 + tmp -= Libbcmath.BASE // yep, subtract 10, add a carry + } else { + carry = 0 + } + sum.n_value[sumptr] = tmp + sumptr-- + n1bytes-- + n2bytes-- + } + + // Now add carry the [rest of the] longer integer part. + if (n1bytes === 0) { + // n2 is a bigger number then n1 + while (n2bytes-- > 0) { + tmp = digitAt(n2.n_value, n2ptr--) + carry + // *sumptr = *n2ptr-- + carry; + if (tmp >= Libbcmath.BASE) { + carry = 1 + tmp -= Libbcmath.BASE + } else { + carry = 0 + } + sum.n_value[sumptr--] = tmp + } + } else { + // n1 is bigger then n2.. + while (n1bytes-- > 0) { + tmp = digitAt(n1.n_value, n1ptr--) + carry + // *sumptr = *n1ptr-- + carry; + if (tmp >= Libbcmath.BASE) { + carry = 1 + tmp -= Libbcmath.BASE + } else { + carry = 0 + } + sum.n_value[sumptr--] = tmp + } + } + + // Set final carry. + if (carry === 1) { + sum.n_value[sumptr] = digitAt(sum.n_value, sumptr) + 1 + // *sumptr += 1; + } + + // Adjust sum and return. + Libbcmath._bc_rm_leading_zeros(sum) + return sum + }, + + /** + * Perform a subtraction + * + * Perform subtraction: N2 is subtracted from N1 and the value is + * returned. The signs of N1 and N2 are ignored. Also, N1 is + * assumed to be larger than N2. SCALE_MIN is the minimum scale + * of the result. + * + * Basic school maths says to subtract 2 numbers.. + * 1. make them the same length, the decimal places, and the integer part + * 2. start from the right and subtract the two numbers from each other + * 3. if the sum of the 2 numbers < 0, carry -1 to the next set and add 10 + * (ie 18 > carry 1 becomes 8). thus 0.9 + 0.9 = 1.8 + * + * @param {bc_num} n1 + * @param {bc_num} n2 + * @param {int} scaleMin + * @return bc_num + */ + _bc_do_sub: function (n1: BcNum, n2: BcNum, scaleMin: number): BcNum { + let diff: BcNum // bc_num + let diffScale + let diffLen // int + let minScale + let minLen // int + let n1ptr + let n2ptr + let diffptr // int + let borrow + let count + let val // int + // Allocate temporary storage. + diffLen = Libbcmath.MAX(n1.n_len, n2.n_len) + diffScale = Libbcmath.MAX(n1.n_scale, n2.n_scale) + minLen = Libbcmath.MIN(n1.n_len, n2.n_len) + minScale = Libbcmath.MIN(n1.n_scale, n2.n_scale) + diff = Libbcmath.bc_new_num(diffLen, Libbcmath.MAX(diffScale, scaleMin)) + + /* Not needed? + // Zero extra digits made by scaleMin. + if (scaleMin > diffScale) { + diffptr = (char *) (diff->n_value + diffLen + diffScale); + for (count = scaleMin - diffScale; count > 0; count--) { + *diffptr++ = 0; + } + } + */ + + // Initialize the subtract. + n1ptr = n1.n_len + n1.n_scale - 1 + n2ptr = n2.n_len + n2.n_scale - 1 + diffptr = diffLen + diffScale - 1 + + // Subtract the numbers. + borrow = 0 + + // Take care of the longer scaled number. + if (n1.n_scale !== minScale) { + // n1 has the longer scale + for (count = n1.n_scale - minScale; count > 0; count--) { + diff.n_value[diffptr--] = digitAt(n1.n_value, n1ptr--) + // *diffptr-- = *n1ptr--; + } + } else { + // n2 has the longer scale + for (count = n2.n_scale - minScale; count > 0; count--) { + val = 0 - digitAt(n2.n_value, n2ptr--) - borrow + // val = - *n2ptr-- - borrow; + if (val < 0) { + val += Libbcmath.BASE + borrow = 1 + } else { + borrow = 0 + } + diff.n_value[diffptr--] = val + //* diffptr-- = val; + } + } + + // Now do the equal length scale and integer parts. + for (count = 0; count < minLen + minScale; count++) { + val = digitAt(n1.n_value, n1ptr--) - digitAt(n2.n_value, n2ptr--) - borrow + // val = *n1ptr-- - *n2ptr-- - borrow; + if (val < 0) { + val += Libbcmath.BASE + borrow = 1 + } else { + borrow = 0 + } + diff.n_value[diffptr--] = val + //* diffptr-- = val; + } + + // If n1 has more digits then n2, we now do that subtract. + if (diffLen !== minLen) { + for (count = diffLen - minLen; count > 0; count--) { + val = digitAt(n1.n_value, n1ptr--) - borrow + // val = *n1ptr-- - borrow; + if (val < 0) { + val += Libbcmath.BASE + borrow = 1 + } else { + borrow = 0 + } + diff.n_value[diffptr--] = val + } + } + + // Clean up and return. + Libbcmath._bc_rm_leading_zeros(diff) + return diff + }, + + /** + * + * @param {int} length + * @param {int} scale + * @return bc_num + */ + bc_new_num: function (length: number, scale: number): BcNum { + let temp: BcNum // bc_num + temp = Libbcmath.bc_num() + temp.n_sign = Libbcmath.PLUS + temp.n_len = length + temp.n_scale = scale + temp.n_value = Libbcmath.safe_emalloc(1, length + scale, 0) + Libbcmath.memset(temp.n_value, 0, 0, length + scale) + return temp + }, + + safe_emalloc: function (size: number, len: number, extra: number): number[] { + return new Array(size * len + extra) + }, + + /** + * Create a new number + */ + bc_init_num: function (): BcNum { + return Libbcmath.bc_new_num(1, 0) + }, + + _bc_rm_leading_zeros: function (num: BcNum) { + // We can move n_value to point to the first non zero digit! + while (num.n_value[0] === 0 && num.n_len > 1) { + num.n_value.shift() + num.n_len-- + } + }, + + /** + * Convert to bc_num detecting scale + */ + php_str2num: function (str: string): BcNum { + let p + p = str.indexOf('.') + if (p === -1) { + return Libbcmath.bc_str2num(str, 0) + } else { + return Libbcmath.bc_str2num(str, str.length - p) + } + }, + + CH_VAL: function (c: string) { + return Number(c) - 0 // ?? + }, + + BCD_CHAR: function (d: number) { + return String(d) + '0' // ?? + }, + + isdigit: function (c: string) { + return isNaN(parseInt(c, 10)) + }, + + bc_str2num: function (strIn: string, scale: number): BcNum { + let str: string[] + let num: BcNum + let ptr: number + let digits: number + let strscale: number + let zeroInt: boolean + let nptr: number + // remove any non-expected characters + // Check for valid number and count digits. + + str = strIn.split('') // convert to array + ptr = 0 // str + digits = 0 + strscale = 0 + zeroInt = false + if (str[ptr] === '+' || str[ptr] === '-') { + ptr++ // Sign + } + while (str[ptr] === '0') { + ptr++ // Skip leading zeros. + } + // while (Libbcmath.isdigit(str[ptr])) { + while (str[ptr] !== undefined && /\d/.test(str[ptr] ?? '')) { + // Libbcmath.isdigit(str[ptr])) { + ptr++ + digits++ // digits + } + + if (str[ptr] === '.') { + ptr++ // decimal point + } + // while (Libbcmath.isdigit(str[ptr])) { + while (str[ptr] !== undefined && /\d/.test(str[ptr] ?? '')) { + // Libbcmath.isdigit(str[ptr])) { + ptr++ + strscale++ // digits + } + + if (str[ptr] || digits + strscale === 0) { + // invalid number, return 0 + return Libbcmath.bc_init_num() + //* num = bc_copy_num (BCG(_zero_)); + } + + // Adjust numbers and allocate storage and initialize fields. + strscale = Libbcmath.MIN(strscale, scale) + if (digits === 0) { + zeroInt = true + digits = 1 + } + + num = Libbcmath.bc_new_num(digits, strscale) + + // Build the whole number. + ptr = 0 // str + if (str[ptr] === '-') { + num.n_sign = Libbcmath.MINUS + // (*num)->n_sign = MINUS; + ptr++ + } else { + num.n_sign = Libbcmath.PLUS + // (*num)->n_sign = PLUS; + if (str[ptr] === '+') { + ptr++ + } + } + while (str[ptr] === '0') { + ptr++ // Skip leading zeros. + } + + nptr = 0 // (*num)->n_value; + if (zeroInt) { + num.n_value[nptr++] = 0 + digits = 0 + } + for (; digits > 0; digits--) { + num.n_value[nptr++] = Libbcmath.CH_VAL(str[ptr++] ?? '0') + //* nptr++ = CH_VAL(*ptr++); + } + + // Build the fractional part. + if (strscale > 0) { + ptr++ // skip the decimal point! + for (; strscale > 0; strscale--) { + num.n_value[nptr++] = Libbcmath.CH_VAL(str[ptr++] ?? '0') + } + } + + return num + }, + + cint: function (v: PhpInput) { + if (typeof v === 'undefined') { + v = 0 + } + let x = Number.parseInt(String(v), 10) + if (isNaN(x)) { + x = 0 + } + return x + }, + + /** + * Basic min function + * @param {int} a + * @param {int} b + */ + MIN: function (a: number, b: number) { + return a > b ? b : a + }, + + /** + * Basic max function + * @param {int} a + * @param {int} b + */ + MAX: function (a: number, b: number) { + return a > b ? a : b + }, + + /** + * Basic odd function + * @param {int} a + */ + ODD: function (a: number) { + return a & 1 + }, + + /** + * replicate c function + * @param {array} r return (by reference) + * @param {int} ptr + * @param {string} chr char to fill + * @param {int} len length to fill + */ + memset: function (r: number[], ptr: number, chr: number, len: number) { + let i + for (i = 0; i < len; i++) { + r[ptr + i] = chr + } + }, + + /** + * Replacement c function + * Obviously can't work like c does, so we've added an "offset" + * param so you could do memcpy(dest+1, src, len) as memcpy(dest, 1, src, len) + * Also only works on arrays + */ + memcpy: function (dest: number[], ptr: number, src: number[], srcptr: number, len: number) { + let i + for (i = 0; i < len; i++) { + dest[ptr + i] = src[srcptr + i] ?? 0 + } + return true + }, + + /** + * Determine if the number specified is zero or not + * @param {bc_num} num number to check + * @return boolean true when zero, false when not zero. + */ + bc_is_zero: function (num: BcNum) { + let count // int + let nptr // int + // Quick check. + // if (num === BCG(_zero_)) return TRUE; + // Initialize + count = num.n_len + num.n_scale + nptr = 0 // num->n_value; + // The check + while (count > 0 && num.n_value[nptr++] === 0) { + count-- + } + + if (count !== 0) { + return false + } else { + return true + } + }, + + bc_out_of_memory: function () { + throw new Error('(BC) Out of memory') + }, + } + return Libbcmath +} + +export function _bc(): ReturnType<typeof createBcLibrary> { + // original by: lmeyrick (https://sourceforge.net/projects/bcmath-js/) + // improved by: Brett Zamir (https://brett-zamir.me) + return createBcLibrary() +}
src/php/_helpers/_callbackResolver.ts+79 −0 added@@ -0,0 +1,79 @@ +import { getPhpGlobalEntry, getPhpObjectEntry } from './_phpRuntimeState.ts' +import { + isObjectLike, + isPhpCallable, + type NumericLike, + type PhpCallable, + type PhpCallableArgs, + type PhpCallableDescriptor, + type PhpInput, +} from './_phpTypes.ts' + +type CallbackValue = PhpInput + +interface CallbackResolverOptions { + invalidMessage: string + missingScopeMessage?: (scopeName: string) => string +} + +interface ResolvedCallback<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = CallbackValue> { + fn: PhpCallable<TArgs, TResult> + scope: CallbackValue +} + +export function resolvePhpCallable<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = CallbackValue>( + callback: PhpCallableDescriptor<TArgs, TResult>, + options: CallbackResolverOptions, +): ResolvedCallback<TArgs, TResult> { + if (isPhpCallable<TArgs, TResult>(callback)) { + return { fn: callback, scope: null } + } + + if (typeof callback === 'string') { + const candidate = getPhpGlobalEntry(callback) + if (isPhpCallable<TArgs, TResult>(candidate)) { + return { fn: candidate, scope: null } + } + throw new Error(options.invalidMessage) + } + + if (Array.isArray(callback) && callback.length >= 2) { + const scopeDescriptor = callback[0] + const callableDescriptor = callback[1] + + let scope: CallbackValue + if (typeof scopeDescriptor === 'string') { + scope = getPhpGlobalEntry(scopeDescriptor) + if (typeof scope === 'undefined' && options.missingScopeMessage) { + throw new Error(options.missingScopeMessage(scopeDescriptor)) + } + } else { + scope = scopeDescriptor + } + + if (isPhpCallable<TArgs, TResult>(callableDescriptor)) { + return { fn: callableDescriptor, scope } + } + + if (typeof callableDescriptor === 'string' && (isObjectLike(scope) || typeof scope === 'function')) { + const candidate = getPhpObjectEntry(scope, callableDescriptor) + if (isPhpCallable<TArgs, TResult>(candidate)) { + return { fn: candidate, scope } + } + } + } + + throw new Error(options.invalidMessage) +} + +export function resolveNumericComparator< + TLeft extends CallbackValue = CallbackValue, + TRight extends CallbackValue = CallbackValue, +>( + callback: PhpCallableDescriptor<[TLeft, TRight], NumericLike>, + invalidMessage: string, +): (left: TLeft, right: TRight) => number { + const resolved = resolvePhpCallable<[TLeft, TRight], NumericLike>(callback, { invalidMessage }) + + return (left: TLeft, right: TRight): number => Number(resolved.fn.apply(resolved.scope, [left, right])) +}
src/php/_helpers/_ctypePattern.ts+32 −0 added@@ -0,0 +1,32 @@ +import { getPhpLocaleGroup } from './_phpRuntimeState.ts' + +const defaultCtypePatterns: { [key: string]: RegExp } = { + an: /^[A-Za-z\d]+$/g, + al: /^[A-Za-z]+$/g, + // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional for LC_CTYPE control character class + ct: /^[\u0000-\u001F\u007F]+$/g, + dg: /^[\d]+$/g, + gr: /^[\u0021-\u007E]+$/g, + lw: /^[a-z]+$/g, + pr: /^[\u0020-\u007E]+$/g, + pu: /^[\u0021-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u007E]+$/g, + sp: /^[\f\n\r\t\v ]+$/g, + up: /^[A-Z]+$/g, + xd: /^[A-Fa-f\d]+$/g, +} + +export function getCtypePattern(key: string): RegExp | undefined { + const ctypeGroup = getPhpLocaleGroup('LC_CTYPE', 'LC_CTYPE') + if (!ctypeGroup) { + const fallbackPattern = defaultCtypePatterns[key] + return fallbackPattern ? new RegExp(fallbackPattern) : undefined + } + + const pattern = ctypeGroup[key] + if (pattern instanceof RegExp) { + return new RegExp(pattern) + } + + const fallbackPattern = defaultCtypePatterns[key] + return fallbackPattern ? new RegExp(fallbackPattern) : undefined +}
src/php/_helpers/index.js+0 −4 removed@@ -1,4 +0,0 @@ -module.exports._bc = require('./_bc') -module.exports._phpCastString = require('./_phpCastString') -module.exports._php_cast_float = require('./_php_cast_float') -module.exports._php_cast_int = require('./_php_cast_int')
src/php/_helpers/index.ts+17 −0 added@@ -0,0 +1,17 @@ +export { getArrayLikeLength, getEntryAtCursor, getPointerState } from './_arrayPointers.ts' +export { _bc } from './_bc.ts' +export { resolveNumericComparator, resolvePhpCallable } from './_callbackResolver.ts' +export { _php_cast_float } from './_php_cast_float.ts' +export { _php_cast_int } from './_php_cast_int.ts' +export { _phpCastString } from './_phpCastString.ts' +export { ensurePhpRuntimeState, setPhpLocaleDefault } from './_phpRuntimeState.ts' +export { + assertIsObjectLike, + assertIsPhpCallable, + isNumericLike, + isObjectLike, + isPhpArrayObject, + isPhpCallable, + isPhpScalar, + toPhpArrayObject, +} from './_phpTypes.ts'
src/php/_helpers/_php_cast_float.js+0 −46 removed@@ -1,46 +0,0 @@ -module.exports = function _php_cast_float(value) { - // original by: Rafał Kukawski - // example 1: _php_cast_float(false) - // returns 1: 0 - // example 2: _php_cast_float(true) - // returns 2: 1 - // example 3: _php_cast_float(0) - // returns 3: 0 - // example 4: _php_cast_float(1) - // returns 4: 1 - // example 5: _php_cast_float(3.14) - // returns 5: 3.14 - // example 6: _php_cast_float('') - // returns 6: 0 - // example 7: _php_cast_float('0') - // returns 7: 0 - // example 8: _php_cast_float('abc') - // returns 8: 0 - // example 9: _php_cast_float(null) - // returns 9: 0 - // example 10: _php_cast_float(undefined) - // returns 10: 0 - // example 11: _php_cast_float('123abc') - // returns 11: 123 - // example 12: _php_cast_float('123e4') - // returns 12: 1230000 - // example 13: _php_cast_float(0x200000001) - // returns 13: 8589934593 - // example 14: _php_cast_float('3.14abc') - // returns 14: 3.14 - - const type = typeof value - - switch (type) { - case 'number': - return value - case 'string': - return parseFloat(value) || 0 - case 'boolean': - // fall through - default: - // PHP docs state, that for types other than string - // conversion is {input type}->int->float - return require('./_php_cast_int')(value) - } -}
src/php/_helpers/_php_cast_float.ts+19 −0 added@@ -0,0 +1,19 @@ +import { _php_cast_int as __php_cast_int } from './_php_cast_int.ts' +import type { PhpInput } from './_phpTypes.ts' + +type CastFloatValue = PhpInput + +export function _php_cast_float(value: CastFloatValue): number { + // original by: Rafał Kukawski + + if (typeof value === 'number') { + return value + } + if (typeof value === 'string') { + return parseFloat(value) || 0 + } + + // PHP docs state, that for types other than string + // conversion is {input type}->int->float + return __php_cast_int(value) +}
src/php/_helpers/_php_cast_int.js+0 −52 removed@@ -1,52 +0,0 @@ -module.exports = function _php_cast_int(value) { - // original by: Rafał Kukawski - // example 1: _php_cast_int(false) - // returns 1: 0 - // example 2: _php_cast_int(true) - // returns 2: 1 - // example 3: _php_cast_int(0) - // returns 3: 0 - // example 4: _php_cast_int(1) - // returns 4: 1 - // example 5: _php_cast_int(3.14) - // returns 5: 3 - // example 6: _php_cast_int('') - // returns 6: 0 - // example 7: _php_cast_int('0') - // returns 7: 0 - // example 8: _php_cast_int('abc') - // returns 8: 0 - // example 9: _php_cast_int(null) - // returns 9: 0 - // example 10: _php_cast_int(undefined) - // returns 10: 0 - // example 11: _php_cast_int('123abc') - // returns 11: 123 - // example 12: _php_cast_int('123e4') - // returns 12: 123 - // example 13: _php_cast_int(0x200000001) - // returns 13: 8589934593 - - const type = typeof value - - switch (type) { - case 'number': - if (isNaN(value) || !isFinite(value)) { - // from PHP 7, NaN and Infinity are casted to 0 - return 0 - } - - return value < 0 ? Math.ceil(value) : Math.floor(value) - case 'string': - return parseInt(value, 10) || 0 - case 'boolean': - // fall through - default: - // Behaviour for types other than float, string, boolean - // is undefined and can change any time. - // To not invent complex logic - // that mimics PHP 7.0 behaviour - // casting value->bool->number is used - return +!!value - } -}
src/php/_helpers/_php_cast_int.ts+26 −0 added@@ -0,0 +1,26 @@ +import type { PhpInput } from './_phpTypes.ts' + +type CastIntValue = PhpInput + +export function _php_cast_int(value: CastIntValue): number { + // original by: Rafał Kukawski + + if (typeof value === 'number') { + if (isNaN(value) || !isFinite(value)) { + // from PHP 7, NaN and Infinity are casted to 0 + return 0 + } + + return value < 0 ? Math.ceil(value) : Math.floor(value) + } + if (typeof value === 'string') { + return parseInt(value, 10) || 0 + } + + // Behaviour for types other than float, string, boolean + // is undefined and can change any time. + // To not invent complex logic + // that mimics PHP 7.0 behaviour + // casting value->bool->number is used + return +!!value +}
src/php/_helpers/_phpCastString.js+0 −64 removed@@ -1,64 +0,0 @@ -module.exports = function _phpCastString(value) { - // original by: Rafał Kukawski - // example 1: _phpCastString(true) - // returns 1: '1' - // example 2: _phpCastString(false) - // returns 2: '' - // example 3: _phpCastString('foo') - // returns 3: 'foo' - // example 4: _phpCastString(0/0) - // returns 4: 'NAN' - // example 5: _phpCastString(1/0) - // returns 5: 'INF' - // example 6: _phpCastString(-1/0) - // returns 6: '-INF' - // example 7: _phpCastString(null) - // returns 7: '' - // example 8: _phpCastString(undefined) - // returns 8: '' - // example 9: _phpCastString([]) - // returns 9: 'Array' - // example 10: _phpCastString({}) - // returns 10: 'Object' - // example 11: _phpCastString(0) - // returns 11: '0' - // example 12: _phpCastString(1) - // returns 12: '1' - // example 13: _phpCastString(3.14) - // returns 13: '3.14' - - const type = typeof value - - switch (type) { - case 'boolean': - return value ? '1' : '' - case 'string': - return value - case 'number': - if (isNaN(value)) { - return 'NAN' - } - - if (!isFinite(value)) { - return (value < 0 ? '-' : '') + 'INF' - } - - return value + '' - case 'undefined': - return '' - case 'object': - if (Array.isArray(value)) { - return 'Array' - } - - if (value !== null) { - return 'Object' - } - - return '' - case 'function': - // fall through - default: - throw new Error('Unsupported value type') - } -}
src/php/_helpers/_phpCastString.ts+41 −0 added@@ -0,0 +1,41 @@ +import type { PhpInput } from './_phpTypes.ts' + +type CastStringValue = PhpInput + +export function _phpCastString(value: CastStringValue): string { + // original by: Rafał Kukawski + + if (typeof value === 'boolean') { + return value ? '1' : '' + } + if (typeof value === 'string') { + return value + } + if (typeof value === 'number') { + if (isNaN(value)) { + return 'NAN' + } + + if (!isFinite(value)) { + return (value < 0 ? '-' : '') + 'INF' + } + + return value + '' + } + if (typeof value === 'undefined') { + return '' + } + if (typeof value === 'object') { + if (Array.isArray(value)) { + return 'Array' + } + + if (value !== null) { + return 'Object' + } + + return '' + } + + throw new Error('Unsupported value type') +}
src/php/_helpers/_phpRuntimeState.ts+283 −0 added@@ -0,0 +1,283 @@ +import { + isPhpAssocObject, + isPhpCallable, + type PhpAssoc, + type PhpCallable, + type PhpInput, + type PhpList, +} from './_phpTypes.ts' + +interface IniEntry { + local_value?: PhpInput +} + +type LocaleEntry = PhpAssoc<PhpInput> & { + sorting?: (left: PhpInput, right: PhpInput) => number +} +type LocaleCategoryMap = PhpAssoc<string | undefined> +interface LocutusRuntimeContainer { + php?: PhpAssoc<PhpInput> +} + +interface PhpRuntimeKnownEntryMap { + ini: PhpAssoc<IniEntry | undefined> + locales: PhpAssoc<LocaleEntry | undefined> + localeCategories: LocaleCategoryMap + pointers: PhpList<PhpInput> + locale_default: string + locale: string + uniqidSeed: number + timeoutStatus: boolean + last_error_json: number + strtokleftOver: string +} + +type PhpRuntimeNumberKey = { + [K in keyof PhpRuntimeKnownEntryMap]: PhpRuntimeKnownEntryMap[K] extends number ? K : never +}[keyof PhpRuntimeKnownEntryMap] +type PhpRuntimeBooleanKey = { + [K in keyof PhpRuntimeKnownEntryMap]: PhpRuntimeKnownEntryMap[K] extends boolean ? K : never +}[keyof PhpRuntimeKnownEntryMap] +type PhpRuntimeStringKey = { + [K in keyof PhpRuntimeKnownEntryMap]: PhpRuntimeKnownEntryMap[K] extends string ? K : never +}[keyof PhpRuntimeKnownEntryMap] + +interface PhpGlobalProcessLike { + env?: PhpAssoc<string | undefined> +} + +interface PhpGlobalBufferLike { + from?: (...args: PhpInput[]) => PhpInput +} + +interface PhpGlobalKnownEntryMap { + process: PhpGlobalProcessLike + Buffer: PhpGlobalBufferLike +} + +type GlobalWithLocutus = { + $locutus?: LocutusRuntimeContainer + [key: string]: PhpInput +} + +type PhpObjectEntryContainer = object | PhpCallable + +export interface PhpRuntimeState { + ini: PhpAssoc<IniEntry | undefined> + locales: PhpAssoc<LocaleEntry | undefined> + localeCategories: LocaleCategoryMap + pointers: PhpList<PhpInput> + locale_default: string | undefined +} + +const isIniBag = (value: PhpInput): value is PhpAssoc<IniEntry | undefined> => + isPhpAssocObject<IniEntry | undefined>(value) + +const isLocaleBag = (value: PhpInput): value is PhpAssoc<LocaleEntry | undefined> => + isPhpAssocObject<LocaleEntry | undefined>(value) + +const isLocaleCategoryBag = (value: PhpInput): value is LocaleCategoryMap => isPhpAssocObject<string | undefined>(value) + +const globalContext: GlobalWithLocutus = + typeof window === 'object' && window !== null ? window : typeof global === 'object' && global !== null ? global : {} + +const ensurePhpRuntimeObject = (): PhpAssoc<PhpInput> => { + let locutus = globalContext.$locutus + if (typeof locutus !== 'object' || locutus === null) { + locutus = {} + globalContext.$locutus = locutus + } + + let php = locutus.php + if (typeof php !== 'object' || php === null) { + php = {} + locutus.php = php + } + + return php +} + +export function ensurePhpRuntimeState(): PhpRuntimeState { + const php = ensurePhpRuntimeObject() + const iniValue = php.ini + const localesValue = php.locales + const localeCategoriesValue = php.localeCategories + const pointersValue = php.pointers + + const ini = isIniBag(iniValue) ? iniValue : {} + const locales = isLocaleBag(localesValue) ? localesValue : {} + const localeCategories = isLocaleCategoryBag(localeCategoriesValue) ? localeCategoriesValue : {} + const pointers: PhpList<PhpInput> = Array.isArray(pointersValue) ? pointersValue : [] + + if (iniValue !== ini) { + php.ini = ini + } + if (localesValue !== locales) { + php.locales = locales + } + if (localeCategoriesValue !== localeCategories) { + php.localeCategories = localeCategories + } + if (pointersValue !== pointers) { + php.pointers = pointers + } + + const localeDefaultValue = php.locale_default + const localeDefault = typeof localeDefaultValue === 'string' ? localeDefaultValue : undefined + + return { + ini, + locales, + localeCategories, + pointers, + locale_default: localeDefault, + } +} + +export function setPhpLocaleDefault(localeDefault: string): void { + const php = ensurePhpRuntimeObject() + php.locale_default = localeDefault +} + +export function getPhpRuntimeEntry<TKey extends keyof PhpRuntimeKnownEntryMap>( + key: TKey, +): PhpRuntimeKnownEntryMap[TKey] | undefined +export function getPhpRuntimeEntry(key: string): PhpInput | undefined +export function getPhpRuntimeEntry(key: string): PhpInput | undefined { + const php = ensurePhpRuntimeObject() + const value = php[key] + return typeof value === 'undefined' ? undefined : value +} + +export function setPhpRuntimeEntry<TKey extends keyof PhpRuntimeKnownEntryMap>( + key: TKey, + value: PhpRuntimeKnownEntryMap[TKey], +): void +export function setPhpRuntimeEntry(key: string, value: PhpInput): void +export function setPhpRuntimeEntry(key: string, value: PhpInput): void { + const php = ensurePhpRuntimeObject() + php[key] = value +} + +export function getPhpRuntimeNumber(key: PhpRuntimeNumberKey, fallback: number): number +export function getPhpRuntimeNumber(key: string, fallback: number): number +export function getPhpRuntimeNumber(key: string, fallback: number): number { + const value = getPhpRuntimeEntry(key) + return typeof value === 'number' ? value : fallback +} + +export function getPhpRuntimeBoolean(key: PhpRuntimeBooleanKey, fallback: boolean): boolean +export function getPhpRuntimeBoolean(key: string, fallback: boolean): boolean +export function getPhpRuntimeBoolean(key: string, fallback: boolean): boolean { + const value = getPhpRuntimeEntry(key) + return typeof value === 'boolean' ? value : fallback +} + +export function getPhpRuntimeString(key: PhpRuntimeStringKey, fallback: string): string +export function getPhpRuntimeString(key: string, fallback: string): string +export function getPhpRuntimeString(key: string, fallback: string): string { + const value = getPhpRuntimeEntry(key) + return typeof value === 'string' ? value : fallback +} + +export function getPhpGlobalEntry<TKey extends keyof PhpGlobalKnownEntryMap>( + key: TKey, +): PhpGlobalKnownEntryMap[TKey] | undefined +export function getPhpGlobalEntry(key: string): PhpInput | undefined +export function getPhpGlobalEntry(key: string): PhpInput | undefined { + const value = globalContext[key] + return typeof value === 'undefined' ? undefined : value +} + +export function setPhpGlobalEntry(key: string, value: PhpInput): void { + globalContext[key] = value +} + +export function getPhpGlobalScope(): PhpAssoc<PhpInput> { + return globalContext +} + +export function getPhpGlobalCallable<TArgs extends PhpInput[] = PhpInput[], TResult = PhpInput>( + key: string, +): PhpCallable<TArgs, TResult> | undefined { + const value = getPhpGlobalEntry(key) + return isPhpCallable<TArgs, TResult>(value) ? value : undefined +} + +export function getPhpObjectEntry(value: PhpInput, key: string): PhpInput | undefined { + if ((typeof value !== 'object' && typeof value !== 'function') || value === null) { + return undefined + } + + let current: object | null = value + while (current) { + const descriptor = Object.getOwnPropertyDescriptor(current, key) + if (descriptor) { + if (typeof descriptor.get === 'function') { + const getterValue = descriptor.get.call(value) + return typeof getterValue === 'undefined' ? undefined : getterValue + } + const directValue = descriptor.value + return typeof directValue === 'undefined' ? undefined : directValue + } + current = Object.getPrototypeOf(current) + } + + return undefined +} + +export function setPhpObjectEntry(value: PhpInput, key: string, entry: PhpInput): boolean { + if ((typeof value !== 'object' && typeof value !== 'function') || value === null) { + return false + } + + const container: PhpObjectEntryContainer = value + let current: object | null = container + while (current) { + const descriptor = Object.getOwnPropertyDescriptor(current, key) + if (descriptor) { + if (typeof descriptor.set === 'function') { + descriptor.set.call(container, entry) + return true + } + if (descriptor.writable) { + Object.defineProperty(container, key, { + configurable: descriptor.configurable ?? true, + enumerable: descriptor.enumerable ?? true, + value: entry, + writable: true, + }) + return true + } + return false + } + current = Object.getPrototypeOf(current) + } + + Object.defineProperty(container, key, { + configurable: true, + enumerable: true, + value: entry, + writable: true, + }) + return true +} + +function getPhpLocaleEntry(category: string): LocaleEntry | undefined { + const runtime = ensurePhpRuntimeState() + const localeName = runtime.localeCategories[category] + if (typeof localeName !== 'string') { + return undefined + } + const localeEntry = runtime.locales[localeName] + return isPhpAssocObject(localeEntry) ? localeEntry : undefined +} + +export function getPhpLocaleGroup(category: string, groupKey: string): PhpAssoc<PhpInput> | undefined { + const localeEntry = getPhpLocaleEntry(category) + if (!localeEntry) { + return undefined + } + const group = localeEntry[groupKey] + return isPhpAssocObject(group) ? group : undefined +}
src/php/_helpers/_phpTypes.ts+179 −0 added@@ -0,0 +1,179 @@ +export type PhpNullish = null | undefined +export type PhpInput = {} | PhpNullish + +export type PhpScalar = string | number | boolean +export type PhpLiteral = PhpScalar | null +export type PhpPrimitive = PhpScalar | bigint +export type PhpKey = string | number +export type NumericLike = number | bigint | string +export type StringLike = string | number | boolean | bigint +export type PhpStringish = StringLike | PhpNullish + +export type PhpList<T = PhpInput> = T[] +export type PhpAssoc<T = PhpInput> = { [key: string]: T } +export type PhpContainer<T = PhpInput> = PhpList<T> | PhpAssoc<T> +export type PhpArrayLike<T = PhpInput> = PhpList<T> | PhpAssoc<T> +export interface PhpRecursiveAssoc { + [key: string]: PhpRecursiveValue +} +export interface PhpRecursiveList extends Array<PhpRecursiveValue> {} +export type PhpRecursiveValue = PhpPrimitive | PhpNullish | PhpRecursiveList | PhpRecursiveAssoc +export type PhpFunctionValue = (...args: PhpInput[]) => PhpInput +export interface PhpRuntimeAssoc { + [key: string]: PhpRuntimeValue +} +export interface PhpRuntimeList extends Array<PhpRuntimeValue> {} +export type PhpRuntimeValue = PhpPrimitive | PhpNullish | PhpRuntimeList | PhpRuntimeAssoc | PhpFunctionValue +export type PhpReadonlyList<T = PhpInput> = readonly T[] +export type PhpReadonlyAssoc<T = PhpInput> = Readonly<PhpAssoc<T>> +export type PhpReadonlyArrayLike<T = PhpInput> = PhpReadonlyList<T> | PhpReadonlyAssoc<T> + +export const entriesOfPhpAssoc = <T>(value: PhpAssoc<T>): Array<[string, T]> => { + return Object.entries(value) +} + +export type PhpCallableArgs = PhpInput[] +export type PhpCallable<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = PhpInput> = ( + ...args: TArgs +) => TResult +export type PhpCallableScope = PhpInput +export type PhpCallableTuple<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = PhpInput> = readonly [ + PhpCallableScope, + string | PhpCallable<TArgs, TResult>, +] +export type PhpCallableDescriptor<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = PhpInput> = + | string + | PhpCallable<TArgs, TResult> + | PhpCallableTuple<TArgs, TResult> +export type PhpComparatorDescriptor<T> = PhpCallableDescriptor<[T, T], NumericLike> +export type PhpKeyComparatorDescriptor = PhpCallableDescriptor<[string, string], NumericLike> + +export function isPhpNullish(value: PhpInput): value is PhpNullish { + return typeof value === 'undefined' || value === null +} + +export function isPhpList<T = PhpInput>(value: PhpInput): value is PhpList<T> { + return Array.isArray(value) +} + +export function isObjectLike(value: PhpInput): value is PhpArrayLike<PhpInput> { + return typeof value === 'object' && value !== null +} + +export function isPhpAssocObject<T = PhpInput>(value: PhpInput): value is PhpAssoc<T> { + return isObjectLike(value) && !isPhpList(value) +} + +export function isPhpScalar(value: PhpInput): value is PhpScalar { + return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' +} + +export function isPhpKey(value: PhpInput): value is PhpKey { + return typeof value === 'string' || typeof value === 'number' +} + +export function isNumericLike(value: PhpInput): value is NumericLike { + if (typeof value === 'number') { + return Number.isFinite(value) + } + + if (typeof value === 'bigint') { + return true + } + + if (typeof value !== 'string') { + return false + } + + const trimmed = value.trim() + if (trimmed === '') { + return false + } + + return Number.isFinite(Number(trimmed)) +} + +export function isPhpCallable<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = PhpInput>( + value: PhpInput, +): value is PhpCallable<TArgs, TResult> { + return typeof value === 'function' +} + +export function isPhpCallableDescriptor<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = PhpInput>( + value: PhpInput, +): value is PhpCallableDescriptor<TArgs, TResult> { + if (typeof value === 'string') { + return true + } + + if (isPhpCallable<TArgs, TResult>(value)) { + return true + } + + if (!Array.isArray(value) || value.length < 2) { + return false + } + + const callableDescriptor = value[1] + return typeof callableDescriptor === 'string' || isPhpCallable<TArgs, TResult>(callableDescriptor) +} + +export function assertIsObjectLike( + value: PhpInput, + message = 'Expected object-like value', +): asserts value is PhpArrayLike<PhpInput> { + if (!isObjectLike(value)) { + throw new TypeError(message) + } +} + +export function assertIsPhpAssocObject( + value: PhpInput, + message = 'Expected associative object value', +): asserts value is PhpAssoc<PhpInput> { + if (!isPhpAssocObject(value)) { + throw new TypeError(message) + } +} + +export function assertIsPhpList(value: PhpInput, message = 'Expected list value'): asserts value is PhpList<PhpInput> { + if (!isPhpList(value)) { + throw new TypeError(message) + } +} + +export function assertIsPhpKey(value: PhpInput, message = 'Expected key value'): asserts value is PhpKey { + if (!isPhpKey(value)) { + throw new TypeError(message) + } +} + +export function assertIsNumericLike( + value: PhpInput, + message = 'Expected numeric-like value', +): asserts value is NumericLike { + if (!isNumericLike(value)) { + throw new TypeError(message) + } +} + +export function assertIsPhpCallable<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = PhpInput>( + value: PhpInput, + message = 'Expected callable value', +): asserts value is PhpCallable<TArgs, TResult> { + if (!isPhpCallable<TArgs, TResult>(value)) { + throw new TypeError(message) + } +} + +export function isPhpArrayObject<T = PhpInput>(value: PhpInput): value is PhpAssoc<T> { + return isObjectLike(value) +} + +export function toPhpArrayObject<T = PhpInput>(value: PhpInput): PhpAssoc<T> { + if (isPhpArrayObject<T>(value)) { + return value + } + + return {} +}
src/_util/headerFormatter.ts+16 −11 modified@@ -9,11 +9,7 @@ import path from 'node:path' import globby from 'globby' import { isValidHeaderKey } from './headerSchema.ts' -interface ParsedLine { - isHeader: boolean - key?: string - value?: string -} +type ParsedLine = { isHeader: false } | { isHeader: true; key: string; value: string } interface FormatResult { formatted: string @@ -50,6 +46,9 @@ export function parseHeaderLine(line: string): ParsedLine { } const [, key, value] = match + if (typeof key === 'undefined' || typeof value === 'undefined') { + return { isHeader: false } + } const keyLower = key.toLowerCase().trim() if (!isValidHeaderKey(keyLower)) { @@ -78,23 +77,29 @@ export function formatHeaders(content: string): FormatResult { const lines = content.split('\n') const parsedLines = lines.map((line) => parseHeaderLine(line)) - const headerKeys = parsedLines.filter((p) => p.isHeader && p.key).map((p) => p.key as string) + const headerKeys = parsedLines + .filter((p): p is Extract<ParsedLine, { isHeader: true }> => p.isHeader) + .map((p) => p.key) const longestKeyLength = headerKeys.length > 0 ? Math.max(...headerKeys.map((k) => k.length)) : 0 let changed = false const formattedLines: string[] = [] for (let i = 0; i < lines.length; i++) { + const line = lines[i] + if (typeof line === 'undefined') { + continue + } const parsed = parsedLines[i] - if (parsed.isHeader && parsed.key && parsed.value !== undefined) { + if (parsed && parsed.isHeader) { const formatted = formatHeaderLine(parsed.key, parsed.value, longestKeyLength) - if (formatted !== lines[i]) { + if (formatted !== line) { changed = true } formattedLines.push(formatted) } else { - formattedLines.push(lines[i]) + formattedLines.push(line) } } @@ -134,7 +139,7 @@ export function formatFile(filepath: string): FormatFileResult { * Check all function files for proper header formatting */ export function checkAll(srcDir: string): CheckAllResult { - const pattern = [path.join(srcDir, '**/*.js'), '!**/index.js', '!**/_util/**'] + const pattern = [path.join(srcDir, '**/*.{js,ts}'), '!**/index.{js,ts}', '!**/_util/**', '!**/*.vitest.ts'] const files = globby.sync(pattern) const badFiles: string[] = [] @@ -155,7 +160,7 @@ export function checkAll(srcDir: string): CheckAllResult { * Format all function files */ export function formatAll(srcDir: string): FormatAllResult { - const pattern = [path.join(srcDir, '**/*.js'), '!**/index.js', '!**/_util/**'] + const pattern = [path.join(srcDir, '**/*.{js,ts}'), '!**/index.{js,ts}', '!**/_util/**', '!**/*.vitest.ts'] const files = globby.sync(pattern) const formattedFiles: string[] = []
src/_util/util.ts+1980 −314 modified@@ -1,12 +1,13 @@ +import { spawnSync } from 'node:child_process' import fs from 'node:fs' import path from 'node:path' import Debug from 'debug' -import esprima from 'esprima' import globby from 'globby' import indentStringModule from 'indent-string' import YAML from 'js-yaml' import _ from 'lodash' import pMap from 'p-map' +import ts from 'typescript' import { isValidHeaderKey, validateHeaderKeys } from './headerSchema.ts' const debug = Debug('locutus:utils') @@ -40,13 +41,69 @@ interface ParsedParams { codeDependencies?: string[] // Extracted require() paths } +interface StandaloneDependencyOptions { + includeTypeOnlyImports?: boolean +} + +interface WebsiteJsOptions { + preserveBlankLines?: boolean +} + +interface WebsiteJsFormatOptions { + restorePreservedBlankLines?: boolean +} + +type StandaloneMode = 'js' | 'ts' + +interface StandaloneImportSpec { + depKey: string + localName: string + importedName: string + isTypeOnly: boolean +} + +interface StandaloneStatementInfo { + declaredNames: Set<string> + runtimeTopLevelDeps: Set<string> + allTopLevelDeps: Set<string> + runtimeImportLocals: Set<string> + typeImportLocals: Set<string> +} + +interface StandaloneModuleInfo { + moduleKey: string + modulePath: string + params: ParsedParams + sourceFile: ts.SourceFile + hasExternalImports: boolean + statements: ts.Statement[] + statementInfo: StandaloneStatementInfo[] + declarationsByName: Map<string, Set<number>> + exportToLocalName: Map<string, string> + importSpecsByLocalName: Map<string, StandaloneImportSpec[]> +} + +interface StandaloneModuleSelection { + includedStatementIndexes: Set<number> + depRuntimeNames: Map<string, Set<string>> + depTypeNames: Map<string, Set<string>> + runtimeAliases: Set<string> + wrapperAliases: Set<string> + wrapperRenameByStatementIndex: Map<number, string> + typeAliases: Set<string> +} + +type UnknownMap = { [key: string]: unknown } + type Callback<T = void> = (err: Error | null, result?: T) => void +const WEBSITE_BLANK_LINE_MARKER_COMMENT = '/*__LOCUTUS_PRESERVE_BLANK_LINE__*/' + class Util { __src: string __root: string __test: string - globals: Record<string, unknown> + globals: UnknownMap pattern: string[] concurrency: number authorKeys: string[] @@ -65,7 +122,14 @@ class Util { this.globals = {} - this.pattern = [this.__src + '/**/**/*.js', '!**/index.js', '!**/_util/**', '!**/*.mocha.js'] + this.pattern = [ + this.__src + '/**/**/*.{js,ts}', + '!**/index.js', + '!**/index.ts', + '!**/_util/**', + '!**/*.mocha.js', + '!**/*.vitest.ts', + ] this.concurrency = 8 this.authorKeys = [ 'original by', @@ -80,18 +144,18 @@ class Util { this.langDefaults = { c: { order: 1, - function_title_template: "[language]'s [category].[function] in JavaScript", + function_title_template: "[language]'s [category].[function] in TypeScript", human: 'C', packageType: 'header file', inspiration_urls: [ '<a href="https://en.cppreference.com/w/c/numeric/math">the C math.h documentation</a>', '<a href="https://sourceware.org/git/?p=glibc.git;a=tree;f=math;hb=HEAD">the C math.h source</a>', ], - function_description_template: `Here's what our current JavaScript equivalent to <a href="https://en.cppreference.com/w/c/numeric/[category]/[function]">[language]'s [function] found in the [category].h header file</a> looks like.`, + function_description_template: `Here's what our current TypeScript equivalent to <a href="https://en.cppreference.com/w/c/numeric/[category]/[function]">[language]'s [function] found in the [category].h header file</a> looks like.`, }, golang: { order: 2, - function_title_template: "[language]'s [category].[function] in JavaScript", + function_title_template: "[language]'s [category].[function] in TypeScript", human: 'Go', packageType: 'package', inspiration_urls: [ @@ -100,37 +164,37 @@ class Util { '<a href="https://golang.org/src/strings/example_test.go">Go strings examples source</a>', '<a href="https://gophersjs.com">GopherJS</a>', ], - function_description_template: `Here's what our current JavaScript equivalent to <a href="https://golang.org/pkg/[category]/#[function]">[language]'s [category].[function]</a> looks like.`, + function_description_template: `Here's what our current TypeScript equivalent to <a href="https://golang.org/pkg/[category]/#[function]">[language]'s [category].[function]</a> looks like.`, }, python: { order: 3, - function_title_template: "[language]'s [category].[function] in JavaScript", + function_title_template: "[language]'s [category].[function] in TypeScript", human: 'Python', packageType: 'module', inspiration_urls: [ '<a href="https://docs.python.org/3/library/string.html">the Python 3 standard library string page</a>', ], - function_description_template: `Here's what our current JavaScript equivalent to <a href="https://docs.python.org/3/library/[category].html#[category].[function]">[language]'s [category].[function]</a> looks like.`, + function_description_template: `Here's what our current TypeScript equivalent to <a href="https://docs.python.org/3/library/[category].html#[category].[function]">[language]'s [category].[function]</a> looks like.`, }, ruby: { order: 4, - function_title_template: "[language]'s [category].[function] in JavaScript", + function_title_template: "[language]'s [category].[function] in TypeScript", human: 'Ruby', packageType: 'module', inspiration_urls: ['<a href="https://ruby-doc.org/core-2.2.2/Math.html">the Ruby core documentation</a>'], - function_description_template: `Here's what our current JavaScript equivalent to <a href="https://ruby-doc.org/core-2.2.2/[category].html#method-c-[function]">[language]'s [category].[function]</a> looks like.`, + function_description_template: `Here's what our current TypeScript equivalent to <a href="https://ruby-doc.org/core-2.2.2/[category].html#method-c-[function]">[language]'s [category].[function]</a> looks like.`, }, php: { order: 5, - function_title_template: "[language]'s [function] in JavaScript", + function_title_template: "[language]'s [function] in TypeScript", human: 'PHP', packageType: 'extension', inspiration_urls: [ '<a href="https://php.net/manual/en/book.strings.php">the PHP string documentation</a>', '<a href="https://github.com/php/php-src/blob/master/ext/standard/string.c#L5338">the PHP string source</a>', '<a href="https://github.com/php/php-src/blob/master/ext/standard/tests/strings/str_pad_variation1.phpt">a PHP str_pad test</a>', ], - function_description_template: `Here's what our current JavaScript equivalent to <a href="https://php.net/manual/en/function.[functiondashed].php">[language]'s [function]</a> looks like.`, + function_description_template: `Here's what our current TypeScript equivalent to <a href="https://php.net/manual/en/function.[functiondashed].php">[language]'s [function]</a> looks like.`, alias: [ '/categories/', '/categories/array/', @@ -162,61 +226,61 @@ class Util { }, perl: { order: 6, - function_title_template: "[language]'s [category]::[function] in JavaScript", + function_title_template: "[language]'s [category]::[function] in TypeScript", human: 'Perl', packageType: 'module', inspiration_urls: ['<a href="https://perldoc.perl.org/">the Perl documentation</a>'], - function_description_template: `Here's what our current JavaScript equivalent to <a href="https://perldoc.perl.org/[category]">[language]'s [category]::[function]</a> looks like.`, + function_description_template: `Here's what our current TypeScript equivalent to <a href="https://perldoc.perl.org/[category]">[language]'s [category]::[function]</a> looks like.`, }, lua: { order: 7, - function_title_template: "[language]'s [category].[function] in JavaScript", + function_title_template: "[language]'s [category].[function] in TypeScript", human: 'Lua', packageType: 'library', inspiration_urls: ['<a href="https://www.lua.org/manual/5.4/">the Lua 5.4 manual</a>'], - function_description_template: `Here's what our current JavaScript equivalent to <a href="https://www.lua.org/manual/5.4/manual.html#pdf-[category].[function]">[language]'s [category].[function]</a> looks like.`, + function_description_template: `Here's what our current TypeScript equivalent to <a href="https://www.lua.org/manual/5.4/manual.html#pdf-[category].[function]">[language]'s [category].[function]</a> looks like.`, }, r: { order: 8, - function_title_template: "[language]'s [function] in JavaScript", + function_title_template: "[language]'s [function] in TypeScript", human: 'R', packageType: 'function', inspiration_urls: [ '<a href="https://stat.ethz.ch/R-manual/R-devel/library/base/html/00Index.html">the R base documentation</a>', ], - function_description_template: `Here's what our current JavaScript equivalent to <a href="https://stat.ethz.ch/R-manual/R-devel/library/[category]/html/[function].html">[language]'s [function]</a> looks like.`, + function_description_template: `Here's what our current TypeScript equivalent to <a href="https://stat.ethz.ch/R-manual/R-devel/library/[category]/html/[function].html">[language]'s [function]</a> looks like.`, }, julia: { order: 9, - function_title_template: "[language]'s [function] in JavaScript", + function_title_template: "[language]'s [function] in TypeScript", human: 'Julia', packageType: 'function', inspiration_urls: ['<a href="https://docs.julialang.org/en/v1/">the Julia documentation</a>'], - function_description_template: `Here's what our current JavaScript equivalent to <a href="https://docs.julialang.org/en/v1/base/math/#Base.[function]">[language]'s [function]</a> looks like.`, + function_description_template: `Here's what our current TypeScript equivalent to <a href="https://docs.julialang.org/en/v1/base/math/#Base.[function]">[language]'s [function]</a> looks like.`, }, elixir: { order: 10, - function_title_template: "[language]'s [category].[function] in JavaScript", + function_title_template: "[language]'s [category].[function] in TypeScript", human: 'Elixir', packageType: 'module', inspiration_urls: ['<a href="https://hexdocs.pm/elixir/">the Elixir documentation</a>'], - function_description_template: `Here's what our current JavaScript equivalent to <a href="https://hexdocs.pm/elixir/[category].html#[function]/1">[language]'s [category].[function]</a> looks like.`, + function_description_template: `Here's what our current TypeScript equivalent to <a href="https://hexdocs.pm/elixir/[category].html#[function]/1">[language]'s [category].[function]</a> looks like.`, }, clojure: { order: 11, - function_title_template: "[language]'s [category]/[function] in JavaScript", + function_title_template: "[language]'s [category]/[function] in TypeScript", human: 'Clojure', packageType: 'function', inspiration_urls: ['<a href="https://clojuredocs.org/">ClojureDocs</a>'], - function_description_template: `Here's what our current JavaScript equivalent to <a href="https://clojuredocs.org/clojure.core/[function]">[language]'s [category]/[function]</a> looks like.`, + function_description_template: `Here's what our current TypeScript equivalent to <a href="https://clojuredocs.org/clojure.core/[function]">[language]'s [category]/[function]</a> looks like.`, }, awk: { order: 12, - function_title_template: "[language]'s [function] in JavaScript", + function_title_template: "[language]'s [function] in TypeScript", human: 'AWK', packageType: 'function', inspiration_urls: ['<a href="https://www.gnu.org/software/gawk/manual/gawk.html">the GNU AWK manual</a>'], - function_description_template: `Here's what our current JavaScript equivalent to <a href="https://www.gnu.org/software/gawk/manual/gawk.html#String-Functions">[language]'s [function]</a> looks like.`, + function_description_template: `Here's what our current TypeScript equivalent to <a href="https://www.gnu.org/software/gawk/manual/gawk.html#String-Functions">[language]'s [function]</a> looks like.`, }, } @@ -243,8 +307,12 @@ class Util { // Write all buffered index files let filesWritten = 0 for (const indexHtml in this._injectwebBuffer) { + const html = this._injectwebBuffer[indexHtml] + if (typeof html === 'undefined') { + continue + } debug('writing: ' + indexHtml) - fs.writeFileSync(indexHtml, this._injectwebBuffer[indexHtml], 'utf-8') + fs.writeFileSync(indexHtml, html, 'utf-8') filesWritten++ } @@ -286,6 +354,9 @@ class Util { for (const indexJs in this._reindexBuffer) { const requires = this._reindexBuffer[indexJs] + if (!requires) { + continue + } requires.sort() debug('writing: ' + indexJs) fs.writeFileSync(indexJs, requires.join('\n') + '\n', 'utf-8') @@ -330,20 +401,53 @@ class Util { _reindexOne(params: ParsedParams): Promise<void> { const fullpath = this.__src + '/' + params.filepath const dir = path.dirname(fullpath) - const basefile = path.basename(fullpath, '.js') - const indexJs = dir + '/index.js' + const ext = path.extname(fullpath) + const basefile = path.basename(fullpath, ext) + const indexTs = dir + '/index.ts' - let module = basefile - if (basefile === 'Index2') { - module = 'Index' + if (!this._reindexBuffer[indexTs]) { + this._reindexBuffer[indexTs] = [] } - if (!this._reindexBuffer[indexJs]) { - this._reindexBuffer[indexJs] = [] + const scriptKind = params.filepath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS + const sourceFile = ts.createSourceFile(params.filepath, params.code, ts.ScriptTarget.ES2022, true, scriptKind) + const exportNames = new Set<string>() + + ts.forEachChild(sourceFile, (node) => { + if (ts.isFunctionDeclaration(node) && node.name) { + const modifiers = ts.getModifiers(node) + const hasExport = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) + if (hasExport) { + exportNames.add(node.name.text) + } + return + } + + if (ts.isVariableStatement(node)) { + const modifiers = ts.getModifiers(node) + const hasExport = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) + if (!hasExport) { + return + } + + for (const declaration of node.declarationList.declarations) { + if (ts.isIdentifier(declaration.name)) { + exportNames.add(declaration.name.text) + } + } + } + }) + + if (exportNames.size === 0) { + exportNames.add(params.func_name) } - const line = 'module.exports.' + module + " = require('./" + basefile + "')" - this._reindexBuffer[indexJs].push(line) + for (const exportName of exportNames) { + const line = `export { ${exportName} } from './${basefile}.ts'` + if (!this._reindexBuffer[indexTs]?.includes(line)) { + this._reindexBuffer[indexTs]?.push(line) + } + } return Promise.resolve() } @@ -361,14 +465,20 @@ class Util { const catPath = langPath + '/' + params.category const catIndexPath = catPath + '/' + 'index.html' const funcPath = catPath + '/' + params.func_name + '.html' + const langDefaults = this.langDefaults[params.language] + if (!langDefaults) { + throw new Error(`Unknown language defaults for: ${params.language}`) + } + + const targetLangLabel = params.filepath.endsWith('.ts') ? 'TypeScript' : 'JavaScript' if (!this._injectwebBuffer[langIndexPath]) { let langTitle = '' - langTitle += this.langDefaults[params.language].human + ' ' - langTitle += this.langDefaults[params.language].packageType + 's ' - langTitle += ' in JavaScript' + langTitle += langDefaults.human + ' ' + langTitle += langDefaults.packageType + 's ' + langTitle += ` in ${targetLangLabel}` - const langData = Object.assign({}, this.langDefaults[params.language], { + const langData = Object.assign({}, langDefaults, { warning: 'This file is auto generated by `yarn web:inject`, do not edit by hand', type: 'language', layout: 'language', @@ -380,10 +490,10 @@ class Util { if (!this._injectwebBuffer[catIndexPath]) { let catTitle = '' - catTitle += this.langDefaults[params.language].human + "'s " + catTitle += langDefaults.human + "'s " catTitle += params.category + ' ' - catTitle += this.langDefaults[params.language].packageType + ' ' - catTitle += ' in JavaScript' + catTitle += langDefaults.packageType + ' ' + catTitle += ` in ${targetLangLabel}` const catData = { warning: 'This file is auto generated by `yarn web:inject`, do not edit by hand', @@ -396,29 +506,32 @@ class Util { this._injectwebBuffer[catIndexPath] = '---' + '\n' + YAML.dump(catData).trim() + '\n' + '---' + '\n' } - const functionTitle = this.langDefaults[params.language].function_title_template - .replace(/\[language]/g, this.langDefaults[params.language].human) + const functionTitle = langDefaults.function_title_template + .replace(/\[language]/g, langDefaults.human) .replace(/\[category]/g, params.category) .replace(/\[function]/g, params.func_name) .replace(/\[functiondashed]/g, params.func_name.replace(/_/g, '-')) - const functionDescription = this.langDefaults[params.language].function_description_template - .replace(/\[language]/g, this.langDefaults[params.language].human) + const functionDescription = langDefaults.function_description_template + .replace(/\[language]/g, langDefaults.human) .replace(/\[category]/g, params.category) .replace(/\[function]/g, params.func_name) .replace(/\[functiondashed]/g, params.func_name.replace(/_/g, '-')) // Extract parity verified values (e.g., "PHP 8.3", "Python 3.12") const parityVerified = (params.headKeys['parity verified'] || []).map((lines) => lines.join('\n')) - const funcData: Record<string, unknown> = { + const isTS = params.filepath.endsWith('.ts') + + const funcData: UnknownMap = { warning: 'This file is auto generated by `yarn web:inject`, do not edit by hand', examples: (params.headKeys.example || []).map((lines) => lines.join('\n')), returns: (params.headKeys.returns || []).map((lines) => lines.join('\n')), dependencies: params.codeDependencies || [], authors: authors || {}, notes: (params.headKeys.note || []).map((lines) => lines.join('\n')), parityVerified: parityVerified.length > 0 ? parityVerified : null, + isTypescript: isTS, type: 'function', layout: 'function', title: functionTitle, @@ -439,339 +552,1894 @@ class Util { } let buf = '---' + '\n' + YAML.dump(funcData).trim() + '\n' + '---' + '\n' - buf += `{% codeblock lang:javascript %}${params.code}{% endcodeblock %}` + + if (isTS) { + const hasStandaloneDependencies = (params.codeDependencies || []).length > 0 + const jsCode = this._formatWebsiteJavascript( + this._toWebsiteJs(params.code, { preserveBlankLines: true }), + params.filepath.replace(/\.ts$/, '.js'), + { restorePreservedBlankLines: true }, + ) + const standaloneCode = hasStandaloneDependencies ? await this._buildStandaloneJs(params) : '' + const standaloneTsCode = hasStandaloneDependencies ? await this._buildStandaloneTs(params) : null + + buf += `<div class="code-tab-panel" data-lang="ts">\n{% codeblock lang:typescript %}${params.code}{% endcodeblock %}\n</div>\n` + buf += `<div class="code-tab-panel" data-lang="js" style="display:none">\n{% codeblock lang:javascript %}${jsCode}{% endcodeblock %}\n</div>\n` + if (hasStandaloneDependencies && standaloneTsCode) { + buf += `<div class="code-tab-panel" data-lang="standalone-ts" style="display:none">\n{% codeblock lang:typescript %}${standaloneTsCode}{% endcodeblock %}\n</div>\n` + } + if (hasStandaloneDependencies && standaloneCode.trim().length > 0) { + buf += `<div class="code-tab-panel" data-lang="standalone-js" style="display:none">\n{% codeblock lang:javascript %}${standaloneCode}{% endcodeblock %}\n</div>` + } + } else { + buf += `{% codeblock lang:javascript %}${params.code}{% endcodeblock %}` + } await fs.promises.mkdir(path.dirname(funcPath), { recursive: true }) await fs.promises.writeFile(funcPath, buf, 'utf-8') } _addRequire(name: string, relativeSrcForTest: string): string { - return ['const ', name, " = require('", relativeSrcForTest, "')"].join('') + const isTs = relativeSrcForTest.endsWith('.ts') + const suffix = isTs ? '.' + name : '' + return ['const ', name, " = require('", relativeSrcForTest, "')", suffix].join('') } - async _writetestOne(params: ParsedParams): Promise<void> { - if (!params.func_name) { - throw new Error('No func_name in ' + JSON.stringify(params)) - } - if (!params.headKeys) { - throw new Error('No headKeys in ' + params.func_name) + _addRequireExport(localName: string, exportName: string, relativeSrcForTest: string): string { + const isTs = relativeSrcForTest.endsWith('.ts') + if (isTs) { + return ['const ', localName, " = require('", relativeSrcForTest, "').", exportName].join('') } - if (!params.headKeys.example) { - throw new Error('No example in ' + params.func_name) + return ['const ', localName, " = require('", relativeSrcForTest, "')"].join('') + } + + _resolveSourceExt(subpath: string): string { + if (fs.existsSync(path.join(this.__src, subpath + '.ts'))) { + return subpath + '.ts' } + return subpath + '.js' + } - const basename = path.basename(params.filepath, '.js') + '.vitest.ts' - const subdir = path.dirname(params.filepath) - const testpath = this.__test + '/generated/' + subdir + '/' + basename - const testdir = path.dirname(testpath) - const relativeSrcForTestDir = path.relative(testdir, this.__src) - const relativeTestFileForRoot = path.relative(this.__root, testpath) + _markWebsiteBlankLines(code: string): string { + const lines = code.split('\n') + let inBlockComment = false + + const updateBlockCommentState = (line: string): void => { + let cursor = 0 + while (cursor < line.length) { + if (!inBlockComment) { + const lineCommentIndex = line.indexOf('//', cursor) + const blockStartIndex = line.indexOf('/*', cursor) + if (blockStartIndex === -1) { + return + } + if (lineCommentIndex !== -1 && lineCommentIndex < blockStartIndex) { + return + } + const blockEndIndex = line.indexOf('*/', blockStartIndex + 2) + if (blockEndIndex === -1) { + inBlockComment = true + return + } + cursor = blockEndIndex + 2 + continue + } - let testProps = '' - if (params.headKeys.test) { - testProps = params.headKeys.test[0][0] + const blockEndIndex = line.indexOf('*/', cursor) + if (blockEndIndex === -1) { + return + } + inBlockComment = false + cursor = blockEndIndex + 2 + } } - let describeSkip = '' - if (this.allowSkip && testProps.indexOf('skip-all') !== -1) { - describeSkip = '.skip' - } + return lines + .map((line) => { + if (line.trim().length === 0 && !inBlockComment) { + return WEBSITE_BLANK_LINE_MARKER_COMMENT + } + updateBlockCommentState(line) + return line + }) + .join('\n') + } - const codez: string[] = [] + _restoreWebsiteBlankLines(code: string): string { + return code.replace(/^\s*\/\*__LOCUTUS_PRESERVE_BLANK_LINE__\*\/;?\s*$/gm, '') + } - codez.push('// warning: This file is auto generated by `yarn build:tests`') - codez.push('// Do not edit by hand!') - codez.push('') - codez.push("import { describe, it, expect } from 'vitest'") - codez.push('') + _toWebsiteJs(code: string, options: WebsiteJsOptions = {}): string { + // website/source snippets are generated snapshots; refresh with `yarn injectweb` after signature changes. + const sourceCode = options.preserveBlankLines ? this._markWebsiteBlankLines(code) : code + const jsResult = ts.transpileModule(sourceCode, { + compilerOptions: { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ESNext, + removeComments: false, + newLine: ts.NewLineKind.LineFeed, + }, + }) - for (const global in this.globals) { - codez.push('const ' + global + ' = ' + this.globals[global]) - } + // Halve indentation (tsc emits 4 spaces, we use 2) and trim trailing whitespace + return jsResult.outputText + .split('\n') + .map((line) => { + const match = line.match(/^( +)/) + if (!match) { + return line + } + const prefix = match[1] + if (!prefix) { + return line + } + const spaces = prefix.length + return ' '.repeat(Math.floor(spaces / 2)) + line.slice(spaces) + }) + .join('\n') + .trimEnd() + } - codez.push("process.env.TZ = 'UTC'") + _resolveBiomeBinPath(): string | null { + const biomeBinName = process.platform === 'win32' ? 'biome.cmd' : 'biome' + const localBiomeBin = path.join(this.__root, 'node_modules', '.bin', biomeBinName) + if (fs.existsSync(localBiomeBin)) { + return localBiomeBin + } + return null + } - if (params.language === 'php') { - // Add PHP helpers, but skip if we're testing that helper itself - if (params.func_name !== 'ini_set') { - codez.push(this._addRequire('ini_set', relativeSrcForTestDir + '/' + 'php/info/ini_set')) - } - if (params.func_name !== 'ini_get') { - codez.push(this._addRequire('ini_get', relativeSrcForTestDir + '/' + 'php/info/ini_get')) - } - if (params.func_name === 'localeconv') { - codez.push(this._addRequire('setlocale', relativeSrcForTestDir + '/' + 'php/strings/setlocale')) - } - if (params.func_name === 'i18n_loc_get_default') { - codez.push( - this._addRequire('i18n_loc_set_default', relativeSrcForTestDir + '/' + 'php/i18n/i18n_loc_set_default'), - ) - } + _formatWebsiteJavascript(code: string, sourcePath: string, options: WebsiteJsFormatOptions = {}): string { + const finalize = (text: string): string => { + const formatted = options.restorePreservedBlankLines ? this._restoreWebsiteBlankLines(text) : text + return formatted.trimEnd() } - codez.push(this._addRequire(params.func_name, relativeSrcForTestDir + '/' + params.filepath)) - codez.push('') + const input = code.trimEnd() + if (!input) { + return '' + } - codez.push( - [ - 'describe', - describeSkip, - "('src/", - params.filepath, - ' (tested in ', - relativeTestFileForRoot, - ")', function () {", - ].join(''), - ) + const biomeBin = this._resolveBiomeBinPath() + if (!biomeBin) { + return finalize(input) + } - for (const i in params.headKeys.example) { - if (!params.headKeys.returns[i] || !params.headKeys.returns[i].length) { - throw new Error(`There is no return for example ${i} in ${params.filepath}`) - } + const result = spawnSync(biomeBin, ['format', '--stdin-file-path', sourcePath], { + encoding: 'utf8', + input, + maxBuffer: 16 * 1024 * 1024, + }) - const humanIndex = parseInt(i, 10) + 1 - let itSkip = '' - if (this.allowSkip && testProps.indexOf('skip-' + humanIndex) !== -1) { - itSkip = '.skip' + if (result.error || result.status !== 0 || typeof result.stdout !== 'string') { + const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '' + if (stderr) { + debug(`biome format failed for ${sourcePath}: ${stderr}`) } + return finalize(input) + } - codez.push([' it', itSkip, "('should pass example ", humanIndex, "', function () {"].join('')) + const output = result.stdout.trimEnd() + return finalize(output || input) + } - const body: string[] = [] - const testExpected = params.headKeys.returns[i].join('\n') + _toCommonJsRuntimeCode(code: string, options: { sourcePath: string }): string { + return ts + .transpileModule(code, { + compilerOptions: { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.CommonJS, + esModuleInterop: true, + allowSyntheticDefaultImports: true, + removeComments: false, + newLine: ts.NewLineKind.LineFeed, + }, + fileName: options.sourcePath, + }) + .outputText.trimEnd() + } - body.push('const expected = ' + testExpected) + _extractRelativeImportAliases(sourceFile: ts.SourceFile): string[] { + const aliases = new Set<string>() - for (const j in params.headKeys.example[i]) { - if (parseInt(j, 10) === params.headKeys.example[i].length - 1) { - body.push('const result = ' + params.headKeys.example[i][j].replace('var $result = ', '')) - } else { - body.push(params.headKeys.example[i][j]) - } + const addAlias = (localName: string, importedName: string): void => { + if (localName === importedName) { + return } - - body.push('expect(result).toEqual(expected)') - - codez.push(indentString(body.join('\n'), ' ', 4)) - codez.push(' })') + aliases.add(`const ${localName} = ${importedName};`) } - codez.push('})') - codez.push('') + ts.forEachChild(sourceFile, (node) => { + if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) { + return + } - const code = codez.join('\n') + const importPath = node.moduleSpecifier.text + if (!importPath.startsWith('./') && !importPath.startsWith('../')) { + return + } - await fs.promises.mkdir(testdir, { recursive: true }) - debug('writing: ' + testpath) - await fs.promises.writeFile(testpath, code, 'utf-8') - } + const importClause = node.importClause + if (!importClause || importClause.isTypeOnly) { + return + } - async _opener( - fileOrName: string, - requesterParams: Partial<ParsedParams>, - ): Promise<{ fullpath: string; code: string } | null> { - let pattern: string + if (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings)) { + for (const importElement of importClause.namedBindings.elements) { + if (importElement.isTypeOnly) { + continue + } + const localName = importElement.name.text + const importedName = importElement.propertyName?.text || localName + addAlias(localName, importedName) + } + } + }) - const language = requesterParams.language || '*' + return Array.from(aliases) + } - // Check for absolute path first (before checking for dots in basename) - if (fileOrName.startsWith('/')) { - pattern = fileOrName - } else if (path.basename(fileOrName, '.js').indexOf('.') !== -1) { - pattern = this.__src + '/' + language + '/' + fileOrName.replace(/\./g, '/') + '.js' - } else if (fileOrName.indexOf('/') === -1) { - pattern = this.__src + '/' + language + '/*/' + fileOrName + '.js' - } else { - pattern = this.__src + '/' + fileOrName - } + _stripLocutusModuleSyntax(jsCode: string): string { + return jsCode + .replace(/^\s*import[\s\S]*?from\s+['"](?:\.\.?\/)[^'"]+['"];\s*$/gm, '') + .replace(/^\s*export\s+\{[^}]+\};?\s*$/gm, '') + .replace( + /^(\s*)export\s+(?=(?:async\s+)?function\b|const\b|let\b|var\b|class\b|type\b|interface\b|enum\b)/gm, + '$1', + ) + .replace(/^(\s*)export\s+default\s+/gm, '$1') + .trim() + } - pattern = pattern.replace('golang/strings/Index.js', 'golang/strings/Index2.js') - debug('loading: ' + pattern) - const files = globby.sync(pattern, {}) + async _collectStandaloneDependencyParams( + params: ParsedParams, + options: StandaloneDependencyOptions = {}, + seen = new Set<string>(), + visiting = new Set<string>(), + ): Promise<ParsedParams[]> { + const ordered: ParsedParams[] = [] + const includeTypeOnlyImports = options.includeTypeOnlyImports === true + + const nestedDepsFor = (moduleParams: ParsedParams): string[] => { + const deps = [...(moduleParams.codeDependencies || [])] + if (!includeTypeOnlyImports) { + return deps + } - if (files.length !== 1) { - const msg = `Found ${files.length} occurances of ${fileOrName} via pattern: ${pattern}` - throw new Error(msg) + const moduleIsTs = moduleParams.filepath.endsWith('.ts') + const scriptKind = moduleIsTs ? ts.ScriptKind.TS : ts.ScriptKind.JS + const sourceFile = ts.createSourceFile( + moduleParams.filepath, + moduleParams.code, + ts.ScriptTarget.ES2022, + true, + scriptKind, + ) + const importDeps = this._extractDependencies(sourceFile, moduleParams.filepath, { + includeTypeOnlyImports: true, + }) + for (const importDep of importDeps) { + if (deps.indexOf(importDep) === -1) { + deps.push(importDep) + } + } + return deps } - const filepath = files[0] - - if (path.basename(filepath) === 'index.js') { - return null + const visit = async (depCodePath: string): Promise<void> => { + const depKey = depCodePath.replace(/\.(js|ts)$/, '') + if (seen.has(depKey) || visiting.has(depKey)) { + return + } + visiting.add(depKey) + + const depLoadPath = + depCodePath.endsWith('.js') || depCodePath.endsWith('.ts') ? depCodePath : this._resolveSourceExt(depCodePath) + const depParams = await this._load(depLoadPath, params) + if (depParams) { + for (const nestedDep of nestedDepsFor(depParams)) { + await visit(nestedDep) + } + ordered.push(depParams) + seen.add(depKey) + } + visiting.delete(depKey) } - if (!filepath) { - throw new Error('Could not find ' + pattern) + for (const depCodePath of nestedDepsFor(params)) { + await visit(depCodePath) } - const code = await fs.promises.readFile(filepath, 'utf-8') - return { fullpath: filepath, code } + return ordered } - async _load(fileOrName: string, requesterParams: Partial<ParsedParams>): Promise<ParsedParams | null> { - const result = await this._opener(fileOrName, requesterParams) - if (!result) { - return null - } - - const filepath = path.relative(this.__src, result.fullpath) - return this._parse(filepath, result.code) + async _buildStandaloneJs(params: ParsedParams): Promise<string> { + return (await this._buildStandalone(params, 'js')) || '' } - async _findDependencies( - _fileOrName: string, - requesterParams: ParsedParams, - dependencies: Record<string, ParsedParams>, - ): Promise<Record<string, ParsedParams>> { - if (!requesterParams.headKeys['depends on'] || !requesterParams.headKeys['depends on'].length) { - return {} - } - - for (const i in requesterParams.headKeys['depends on']) { - const depCodePath = requesterParams.headKeys['depends on'][i][0] + _buildStandaloneTs(params: ParsedParams): Promise<string | null> { + return this._buildStandalone(params, 'ts') + } - const params = await this._load(depCodePath, requesterParams) - if (params) { - dependencies[depCodePath] = params - await this._findDependencies(depCodePath, params, dependencies) + async _buildStandalone(params: ParsedParams, mode: StandaloneMode): Promise<string | null> { + const includeTypeOnlyImports = mode === 'ts' + const dependencies = await this._collectStandaloneDependencyParams(params, { + includeTypeOnlyImports, + }) + const modules = [...dependencies, params] + const moduleInfoByKey = new Map<string, StandaloneModuleInfo>() + const moduleOrder: string[] = [] + + for (const moduleParams of modules) { + const info = this._createStandaloneModuleInfo(moduleParams) + if (mode === 'ts' && info.hasExternalImports) { + return null } + moduleInfoByKey.set(info.moduleKey, info) + moduleOrder.push(info.moduleKey) } - return dependencies - } + const rootKey = params.filepath.replace(/\.(js|ts)$/, '') + const runtimeRequiredByModule = new Map<string, Set<string>>() + const typeRequiredByModule = new Map<string, Set<string>>() + this._addRequiredName(runtimeRequiredByModule, rootKey, params.func_name) - /** - * Extract require() calls from AST that reference other locutus functions - * Returns array of resolved function paths (e.g., "php/strings/echo") - */ - _extractRequires(ast: { body: unknown[] }, filepath: string): string[] { - const requires: string[] = [] - const seen = new Set<string>() + const selectionByModule = new Map<string, StandaloneModuleSelection>() + let changed = true + while (changed) { + changed = false - // Recursive AST walker to find require() calls - const walk = (node: unknown): void => { - if (!node || typeof node !== 'object') { - return - } + for (const moduleKey of moduleOrder) { + const info = moduleInfoByKey.get(moduleKey) + if (!info) { + continue + } - const n = node as Record<string, unknown> + const runtimeRequired = runtimeRequiredByModule.get(moduleKey) || new Set<string>() + const typeRequired = typeRequiredByModule.get(moduleKey) || new Set<string>() + if (moduleKey !== rootKey && runtimeRequired.size === 0 && typeRequired.size === 0) { + continue + } - // Check if this is a require() call - if ( - n.type === 'CallExpression' && - n.callee && - (n.callee as { type?: string; name?: string }).type === 'Identifier' && - (n.callee as { name: string }).name === 'require' && - Array.isArray(n.arguments) && - n.arguments.length > 0 - ) { - const arg = n.arguments[0] as { type?: string; value?: string } - if (arg.type === 'Literal' && typeof arg.value === 'string') { - const requirePath = arg.value - // Only process relative requires that reference locutus functions - if (requirePath.startsWith('../') || requirePath.startsWith('./')) { - // Resolve the path relative to the current file - const dir = path.dirname(filepath) - const resolved = path.normalize(path.join(dir, requirePath)) - // Remove leading slashes and convert to locutus path format - const locutusPath = resolved.replace(/^\/+/, '').replace(/\.js$/, '') - if (!seen.has(locutusPath)) { - seen.add(locutusPath) - requires.push(locutusPath) + const selection = this._selectStandaloneModuleSymbols(info, runtimeRequired, typeRequired, mode) + selectionByModule.set(moduleKey, selection) + + for (const [depKey, names] of selection.depRuntimeNames.entries()) { + for (const name of names) { + if (this._addRequiredName(runtimeRequiredByModule, depKey, name)) { + changed = true } } } - } - // Recursively walk child nodes - for (const key of Object.keys(n)) { - const child = n[key] - if (Array.isArray(child)) { - for (const item of child) { - walk(item) + for (const [depKey, names] of selection.depTypeNames.entries()) { + for (const name of names) { + if (this._addRequiredName(typeRequiredByModule, depKey, name)) { + changed = true + } } - } else if (child && typeof child === 'object') { - walk(child) } } } - walk(ast) - return requires - } - - async _parse(filepath: string, code: string): Promise<ParsedParams> { - if (!code) { - throw new Error('Unable to parse ' + filepath + '. Received no code') - } - - if (filepath.indexOf('/') === -1) { - throw new Error("Parse only accepts relative filepaths. Received: '" + filepath + "'") - } + const chunks: string[] = [] + for (const moduleKey of moduleOrder) { + const info = moduleInfoByKey.get(moduleKey) + if (!info) { + continue + } - const parts = filepath.split('/') - const language = parts.shift() as string - const codepath = parts.join('.') - const name = parts.pop() as string - const category = parts.join('.') + const runtimeRequired = runtimeRequiredByModule.get(moduleKey) || new Set<string>() + const typeRequired = typeRequiredByModule.get(moduleKey) || new Set<string>() + if (moduleKey !== rootKey && runtimeRequired.size === 0 && typeRequired.size === 0) { + continue + } - const ast = esprima.parseScript(code, { comment: true, loc: true, range: true }) + const selection = + selectionByModule.get(moduleKey) || + this._selectStandaloneModuleSymbols(info, runtimeRequired, typeRequired, mode) - const moduleExports = ast.body.filter((node: unknown) => { - try { - const n = node as { - expression: { - left: { object: { name: string }; property: { name: string } } - right: { type: string; id: { type: string; name: string } } - } - } - const leftArg = n.expression.left - const rightArg = n.expression.right + const selectedModuleCode = this._renderStandaloneModule( + info, + selection.includedStatementIndexes, + selection.wrapperRenameByStatementIndex, + ) + if (!selectedModuleCode.trim()) { + continue + } - return ( - leftArg.object.name === 'module' && - leftArg.property.name === 'exports' && - rightArg.type === 'FunctionExpression' && - rightArg.id.type === 'Identifier' && - !!rightArg.id.name + let renderedModuleCode = selectedModuleCode + if (mode === 'js') { + renderedModuleCode = this._stripLocutusModuleSyntax( + this._toWebsiteJs(renderedModuleCode, { preserveBlankLines: true }), ) - } catch (_err) { + } else { + renderedModuleCode = this._stripLocutusModuleSyntax(renderedModuleCode) + } + + const aliasLines = [...selection.runtimeAliases, ...selection.typeAliases] + const wrapperAliasLines = [...selection.wrapperAliases] + const moduleLabel = + moduleKey === rootKey ? 'target function module' : this._describeStandaloneDependencyRole(info.modulePath) + let chunk = `// ${info.modulePath} (${moduleLabel})\n` + if (aliasLines.length > 0) { + chunk += aliasLines.join('\n') + '\n\n' + } + chunk += renderedModuleCode + if (wrapperAliasLines.length > 0) { + chunk += `\n\n${wrapperAliasLines.join('\n')}` + } + chunks.push(chunk.trim()) + } + + let output = chunks.join('\n\n').trimEnd() + if (mode === 'js') { + output = this._collapseStandaloneJsPassthroughWrappers(output, params.func_name) + return this._formatWebsiteJavascript(output, `${params.filepath}.standalone.js`, { + restorePreservedBlankLines: true, + }) + } + return output + } + + _createStandaloneModuleInfo(moduleParams: ParsedParams): StandaloneModuleInfo { + const moduleIsTs = moduleParams.filepath.endsWith('.ts') + const scriptKind = moduleIsTs ? ts.ScriptKind.TS : ts.ScriptKind.JS + const sourceFile = ts.createSourceFile( + moduleParams.filepath, + moduleParams.code, + ts.ScriptTarget.ES2022, + true, + scriptKind, + ) + const moduleKey = moduleParams.filepath.replace(/\.(js|ts)$/, '') + const modulePath = moduleKey + const statements = sourceFile.statements.slice() + const declarationsByName = new Map<string, Set<number>>() + const exportToLocalName = new Map<string, string>() + const importSpecsByLocalName = new Map<string, StandaloneImportSpec[]>() + const statementDeclaredNames: Array<Set<string>> = [] + + for (let index = 0; index < statements.length; index++) { + const statement = statements[index] + if (!statement) { + continue + } + + const declaredNames = this._collectStatementDeclaredNames(statement) + statementDeclaredNames.push(declaredNames) + + for (const declaredName of declaredNames) { + if (!declarationsByName.has(declaredName)) { + declarationsByName.set(declaredName, new Set<number>()) + } + declarationsByName.get(declaredName)?.add(index) + } + + const exportedNames = this._collectStatementExportedNames(statement, declaredNames) + for (const [exportedName, localName] of exportedNames.entries()) { + exportToLocalName.set(exportedName, localName) + } + + if ( + !ts.isImportDeclaration(statement) || + !statement.moduleSpecifier || + !ts.isStringLiteral(statement.moduleSpecifier) + ) { + continue + } + + const importPath = statement.moduleSpecifier.text + if (!importPath.startsWith('./') && !importPath.startsWith('../')) { + continue + } + + const depKey = path.normalize(path.join(path.dirname(moduleKey), importPath)).replace(/\.(js|ts)$/, '') + const importClause = statement.importClause + if (!importClause) { + continue + } + + if (importClause.name) { + this._addStandaloneImportSpec(importSpecsByLocalName, { + depKey, + localName: importClause.name.text, + importedName: 'default', + isTypeOnly: importClause.isTypeOnly, + }) + } + + if (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings)) { + for (const importElement of importClause.namedBindings.elements) { + const localName = importElement.name.text + const importedName = importElement.propertyName?.text || localName + const isTypeOnly = importClause.isTypeOnly || importElement.isTypeOnly + this._addStandaloneImportSpec(importSpecsByLocalName, { + depKey, + localName, + importedName, + isTypeOnly, + }) + } + } + } + + const topLevelNames = new Set<string>(declarationsByName.keys()) + const runtimeImportLocals = new Set<string>() + const typeImportLocals = new Set<string>() + for (const [localName, importSpecs] of importSpecsByLocalName.entries()) { + const hasRuntimeImport = importSpecs.some((importSpec) => !importSpec.isTypeOnly) + if (hasRuntimeImport) { + runtimeImportLocals.add(localName) + } + + const hasTypeImport = importSpecs.some((importSpec) => importSpec.isTypeOnly) + if (hasTypeImport) { + typeImportLocals.add(localName) + } + } + + const statementInfo: StandaloneStatementInfo[] = [] + for (let index = 0; index < statements.length; index++) { + const statement = statements[index] + if (!statement) { + continue + } + const declaredNames = statementDeclaredNames[index] || new Set<string>() + + const runtimeRefs = this._collectReferencedIdentifiers(statement, false) + const allRefs = this._collectReferencedIdentifiers(statement, true) + for (const declaredName of declaredNames) { + runtimeRefs.delete(declaredName) + allRefs.delete(declaredName) + } + + const runtimeTopLevelDeps = new Set<string>() + const allTopLevelDeps = new Set<string>() + const statementRuntimeImportLocals = new Set<string>() + const statementTypeImportLocals = new Set<string>() + + for (const ref of runtimeRefs) { + if (topLevelNames.has(ref)) { + runtimeTopLevelDeps.add(ref) + } + if (runtimeImportLocals.has(ref)) { + statementRuntimeImportLocals.add(ref) + } + } + + for (const ref of allRefs) { + if (topLevelNames.has(ref)) { + allTopLevelDeps.add(ref) + } + if (typeImportLocals.has(ref)) { + statementTypeImportLocals.add(ref) + } + } + + statementInfo.push({ + declaredNames, + runtimeTopLevelDeps, + allTopLevelDeps, + runtimeImportLocals: statementRuntimeImportLocals, + typeImportLocals: statementTypeImportLocals, + }) + } + + return { + moduleKey, + modulePath, + params: moduleParams, + sourceFile, + hasExternalImports: this._hasExternalImports(sourceFile), + statements, + statementInfo, + declarationsByName, + exportToLocalName, + importSpecsByLocalName, + } + } + + _selectStandaloneModuleSymbols( + info: StandaloneModuleInfo, + runtimeRequiredNames: Set<string>, + typeRequiredNames: Set<string>, + mode: StandaloneMode, + ): StandaloneModuleSelection { + const includeTypeDeps = mode === 'ts' + const pendingLocalNames: string[] = [] + const visitedLocalNames = new Set<string>() + + const seedLocalName = (requiredExportName: string): void => { + const localName = info.exportToLocalName.get(requiredExportName) || requiredExportName + if (!info.declarationsByName.has(localName) || visitedLocalNames.has(localName)) { + return + } + pendingLocalNames.push(localName) + } + + for (const runtimeRequiredName of runtimeRequiredNames) { + seedLocalName(runtimeRequiredName) + } + for (const typeRequiredName of typeRequiredNames) { + seedLocalName(typeRequiredName) + } + + const includedStatementIndexes = new Set<number>() + while (pendingLocalNames.length > 0) { + const localName = pendingLocalNames.pop() + if (!localName || visitedLocalNames.has(localName)) { + continue + } + visitedLocalNames.add(localName) + + const statementIndexes = info.declarationsByName.get(localName) + if (!statementIndexes) { + continue + } + + for (const statementIndex of statementIndexes) { + if (includedStatementIndexes.has(statementIndex)) { + continue + } + includedStatementIndexes.add(statementIndex) + + const statementMeta = info.statementInfo[statementIndex] + if (!statementMeta) { + continue + } + const deps = includeTypeDeps ? statementMeta.allTopLevelDeps : statementMeta.runtimeTopLevelDeps + for (const depName of deps) { + if (!visitedLocalNames.has(depName)) { + pendingLocalNames.push(depName) + } + } + } + } + + const depRuntimeNames = new Map<string, Set<string>>() + const depTypeNames = new Map<string, Set<string>>() + const runtimeAliases = new Set<string>() + const wrapperAliases = new Set<string>() + const wrapperRenameByStatementIndex = new Map<number, string>() + const typeAliases = new Set<string>() + + for (const statementIndex of includedStatementIndexes) { + const statementMeta = info.statementInfo[statementIndex] + if (!statementMeta) { + continue + } + + for (const runtimeImportLocal of statementMeta.runtimeImportLocals) { + const importSpecs = info.importSpecsByLocalName.get(runtimeImportLocal) || [] + for (const importSpec of importSpecs) { + if (importSpec.isTypeOnly) { + continue + } + this._addRequiredName(depRuntimeNames, importSpec.depKey, importSpec.importedName) + if (importSpec.localName !== importSpec.importedName) { + runtimeAliases.add(`const ${importSpec.localName} = ${importSpec.importedName};`) + } + } + } + + if (!includeTypeDeps) { + continue + } + + for (const typeImportLocal of statementMeta.typeImportLocals) { + const importSpecs = info.importSpecsByLocalName.get(typeImportLocal) || [] + for (const importSpec of importSpecs) { + this._addRequiredName(depTypeNames, importSpec.depKey, importSpec.importedName) + if (importSpec.localName !== importSpec.importedName) { + typeAliases.add(`type ${importSpec.localName} = ${importSpec.importedName}`) + } + } + } + } + + this._collapseStandaloneForwardingWrappers( + info, + includedStatementIndexes, + wrapperAliases, + wrapperRenameByStatementIndex, + runtimeRequiredNames, + mode, + ) + + return { + includedStatementIndexes, + depRuntimeNames, + depTypeNames, + runtimeAliases, + wrapperAliases, + wrapperRenameByStatementIndex, + typeAliases, + } + } + + _collapseStandaloneForwardingWrappers( + info: StandaloneModuleInfo, + includedStatementIndexes: Set<number>, + wrapperAliases: Set<string>, + wrapperRenameByStatementIndex: Map<number, string>, + runtimeRequiredNames: Set<string>, + mode: StandaloneMode, + ): void { + if (mode !== 'js') { + return + } + + const sortedIndexes = [...includedStatementIndexes].sort((a, b) => b - a) + for (const statementIndex of sortedIndexes) { + const aliasSpec = this._getStandaloneForwardingWrapperAlias(info, statementIndex, includedStatementIndexes) + if (!aliasSpec) { + continue + } + + includedStatementIndexes.delete(statementIndex) + if ( + this._canRenameStandaloneWrapperTarget( + info, + aliasSpec, + includedStatementIndexes, + statementIndex, + runtimeRequiredNames, + ) + ) { + wrapperRenameByStatementIndex.set(aliasSpec.targetDeclarationIndex, aliasSpec.wrapperName) + continue + } + wrapperAliases.add(`const ${aliasSpec.wrapperName} = ${aliasSpec.targetName};`) + } + } + + _getStandaloneForwardingWrapperAlias( + info: StandaloneModuleInfo, + statementIndex: number, + includedStatementIndexes: Set<number>, + ): { wrapperName: string; targetName: string; targetDeclarationIndex: number } | null { + const statement = info.statements[statementIndex] + if (!statement || !ts.isFunctionDeclaration(statement) || !statement.name || !statement.body) { + return null + } + + const wrapperName = statement.name.text + if (wrapperName === info.params.func_name) { + // Keep the root function declaration visible in standalone output. + return null + } + + if (statement.body.statements.length !== 1) { + return null + } + + const onlyBodyStatement = statement.body.statements[0] + if (!onlyBodyStatement || !ts.isReturnStatement(onlyBodyStatement) || !onlyBodyStatement.expression) { + return null + } + + const returnExpression = onlyBodyStatement.expression + if (!ts.isCallExpression(returnExpression) || !ts.isIdentifier(returnExpression.expression)) { + return null + } + + const targetName = returnExpression.expression.text + if (targetName === wrapperName) { + return null + } + + if (statement.parameters.length !== returnExpression.arguments.length) { + return null + } + + for (let index = 0; index < statement.parameters.length; index++) { + const parameter = statement.parameters[index] + const argument = returnExpression.arguments[index] + if ( + !parameter || + !argument || + !ts.isIdentifier(parameter.name) || + !ts.isIdentifier(argument) || + parameter.dotDotDotToken || + parameter.initializer || + parameter.questionToken || + parameter.name.text !== argument.text + ) { + return null + } + } + + const targetDeclarationIndexes = info.declarationsByName.get(targetName) + if (!targetDeclarationIndexes) { + return null + } + + for (const targetDeclarationIndex of targetDeclarationIndexes) { + if (!includedStatementIndexes.has(targetDeclarationIndex)) { + continue + } + const targetStatement = info.statements[targetDeclarationIndex] + if ( + targetStatement && + ts.isFunctionDeclaration(targetStatement) && + targetStatement.name && + targetStatement.name.text === targetName + ) { + return { wrapperName, targetName, targetDeclarationIndex } + } + } + + return null + } + + _canRenameStandaloneWrapperTarget( + info: StandaloneModuleInfo, + aliasSpec: { wrapperName: string; targetName: string; targetDeclarationIndex: number }, + includedStatementIndexes: Set<number>, + wrapperStatementIndex: number, + runtimeRequiredNames: Set<string>, + ): boolean { + if (runtimeRequiredNames.has(aliasSpec.targetName)) { + return false + } + + const otherWrapperDeclarations = info.declarationsByName.get(aliasSpec.wrapperName) + if (otherWrapperDeclarations) { + for (const declarationIndex of otherWrapperDeclarations) { + if (declarationIndex !== wrapperStatementIndex) { + return false + } + } + } + + for (const statementIndex of includedStatementIndexes) { + const statement = info.statements[statementIndex] + if (!statement) { + continue + } + + if (statementIndex === aliasSpec.targetDeclarationIndex) { + if (this._statementContainsIdentifierReference(statement, aliasSpec.targetName, true)) { + return false + } + continue + } + + if (this._statementContainsIdentifierReference(statement, aliasSpec.targetName, false)) { return false } + } + + return true + } + + _statementContainsIdentifierReference( + statement: ts.Statement, + identifierName: string, + ignoreFunctionDeclarationName: boolean, + ): boolean { + let found = false + + const visit = (node: ts.Node): void => { + if (found) { + return + } + + if (ts.isIdentifier(node) && node.text === identifierName) { + if (ignoreFunctionDeclarationName && ts.isFunctionDeclaration(node.parent) && node.parent.name === node) { + // Declaration name can be renamed safely. + } else { + found = true + return + } + } + + ts.forEachChild(node, visit) + } + + ts.forEachChild(statement, visit) + return found + } + + _collapseStandaloneJsPassthroughWrappers(code: string, rootFunctionName: string): string { + let nextCode = code + const maxPasses = 4 + + for (let pass = 0; pass < maxPasses; pass++) { + const sourceFile = ts.createSourceFile('standalone.js', nextCode, ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS) + const topLevelDeclarationCounts = new Map<string, number>() + for (const statement of sourceFile.statements) { + const declaredNames = this._collectStatementDeclaredNames(statement) + for (const declaredName of declaredNames) { + topLevelDeclarationCounts.set(declaredName, (topLevelDeclarationCounts.get(declaredName) || 0) + 1) + } + } + + const wrapperCandidates = new Map< + string, + { name: string; statementIndex: number; calleeText: string; declarationName: ts.Identifier } + >() + for (let statementIndex = 0; statementIndex < sourceFile.statements.length; statementIndex++) { + const statement = sourceFile.statements[statementIndex] + if (!statement) { + continue + } + const wrapper = this._extractStandaloneJsPassthroughWrapper(statement) + if (!wrapper) { + continue + } + if (wrapper.name === rootFunctionName) { + continue + } + if ((topLevelDeclarationCounts.get(wrapper.name) || 0) !== 1) { + continue + } + wrapperCandidates.set(wrapper.name, { + name: wrapper.name, + statementIndex, + calleeText: wrapper.callee.getText(sourceFile), + declarationName: wrapper.declarationName, + }) + } + + if (wrapperCandidates.size === 0) { + break + } + + const removableWrappers = new Map< + string, + { statementIndex: number; calleeText: string; callCalleeIdentifiers: ts.Identifier[] } + >() + + for (const [wrapperName, wrapper] of wrapperCandidates.entries()) { + let hasNonCallReference = false + let hasNestedDeclaration = false + const callCalleeIdentifiers: ts.Identifier[] = [] + + const visit = (node: ts.Node): void => { + if (hasNonCallReference || hasNestedDeclaration) { + return + } + + if (ts.isIdentifier(node) && node.text === wrapperName) { + if (node === wrapper.declarationName) { + return + } + + if (this._isDeclarationNameIdentifier(node)) { + hasNestedDeclaration = true + return + } + + if (ts.isCallExpression(node.parent) && node.parent.expression === node) { + callCalleeIdentifiers.push(node) + return + } + + hasNonCallReference = true + return + } + + ts.forEachChild(node, visit) + } + + ts.forEachChild(sourceFile, visit) + if (!hasNonCallReference && !hasNestedDeclaration) { + removableWrappers.set(wrapperName, { + statementIndex: wrapper.statementIndex, + calleeText: wrapper.calleeText, + callCalleeIdentifiers, + }) + } + } + + if (removableWrappers.size === 0) { + break + } + + const edits: Array<{ start: number; end: number; text: string }> = [] + for (const removableWrapper of removableWrappers.values()) { + const statement = sourceFile.statements[removableWrapper.statementIndex] + if (statement) { + edits.push({ + start: statement.getStart(sourceFile), + end: statement.getEnd(), + text: '', + }) + } + + for (const callIdentifier of removableWrapper.callCalleeIdentifiers) { + edits.push({ + start: callIdentifier.getStart(sourceFile), + end: callIdentifier.getEnd(), + text: removableWrapper.calleeText, + }) + } + } + + if (edits.length === 0) { + break + } + + edits.sort((a, b) => b.start - a.start) + let updatedCode = nextCode + for (const edit of edits) { + updatedCode = updatedCode.slice(0, edit.start) + edit.text + updatedCode.slice(edit.end) + } + + if (updatedCode === nextCode) { + break + } + nextCode = updatedCode + } + + return nextCode + } + + _isDeclarationNameIdentifier(node: ts.Identifier): boolean { + if ( + (ts.isFunctionDeclaration(node.parent) || + ts.isFunctionExpression(node.parent) || + ts.isClassDeclaration(node.parent) || + ts.isClassExpression(node.parent) || + ts.isTypeAliasDeclaration(node.parent) || + ts.isInterfaceDeclaration(node.parent) || + ts.isEnumDeclaration(node.parent) || + ts.isVariableDeclaration(node.parent) || + ts.isParameter(node.parent) || + ts.isImportClause(node.parent) || + ts.isImportSpecifier(node.parent) || + ts.isNamespaceImport(node.parent) || + ts.isImportEqualsDeclaration(node.parent) || + ts.isBindingElement(node.parent)) && + node.parent.name === node + ) { + return true + } + + return false + } + + _extractStandaloneJsPassthroughWrapper( + statement: ts.Statement, + ): { name: string; callee: ts.Expression; declarationName: ts.Identifier } | null { + if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) { + const passthroughCall = this._extractPassthroughCallFromBody(statement.parameters, statement.body) + if (!passthroughCall) { + return nul ... [truncated]
src/_util/vendor.d.ts+31 −0 added@@ -0,0 +1,31 @@ +declare module 'globby' { + interface Globby { + (patterns: string | readonly string[], options?: unknown): Promise<string[]> + sync(patterns: string | readonly string[], options?: unknown): string[] + } + + const globby: Globby + export default globby +} + +declare module 'indent-string' { + export default function indentString(input: string, indent?: string, count?: number): string +} + +declare module 'js-yaml' { + interface YamlApi { + dump(value: unknown, options?: unknown): string + } + + const YAML: YamlApi + export default YAML +} + +declare module 'lodash' { + interface LodashApi { + flattenDeep<T>(array: unknown[]): T[] + } + + const _: LodashApi + export default _ +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-fp25-p6mj-qqg6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-29091ghsaADVISORY
- developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/evalghsaWEB
- github.com/locutusjs/locutus/blob/main/src/php/funchand/call_user_func_array.jsghsaWEB
- github.com/locutusjs/locutus/commit/977a1fb169441e35996a1d2465b512322de500adghsax_refsource_MISCWEB
- github.com/locutusjs/locutus/security/advisories/GHSA-fp25-p6mj-qqg6ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.