diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index bd091e85..4202d1b0 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -3,8 +3,13 @@ "v6.7.0": { "released_date": "2025/10/26", "feature": [ + "cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))", + "fmt 模块, 用于数据格式化 (参阅 项目文档 > [格式化](https://docs.autojs6.com/#/fmt))", "zip 模块, 用于文件压缩与解压缩操作 (Ref to [Auto.js Pro](https://g.pro.autojs.org/)) (参阅 项目文档 > [Zip](https://docs.autojs6.com/#/zip))", "mediainfo 模块, 用于查看媒体文件的详细信息 (参阅 项目文档 > [媒体信息](https://docs.autojs6.com/#/mediainfo))", + "cvt.bytes 方法, 用于字节数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))", + "fmt.bytes 方法, 用于字节数据格式化 (参阅 项目文档 > [数据格式化](https://docs.autojs6.com/#/fmt))", + "s13n.bytes 方法, 用于标准化字节数据 (参阅 项目文档 > [标准化](https://docs.autojs6.com/#/s13n))", "UiObject#isShifted 方法, 用于检测控件位置变化", "device.getSharedDeviceId 方法, 用于跨应用获取统一共享设备 ID _[`issue #455`](http://issues.autojs6.com/455)_", "structuredClone 全局方法, 用于深拷贝 JavaScript 对象 (参阅 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/structuredClone))", diff --git a/app/src/main/java/org/autojs/autojs/extension/AnyExtensions.kt b/app/src/main/java/org/autojs/autojs/extension/AnyExtensions.kt index 1fd36c9a..e5bae3ce 100644 --- a/app/src/main/java/org/autojs/autojs/extension/AnyExtensions.kt +++ b/app/src/main/java/org/autojs/autojs/extension/AnyExtensions.kt @@ -17,6 +17,7 @@ import org.mozilla.javascript.NativeError import org.mozilla.javascript.Scriptable import org.mozilla.javascript.UniqueTag.NOT_FOUND import org.mozilla.javascript.Wrapper +import java.math.BigInteger object AnyExtensions { @@ -82,8 +83,8 @@ object AnyExtensions { fun T?.jsSanitize() = if (this.isJsNullish()) null else this fun Any?.jsUnwrapped(): Any? = when (this) { - is String -> this - is ConsString -> this.toString() + is String, is ConsString -> Context.toString(this) + is BigInteger -> this is Number -> Context.toNumber(this) is Boolean -> Context.toBoolean(this) is Wrapper -> this.unwrap().jsUnwrapped() diff --git a/app/src/main/java/org/autojs/autojs/pio/PFiles.kt b/app/src/main/java/org/autojs/autojs/pio/PFiles.kt index cf415dfd..b60d6f2f 100644 --- a/app/src/main/java/org/autojs/autojs/pio/PFiles.kt +++ b/app/src/main/java/org/autojs/autojs/pio/PFiles.kt @@ -4,7 +4,7 @@ import android.content.Context import android.content.res.AssetManager import android.text.TextUtils import org.autojs.autojs.app.GlobalAppContext -import org.autojs.autojs.core.pref.Language +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes import org.autojs.autojs.tool.Func1 import org.autojs.autojs.util.EnvironmentUtils import org.autojs.autojs.util.FileUtils @@ -19,8 +19,6 @@ import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.nio.charset.Charset -import kotlin.math.ln -import kotlin.math.pow /** * Created by Stardust on Apr 1, 2017. @@ -60,14 +58,9 @@ object PFiles { @JvmStatic fun create(path: String): Boolean { val f = File(path) - return if (path.endsWith(separator)) { - f.mkdir() - } else { - try { - f.createNewFile() - } catch (e: IOException) { - false - } + return when { + path.endsWith(separator) -> f.mkdir() + else -> runCatching { f.createNewFile() }.isSuccess } } @@ -95,7 +88,7 @@ object PFiles { fun ensureDir(path: String): Boolean { val i = path.lastIndexOf(separator) return if (i >= 0) { - val folder = path.substring(0, i) + val folder = path.take(i) val file = File(folder) file.exists() || file.mkdirs() } else false @@ -355,7 +348,7 @@ object PFiles { val fileName = getName(filePath) var b = fileName.lastIndexOf('.') if (b < 0) b = fileName.length - return fileName.substring(0, b) + return fileName.take(b) } fun copyAssetToTmpFile(context: Context, path: String): File { @@ -450,14 +443,18 @@ object PFiles { } @JvmStatic - fun getHumanReadableSize(bytes: Long): String { - val unit = 1024 - if (bytes < unit) return "$bytes B" - val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt() - - @Suppress("SpellCheckingInspection") - val pre = "KMGTPE".substring(exp - 1, exp) - return String.format(Language.getPrefLanguage().locale, "%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre) + @JvmOverloads + fun getHumanReadableSize(bytes: Long, useIecIdentifier: Boolean = false): String { + return Bytes.string( + source = bytes.toDouble(), + fromUnit = "B", + toUnit = "AUTO", + useIecIdentifier = useIecIdentifier, + useSpace = true, + fractionDigits = 1, + trimTrailingZero = false, + signature = "PFiles.getHumanReadableSize", + ) } @JvmStatic @@ -511,10 +508,6 @@ object PFiles { @JvmStatic fun closeSilently(closeable: Closeable?) { - try { - closeable?.close() - } catch (ignored: IOException) { - /* Ignored. */ - } + runCatching { closeable?.close() } } } \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/runtime/ScriptRuntime.kt b/app/src/main/java/org/autojs/autojs/runtime/ScriptRuntime.kt index 69d9e490..6693a4a6 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/ScriptRuntime.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/ScriptRuntime.kt @@ -44,6 +44,7 @@ import org.autojs.autojs.runtime.api.augment.colors.Color import org.autojs.autojs.runtime.api.augment.colors.Colors import org.autojs.autojs.runtime.api.augment.console.Console import org.autojs.autojs.runtime.api.augment.continuation.Continuation +import org.autojs.autojs.runtime.api.augment.converter.Converter import org.autojs.autojs.runtime.api.augment.cryptyo.Crypto import org.autojs.autojs.runtime.api.augment.device.Device import org.autojs.autojs.runtime.api.augment.dialogs.Dialogs @@ -52,6 +53,7 @@ import org.autojs.autojs.runtime.api.augment.events.Events import org.autojs.autojs.runtime.api.augment.events.Keys import org.autojs.autojs.runtime.api.augment.files.Files import org.autojs.autojs.runtime.api.augment.floaty.Floaty +import org.autojs.autojs.runtime.api.augment.formatter.Formatter import org.autojs.autojs.runtime.api.augment.global.Global import org.autojs.autojs.runtime.api.augment.global.GlobalClasses import org.autojs.autojs.runtime.api.augment.global.IsNullish @@ -160,6 +162,8 @@ import org.autojs.autojs.runtime.api.Toaster as ApiToaster import org.autojs.autojs.runtime.api.UI as ApiUI import org.autojs.autojs.runtime.api.Util as ApiUtil import org.autojs.autojs.runtime.api.augment.autojs.Version as AutojsVersion +import org.autojs.autojs.runtime.api.augment.converter.Bytes as BytesCvt +import org.autojs.autojs.runtime.api.augment.formatter.Bytes as BytesFmt import org.autojs.autojs.runtime.api.augment.global.Legacy as GlobalLegacy import org.autojs.autojs.runtime.api.augment.notice.Channel as NoticeChannel import org.autojs.autojs.runtime.api.augment.util.Inspect as UtilInspect @@ -714,20 +718,20 @@ class ScriptRuntime private constructor(builder: Builder) { private fun augment(target: ScriptableObject) { - Global(this).assignWithGlobal(target, topLevelScope, GlobalClasses) - GlobalLegacy(this).assignWithGlobal(target, topLevelScope) - IsNullish.augmentWithGlobal(target, topLevelScope, false) - Util.augmentWithGlobal(target, topLevelScope, util, true).apply { - UtilJava.augmentWithGlobal(this, topLevelScope, false) - UtilVersion.augmentWithGlobal(this, topLevelScope, false) - UtilVersionCodes.augmentWithGlobal(this, topLevelScope, VersionCodesInfo.obj, false) - UtilInspect.augmentWithGlobal(this, topLevelScope, false) - UtilMorseCode.augmentWithGlobal(this, topLevelScope, false) + Global(this).assignWithRuntime(target, this, GlobalClasses) + GlobalLegacy(this).assignWithRuntime(target, this) + IsNullish.augmentWithRuntime(target, this, false) + Util.augmentWithRuntime(target, this, util, true).also { util -> + UtilJava.augmentWithRuntime(util, this, false) + UtilVersion.augmentWithRuntime(util, this, false) + UtilVersionCodes.augmentWithRuntime(util, this, VersionCodesInfo.obj, false) + UtilInspect.augmentWithRuntime(util, this, false) + UtilMorseCode.augmentWithRuntime(util, this, false) } - Species.augmentWithGlobal(target, topLevelScope, true) + Species.augmentWithRuntime(target, this, true) App(this).augment(target, app, true).also { augmentedApp = it } - Autojs(this).augment(target, true).also { augmentedAutojs = it }.apply { - AutojsVersion.augmentWithGlobal(this, topLevelScope, false) + Autojs(this).augment(target, true).also { augmentedAutojs = it }.also { autojs -> + AutojsVersion.augmentWithRuntime(autojs, this, false) } Shell(this).augment(target, true) Timers(this).augment(target, timers, true) @@ -735,26 +739,32 @@ class ScriptRuntime private constructor(builder: Builder) { Automator(this).augment(target, true) Selector(this).augment(target, true, READONLY) Events(this).augment(target, events, true) - Keys.augmentWithGlobal(target, topLevelScope, true) + Keys.augmentWithRuntime(target, this, true) Images(this).augment(target, true) - Ocr(this).augment(target, true).apply { - OcrMLKit(this@ScriptRuntime).augment(this, false).also { augmentedOcrMLKit = it } - OcrPaddle(this@ScriptRuntime).augment(this, false).also { augmentedOcrPaddle = it } - OcrRapid(this@ScriptRuntime).augment(this, false).also { augmentedOcrRapid = it } + Ocr(this).augment(target, true).also { ocr -> + OcrMLKit(this).augment(ocr, false).also { augmentedOcrMLKit = it } + OcrPaddle(this).augment(ocr, false).also { augmentedOcrPaddle = it } + OcrRapid(this).augment(ocr, false).also { augmentedOcrRapid = it } } Barcode(this).augment(target, true) QrCode(this).augment(target, true) Threads(this).augment(target, threads, true) UI(this).proxying(target, ui, true) - Colors.augmentWithGlobal(target, topLevelScope, listOf(colors, Colors), true) - Color.augmentWithGlobal(target, topLevelScope, false) + Colors.augmentWithRuntime(target, this, listOf(colors, Colors), true) + Color.augmentWithRuntime(target, this, false) Tasks(this).augment(target, true) Dialogs(this).augment(target, true) Continuation(this).augment(target, js_mod_continuation, true, READONLY) Http(this).augment(target, http, true) Web(this).augment(target, true) WebSocket(this).augment(target, WebSocketFields, false) - S13n.augmentWithGlobal(target, topLevelScope, true) + S13n.augmentWithRuntime(target, this, true) + Converter.augmentWithRuntime(target, this, true).also { cvt -> + BytesCvt.augmentWithRuntime(cvt, this, false) + } + Formatter.augmentWithRuntime(target, this, true).also { fmt -> + BytesFmt.augmentWithRuntime(fmt, this, false) + } Console(this).proxying(target, console, true).also { consoleProxyObject = it } Plugins(this).augment(target, true) Arrayx(this).augment(target, false) @@ -762,29 +772,29 @@ class ScriptRuntime private constructor(builder: Builder) { Mathx(this).augment(target, false) Jsox(this).augment(target, true) Files(this).augment(target, files, true) - Crypto.augmentWithGlobal(target, topLevelScope, CoreCrypto, true) + Crypto.augmentWithRuntime(target, this, CoreCrypto, true) RootAutomator(this).augment(target, false) Engines(this).augment(target, true) Floaty(this).augment(target, true) - Storages.augmentWithGlobal(target, topLevelScope, true) + Storages.augmentWithRuntime(target, this, true) Device(this).augment(target, device, true) Recorder(this).augment(target, recorder, true) Toast(this).augment(target, true) - Media.augmentWithGlobal(target, topLevelScope, media, true) - Sensors.augmentWithGlobal(target, topLevelScope, sensors, true) - Base64.augmentWithGlobal(target, topLevelScope, true) - Notice(this).augment(target, true).apply { - NoticeChannel(this@ScriptRuntime).augment(this, false) + Media.augmentWithRuntime(target, this, media, true) + Sensors.augmentWithRuntime(target, this, sensors, true) + Base64.augmentWithRuntime(target, this, true) + Notice(this).augment(target, true).also { notice -> + NoticeChannel(this).augment(notice, false) } Shizuku(this).augment(target, shizuku, true) - OpenCC.augmentWithGlobal(target, topLevelScope, true) + OpenCC.augmentWithRuntime(target, this, true) Mime(this).augment(target, mime, true) SysProps(this).augment(target, true) SQLite(this).augment(target, true) Zip(this).augment(target, true) - NanoID.augmentWithGlobal(target, topLevelScope, true) - Pinyin.augmentWithGlobal(target, topLevelScope, true) - Pinyin4j.augmentWithGlobal(target, topLevelScope, true) + NanoID.augmentWithRuntime(target, this, true) + Pinyin.augmentWithRuntime(target, this, true) + Pinyin4j.augmentWithRuntime(target, this, true) Mediainfo(this).augment(target, true) augmentedApp.defineProp(Autojs::class.java.simpleName.lowercase(), augmentedAutojs) diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/Files.kt b/app/src/main/java/org/autojs/autojs/runtime/api/Files.kt index b3cf9cad..bd4bedeb 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/Files.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/Files.kt @@ -192,8 +192,9 @@ class Files(private val scriptRuntime: ScriptRuntime) { return PFiles.isEmptyDir(path(path)) } - fun getHumanReadableSize(bytes: Long): String { - return PFiles.getHumanReadableSize(bytes) + @JvmOverloads + fun getHumanReadableSize(bytes: Long, useIecIdentifier: Boolean = false): String { + return PFiles.getHumanReadableSize(bytes, useIecIdentifier) } fun getSimplifiedPath(path: String?): String { diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/Augmentable.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/Augmentable.kt index 21d0c7c0..72b35470 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/Augmentable.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/Augmentable.kt @@ -138,19 +138,19 @@ abstract class Augmentable(private val scriptRuntime: ScriptRuntime? = null) : F fun originateKeyName() = also { mIsOriginalKeyName = true } - fun augmentWithGlobal(target: Scriptable, specifiedGlobal: ScriptableObject, withDollarPrefix: Boolean = true, additionalAttributes: Int = 0): ScriptableObject { - return augment(target, withDollarPrefix, additionalAttributes, specifiedGlobal) + fun augmentWithRuntime(target: Scriptable, specifiedRuntime: ScriptRuntime, withDollarPrefix: Boolean = true, additionalAttributes: Int = 0): ScriptableObject { + return augment(target, withDollarPrefix, additionalAttributes, specifiedRuntime) } - fun augmentWithGlobal(target: Scriptable, specifiedGlobal: ScriptableObject, proto: Any, withDollarPrefix: Boolean = true, additionalAttributes: Int = 0): ScriptableObject { - return augment(target, proto, withDollarPrefix, additionalAttributes, specifiedGlobal) + fun augmentWithRuntime(target: Scriptable, specifiedRuntime: ScriptRuntime, proto: Any, withDollarPrefix: Boolean = true, additionalAttributes: Int = 0): ScriptableObject { + return augment(target, proto, withDollarPrefix, additionalAttributes, specifiedRuntime) } - fun augment(target: Scriptable, withDollarPrefix: Boolean = true, additionalAttributes: Int = 0, specifiedGlobal: ScriptableObject? = null): ScriptableObject { - return augment(target, emptyList(), withDollarPrefix, additionalAttributes, specifiedGlobal) + fun augment(target: Scriptable, withDollarPrefix: Boolean = true, additionalAttributes: Int = 0, specifiedRuntime: ScriptRuntime? = null): ScriptableObject { + return augment(target, emptyList(), withDollarPrefix, additionalAttributes, specifiedRuntime) } - fun augment(target: Scriptable, proto: Any, withDollarPrefix: Boolean = true, additionalAttributes: Int = 0, specifiedGlobal: ScriptableObject? = null): ScriptableObject { + fun augment(target: Scriptable, proto: Any, withDollarPrefix: Boolean = true, additionalAttributes: Int = 0, specifiedRuntime: ScriptRuntime? = null): ScriptableObject { val callFunc: (args: Array) -> Any? = { args -> try { (this as Invokable).invoke(*args) @@ -191,7 +191,7 @@ abstract class Augmentable(private val scriptRuntime: ScriptRuntime? = null) : F else -> RhinoUtils.newObject(target) } - assign(newObj, proto, specifiedGlobal) + assign(newObj, proto, specifiedRuntime) val keys = mutableListOf(key) if (withDollarPrefix) keys += "\$$key" @@ -203,17 +203,17 @@ abstract class Augmentable(private val scriptRuntime: ScriptRuntime? = null) : F return newObj } - fun assignWithGlobal(target: ScriptableObject, specifiedGlobal: ScriptableObject, proto: Any? = null) { - assign(target, proto, specifiedGlobal) + fun assignWithRuntime(target: ScriptableObject, specifiedRuntime: ScriptRuntime, proto: Any? = null) { + assign(target, proto, specifiedRuntime) } /** * When the subclass calls assign, the value of key will be ignored. * zh-CN: 子类调用 assign 时将忽略 key 的值. */ - fun assign(target: ScriptableObject, proto: Any? = null, specifiedGlobal: ScriptableObject? = null) { + fun assign(target: ScriptableObject, proto: Any? = null, specifiedRuntime: ScriptRuntime? = null) { - val global: ScriptableObject = specifiedGlobal ?: scriptRuntime?.topLevelScope ?: ScriptableObject.getTopLevelScope(target) as ScriptableObject + val global: ScriptableObject = (specifiedRuntime ?: scriptRuntime)?.topLevelScope ?: ScriptableObject.getTopLevelScope(target) as ScriptableObject val protos = when (proto) { null -> emptyList() diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/console/Console.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/console/Console.kt index 836a9a9f..30a415f3 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/console/Console.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/console/Console.kt @@ -40,7 +40,13 @@ import org.autojs.autojs.util.RhinoUtils.withRhinoContext import org.autojs.autojs.util.StringUtils.lowercaseFirstChar import org.autojs.autojs.util.StringUtils.uppercaseFirstChar import org.autojs.autojs6.R -import org.mozilla.javascript.* +import org.mozilla.javascript.BaseFunction +import org.mozilla.javascript.Context +import org.mozilla.javascript.NativeArray +import org.mozilla.javascript.NativeError +import org.mozilla.javascript.NativeObject +import org.mozilla.javascript.ScriptableObject +import org.mozilla.javascript.Undefined @Suppress("unused", "UNUSED_PARAMETER") class Console(scriptRuntime: ScriptRuntime) : AugmentableProxy(scriptRuntime) { @@ -226,38 +232,38 @@ class Console(scriptRuntime: ScriptRuntime) : AugmentableProxy(scriptRuntime) { @JvmStatic @RhinoRuntimeFunctionInterface - fun log(scriptRuntime: ScriptRuntime, args: Array) = unwrapArguments(args) { - scriptRuntime.console.log(Util.formatRhino(*it)) + fun log(scriptRuntime: ScriptRuntime, args: Array) { + return scriptRuntime.console.log(Util.formatRhino(*args)) } @JvmStatic @RhinoRuntimeFunctionInterface - fun verbose(scriptRuntime: ScriptRuntime, args: Array) = unwrapArguments(args) { - scriptRuntime.console.verbose(Util.formatRhino(*it)) + fun verbose(scriptRuntime: ScriptRuntime, args: Array) { + return scriptRuntime.console.verbose(Util.formatRhino(*args)) } @JvmStatic @RhinoRuntimeFunctionInterface - fun info(scriptRuntime: ScriptRuntime, args: Array) = unwrapArguments(args) { - scriptRuntime.console.info(Util.formatRhino(*it)) + fun info(scriptRuntime: ScriptRuntime, args: Array) { + return scriptRuntime.console.info(Util.formatRhino(*args)) } @JvmStatic @RhinoRuntimeFunctionInterface - fun warn(scriptRuntime: ScriptRuntime, args: Array) = unwrapArguments(args) { - scriptRuntime.console.warn(Util.formatRhino(*it)) + fun warn(scriptRuntime: ScriptRuntime, args: Array) { + return scriptRuntime.console.warn(Util.formatRhino(*args)) } @JvmStatic @RhinoRuntimeFunctionInterface - fun error(scriptRuntime: ScriptRuntime, args: Array) = unwrapArguments(args) { - scriptRuntime.console.error(Util.formatRhino(*it)) + fun error(scriptRuntime: ScriptRuntime, args: Array) { + return scriptRuntime.console.error(Util.formatRhino(*args)) } @JvmStatic @RhinoRuntimeFunctionInterface - fun print(scriptRuntime: ScriptRuntime, args: Array) = unwrapArguments(args) { - scriptRuntime.console.print(Log.DEBUG, Util.formatRhino(*it)) + fun print(scriptRuntime: ScriptRuntime, args: Array) { + return scriptRuntime.console.print(Log.DEBUG, Util.formatRhino(*args)) } @JvmStatic diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/converter/Bytes.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/converter/Bytes.kt new file mode 100644 index 00000000..a96ccdcb --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/converter/Bytes.kt @@ -0,0 +1,215 @@ +@file:Suppress("MayBeConstant") + +package org.autojs.autojs.runtime.api.augment.converter + +import org.autojs.autojs.annotation.RhinoSingletonFunctionInterface +import org.autojs.autojs.extension.AnyExtensions.isJsBoolean +import org.autojs.autojs.extension.AnyExtensions.isJsNullish +import org.autojs.autojs.extension.AnyExtensions.isJsNumber +import org.autojs.autojs.extension.AnyExtensions.isJsObject +import org.autojs.autojs.extension.AnyExtensions.isJsString +import org.autojs.autojs.extension.AnyExtensions.jsBrief +import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire +import org.autojs.autojs.runtime.api.augment.Augmentable +import org.autojs.autojs.runtime.api.augment.Invokable +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.AUTO +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.DEFAULT_BYTES_STRICT +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.IEC_DIV +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.SI_DIV +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.Tough +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.Tough.LOOSE +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.Tough.NONE +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.Tough.STRICT +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.UNITS +import org.autojs.autojs.util.RhinoUtils.coerceBoolean +import org.mozilla.javascript.ScriptableObject +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes as CoreBytes + +object Bytes : Augmentable(), Invokable { + + override val selfAssignmentProperties = listOf( + "UNITS" to UNITS, + "AUTO" to AUTO, + "IEC_DIV" to IEC_DIV, + "SI_DIV" to SI_DIV, + ) + + override val selfAssignmentFunctions = listOf( + ::strict.name, + ::loose.name, + ) + + override fun invoke(vararg args: Any?): Any = ensureArgumentsLengthInRange(args, 1..4) { call(args) } + + @JvmStatic + @RhinoSingletonFunctionInterface + fun call(args: Array, tough: Tough = NONE): Any = ensureArgumentsLengthInRange(args, 1..4) { argList -> + val (arg0, arg1, arg2, arg3) = argList + + when (argList.size) { + 4 -> when { + arg3.isJsObject() -> { + val opts = arg3 as ScriptableObject + listOf("source", "fromUnit", "toUnit", "options").forBytesNumber( + source = arg0, + fromUnit = opts.inquire("fromUnit", arg1), + toUnit = opts.inquire("toUnit", arg2), + fractionDigits = opts.inquire("fractionDigits"), + autoCarryThreshold = opts.inquire("autoCarryThreshold"), + strict = opts.inquire("strict"), + tough = tough, + ) + } + arg3.isJsBoolean() -> { + listOf("source", "fromUnit", "toUnit", "useIecIdentifier").forBytesNumber( + source = arg0, + fromUnit = arg1, + toUnit = arg2, + strict = tough == STRICT, + ) + } + arg3.isJsNumber() -> { + listOf("source", "fromUnit", "toUnit", "fractionDigits").forBytesNumber( + source = arg0, + fromUnit = arg1, + toUnit = arg2, + fractionDigits = arg3, + strict = tough == STRICT, + ) + } + else -> throw IllegalArgumentException("Invalid argument[3] ${arg3.jsBrief()} for ${Converter.key}.bytes") + } + 3 -> when { + arg2.isJsObject() -> { + val opts = arg2 as ScriptableObject + listOf("source", "toUnit", "options").forBytesNumber( + source = arg0, + fromUnit = opts.inquire("fromUnit"), + toUnit = opts.inquire("toUnit", arg1), + fractionDigits = opts.inquire("fractionDigits"), + autoCarryThreshold = opts.inquire("autoCarryThreshold"), + strict = opts.inquire("strict"), + tough = tough, + ) + } + arg2.isJsString() -> { + listOf("source", "fromUnit", "toUnit").forBytesNumber( + source = arg0, + fromUnit = arg1, + toUnit = arg2, + strict = tough == STRICT, + ) + } + arg2.isJsBoolean() -> { + listOf("source", "toUnit", "useIecIdentifier").forBytesNumber( + source = arg0, + toUnit = arg1, + strict = tough == STRICT, + ) + } + arg2.isJsNumber() -> { + listOf("source", "toUnit", "fractionDigits").forBytesNumber( + source = arg0, + toUnit = arg1, + fractionDigits = arg2, + strict = tough == STRICT, + ) + } + else -> throw IllegalArgumentException("Invalid argument[2] ${arg2.jsBrief()} for ${Converter.key}.bytes") + } + 2 -> when { + arg1.isJsObject() -> { + val opts = arg1 as ScriptableObject + listOf("source", "options").forBytesNumber( + source = arg0, + fromUnit = opts.inquire("fromUnit"), + toUnit = opts.inquire("toUnit"), + fractionDigits = opts.inquire("fractionDigits"), + autoCarryThreshold = opts.inquire("autoCarryThreshold"), + strict = opts.inquire("strict"), + tough = tough, + ) + } + arg1.isJsString() -> { + listOf("source", "toUnit").forBytesNumber( + source = arg0, + toUnit = arg1, + strict = tough == STRICT, + ) + } + arg1.isJsBoolean() -> { + listOf("source", "useIecIdentifier").forBytesNumber( + source = arg0, + strict = tough == STRICT, + ) + } + arg1.isJsNumber() -> { + listOf("source", "fractionDigits").forBytesNumber( + source = arg0, + fractionDigits = arg1, + strict = tough == STRICT, + ) + } + else -> throw IllegalArgumentException("Invalid argument[1] ${arg1.jsBrief()} for ${Converter.key}.bytes") + } + 1 -> listOf("source").forBytesNumber( + source = arg0, + strict = tough == STRICT, + ) + else -> throw IllegalArgumentException("Invalid arguments length ${argList.size} for ${Converter.key}.bytes") + } + } + + @JvmStatic + @RhinoSingletonFunctionInterface + fun strict(args: Array): Any = ensureArgumentsLengthInRange(args, 1..4) { + call(args, STRICT) + } + + @JvmStatic + @RhinoSingletonFunctionInterface + fun loose(args: Array): Any = ensureArgumentsLengthInRange(args, 1..4) { + call(args, LOOSE) + } + + private fun List.forBytesNumber( + source: Any? = null, + fromUnit: Any? = null, + toUnit: Any? = null, + fractionDigits: Any? = null, + autoCarryThreshold: Any? = null, + strict: Any? = null, + tough: Tough = NONE, + ): Any { + val (niceStrict, signature) = when (tough) { + STRICT -> { + val signature = "${Converter.key}.bytes.strict(${this.joinToString(", ")})" + require(strict.isJsNullish()) { + "Option \"strict\" ${strict.jsBrief()} must be nullish when in strict mode for $signature" + } + true to signature + } + LOOSE -> { + val signature = "${Converter.key}.bytes.loose(${this.joinToString(", ")})" + require(strict.isJsNullish()) { + "Option \"strict\" ${strict.jsBrief()} must be nullish when in loose mode for $signature" + } + false to signature + } + NONE -> { + val signature = "${Converter.key}.bytes(${this.joinToString(", ")})" + coerceBoolean(strict, DEFAULT_BYTES_STRICT) to signature + } + } + return CoreBytes.numberRhino( + source, + fromUnit, + toUnit, + fractionDigits, + autoCarryThreshold, + niceStrict, + signature, + ) + } + +} diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/converter/Converter.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/converter/Converter.kt new file mode 100644 index 00000000..ae4976dc --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/converter/Converter.kt @@ -0,0 +1,9 @@ +package org.autojs.autojs.runtime.api.augment.converter + +import org.autojs.autojs.runtime.api.augment.Augmentable + +object Converter : Augmentable() { + + override val key = "cvt" + +} diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/converter/core/Bytes.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/converter/core/Bytes.kt new file mode 100644 index 00000000..333f726f --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/converter/core/Bytes.kt @@ -0,0 +1,455 @@ +@file:Suppress("MayBeConstant") + +package org.autojs.autojs.runtime.api.augment.converter.core + +import org.autojs.autojs.extension.AnyExtensions.isJsNullish +import org.autojs.autojs.extension.AnyExtensions.isJsNumber +import org.autojs.autojs.extension.AnyExtensions.jsBrief +import org.autojs.autojs.util.RhinoUtils.MAX_SAFE_INT_IEEE754_BD +import org.autojs.autojs.util.RhinoUtils.MIN_SAFE_INT_IEEE754_BD +import org.autojs.autojs.util.RhinoUtils.coerceBoolean +import org.autojs.autojs.util.RhinoUtils.coerceIntNumber +import org.autojs.autojs.util.RhinoUtils.coerceLongNumber +import org.autojs.autojs.util.RhinoUtils.coerceNumber +import org.autojs.autojs.util.RhinoUtils.coerceString +import org.mozilla.javascript.Context +import java.math.BigDecimal +import java.math.BigInteger +import java.math.RoundingMode +import kotlin.Double.Companion.NaN +import kotlin.text.RegexOption.IGNORE_CASE + +object Bytes { + + // @formatter:off + /** + * ```md + * | Value | IEC | Term | + * |---------|-----|--------------------| + * | 1024^1 | KiB | Kibibyte (Kilo) | + * | 1024^2 | MiB | Mebibyte (Mega) | + * | 1024^3 | GiB | Gibibyte (Giga) | + * | 1024^4 | TiB | Tebibyte (Tera) | + * | 1024^5 | PiB | Pebibyte (Peta) | + * | 1024^6 | EiB | Exbibyte (Exa) | + * | 1024^7 | ZiB | Zebibyte (Zetta) | + * | 1024^8 | YiB | Yobibyte (Yotta) | + * | 1024^9 | RiB | Robibyte (Ronna) | + * | 1024^10 | QiB | Quebibyte (Quetta) | + * ``` + */ + @Suppress("SpellCheckingInspection") + @JvmField val UNITS = "KMGTPEZYRQ" + @JvmField val AUTO = "AUTO" + + @JvmField val IEC_DIV = 1024L + @JvmField val SI_DIV = 1000L + + @JvmField val DEFAULT_BYTES_FROM_UNIT = "B" + @JvmField val DEFAULT_BYTES_TO_UNIT = AUTO + @JvmField val DEFAULT_BYTES_USE_IEC_IDENTIFIER = false + @JvmField val DEFAULT_BYTES_USE_IEC_IDENTIFIER_FOR_STRICT = true + @JvmField val DEFAULT_BYTES_USE_SPACE = true + @JvmField val DEFAULT_BYTES_FRACTION_DIGITS = 2 + @JvmField val DEFAULT_BYTES_TRIM_TRAILING_ZERO = false + @JvmField val DEFAULT_BYTES_AUTO_CARRY_THRESHOLD = IEC_DIV + @JvmField val DEFAULT_BYTES_STRICT = false + // @formatter:on + + private val BI_IEC = BigInteger.valueOf(IEC_DIV) + private val BI_SI = BigInteger.valueOf(SI_DIV) + private val POW_IEC = Array(UNITS.length + 1) { i -> BI_IEC.pow(i) } + private val POW_SI = Array(UNITS.length + 1) { i -> BI_SI.pow(i) } + + @JvmStatic + fun numberRhino( + source: Any? = null, /* Double */ + fromUnit: Any? = null, /* String */ + toUnit: Any? = null, /* String */ + fractionDigits: Any? = null, /* Int */ + autoCarryThreshold: Any? = null, /* Double */ + strict: Any? = null, /* Boolean */ + signature: String? = null, + ): Any { + val r = NumberRetrievalHandler(source, fromUnit, toUnit, fractionDigits, autoCarryThreshold, strict, signature).retrieve() + return number( + r.niceSource, r.niceFromUnit, r.niceToUnit, r.niceFractionDigits, r.niceAutoCarryThreshold, r.niceStrict, signature ?: "bytes.number", + ).toJavaScriptBigIntOrNumber() + } + + @JvmStatic + @JvmOverloads + fun number( + source: Double, + fromUnit: String = DEFAULT_BYTES_FROM_UNIT, + toUnit: String = DEFAULT_BYTES_TO_UNIT, + fractionDigits: Int = DEFAULT_BYTES_FRACTION_DIGITS, + autoCarryThreshold: Long = DEFAULT_BYTES_AUTO_CARRY_THRESHOLD, + strict: Boolean = DEFAULT_BYTES_STRICT, + signature: String = "Bytes.number", + ) = parseArguments(source, fromUnit, toUnit, fractionDigits, autoCarryThreshold, strict, signature).let { (bytesBD, baseBI, toUnit) -> + computeNumberValue(bytesBD, baseBI, toUnit, fractionDigits, autoCarryThreshold, strict, signature) + } + + @JvmStatic + fun stringRhino( + source: Any? = null, + fromUnit: Any? = null, + toUnit: Any? = null, + useIecIdentifier: Any? = null, + useSpace: Any? = null, + fractionDigits: Any? = null, + trimTrailingZero: Any? = null, + autoCarryThreshold: Any? = null, + strict: Any? = null, + signature: String? = null, + ): String { + val r = StringRetrievalHandler(source, fromUnit, toUnit, useIecIdentifier, useSpace, fractionDigits, trimTrailingZero, autoCarryThreshold, strict, signature).retrieve() + return string(r.niceSource, r.niceFromUnit, r.niceToUnit, r.niceUseIecIdentifier, r.niceUseSpace, r.niceFractionDigits, r.niceTrimTrailingZero, r.niceAutoCarryThreshold, r.niceStrict, signature ?: "bytes.string") + } + + @JvmStatic + @JvmOverloads + fun string( + source: Double, + fromUnit: String = DEFAULT_BYTES_FROM_UNIT, + toUnit: String = DEFAULT_BYTES_TO_UNIT, + useIecIdentifier: Boolean? = null, + useSpace: Boolean = DEFAULT_BYTES_USE_SPACE, + fractionDigits: Int = DEFAULT_BYTES_FRACTION_DIGITS, + trimTrailingZero: Boolean = DEFAULT_BYTES_TRIM_TRAILING_ZERO, + autoCarryThreshold: Long = DEFAULT_BYTES_AUTO_CARRY_THRESHOLD, + strict: Boolean = DEFAULT_BYTES_STRICT, + signature: String = "Bytes.string", + ): String { + val (bytesBD, baseBI, niceToUnit) = parseArguments(source, fromUnit, toUnit, fractionDigits, autoCarryThreshold, strict, signature) + val bd = computeNumberValue(bytesBD, baseBI, niceToUnit, fractionDigits, autoCarryThreshold, strict, signature) + val bdFormatted = bd.setScale(fractionDigits, RoundingMode.HALF_UP).toPlainString().let { + if (!trimTrailingZero) it else it.replace(Regex("(\\.\\d*?)0+$"), "$1").removeSuffix(".") + } + val withIec = useIecIdentifier ?: when { + strict -> DEFAULT_BYTES_USE_IEC_IDENTIFIER_FOR_STRICT + else -> DEFAULT_BYTES_USE_IEC_IDENTIFIER + } + val space = if (useSpace) " " else "" + + return when { + strict -> when (niceToUnit) { + AUTO -> { + val (_, core) = autoPickBig(bytesBD, baseBI, BigDecimal.valueOf(autoCarryThreshold)) + val suffix = if (core.isEmpty()) "B" else "${core}iB" + "$bdFormatted$space$suffix" + } + else -> { + val coreRaw = niceToUnit.uppercase().removeSuffix("B") + val isIEC = coreRaw.endsWith("I") + when (val core = if (isIEC) coreRaw.removeSuffix("I") else coreRaw) { + "" -> "$bdFormatted${space}B" + else -> { + val idx = UNITS.indexOf(core) + require(idx != -1) { "Argument \"toUnit\" ${toUnit.jsBrief()} is invalid${signature.toSignatureSuffix()}" } + when { + isIEC -> "$bdFormatted${space}${core}iB" + else -> "$bdFormatted${space}${core}B" + } + } + } + } + } + else -> when (niceToUnit) { + AUTO -> { + val (_, core) = autoPickBig(bytesBD, BI_IEC, BigDecimal.valueOf(autoCarryThreshold)) + val suffix = if (core.isEmpty()) "B" else if (withIec) "${core}iB" else "${core}B" + "$bdFormatted$space$suffix" + } + else -> when (val core = niceToUnit.removeSuffix("B")) { + "" -> "$bdFormatted${space}B" + else -> { + val idx = UNITS.indexOf(core) + require(idx != -1) { "Argument \"toUnit\" ${toUnit.jsBrief()} is invalid${signature.toSignatureSuffix()}" } + val suffix = if (withIec) "${core}iB" else "${core}B" + "$bdFormatted$space$suffix" + } + } + } + } + } + + private fun parseArguments( + source: Double, + fromUnit: String = DEFAULT_BYTES_FROM_UNIT, + toUnit: String = DEFAULT_BYTES_TO_UNIT, + fractionDigits: Int = DEFAULT_BYTES_FRACTION_DIGITS, + autoCarryThreshold: Long = DEFAULT_BYTES_AUTO_CARRY_THRESHOLD, + strict: Boolean = DEFAULT_BYTES_STRICT, + signature: String, + ): UnitConversionDetails { + val signatureSuffix = signature.toSignatureSuffix() + + require(!source.isNaN() && source >= 0.0) { + "Argument \"source\" ${source.jsBrief()} must be non-negative number$signatureSuffix" + } + + require(fractionDigits >= 0) { + "Argument \"fractionDigits\" ${fractionDigits.jsBrief()} must be non-negative$signatureSuffix" + } + + val niceToUnit = when { + strict -> toUnit.trim().uppercase() + else -> toUnit.toSiUnit() + }.takeUnless { it.isBlank() } ?: DEFAULT_BYTES_TO_UNIT + + require(autoCarryThreshold > 0) { + "Argument \"autoCarryThreshold\" ${autoCarryThreshold.jsBrief()} must be a positive finite number$signatureSuffix" + } + require(autoCarryThreshold == DEFAULT_BYTES_AUTO_CARRY_THRESHOLD || niceToUnit.equals(AUTO, ignoreCase = true)) { + "Argument \"autoCarryThreshold\" is only allowed when argument \"toUnit\" is \"$AUTO\"$signatureSuffix" + } + + val (bytesBD, baseBI) = run parseExactBytes@{ + val valueBD = BigDecimal.valueOf(source) + val niceFromUnit = when { + strict -> fromUnit.trim() + else -> fromUnit.toSiUnit() + }.takeUnless { it.isBlank() }?.uppercase() ?: DEFAULT_BYTES_FROM_UNIT + val coreRaw = when { + niceFromUnit.endsWith("B") -> niceFromUnit.dropLast(1) + else -> niceFromUnit + } + val isIec = strict && coreRaw.endsWith("I") + val core = if (isIec) coreRaw.removeSuffix("I") else coreRaw + val base = if (!strict) BI_IEC else if (isIec) BI_IEC else BI_SI + val idx = if (core.isEmpty()) -1 else UNITS.indexOf(core) + require(core.isEmpty() || idx != -1) { "Argument \"fromUnit\" ${fromUnit.jsBrief()} is invalid$signatureSuffix" } + val factor = if (idx == -1) BigInteger.ONE else powBI(base, idx + 1) + valueBD.multiply(BigDecimal(factor)) to base + } + + return UnitConversionDetails(bytesBD, baseBI, niceToUnit) + } + + // Calculate the order of AUTO (avoiding errors from ln/Double), determine whether to carry early (autoCarryThreshold) + // zh-CN: 求 AUTO 的阶 (避免 ln/Double 带来的误差), 决策是否提前进位 (autoCarryThreshold). + private fun autoPickBig(bytes: BigDecimal, base: BigInteger, carryThreshold: BigDecimal): Pair { + if (bytes.compareTo(BigDecimal.ZERO) == 0) return 0 to "" + var exp = 0 + val core = fun() = UNITS.substring((exp - 1).coerceAtLeast(0), exp) + // Find max exp such that bytes >= base^exp. + // zh-CN: 找到最大的 exp 使 bytes >= base^exp. + for (i in 1..UNITS.length) { + val th = BigDecimal(powBI(base, i)) + if (bytes >= th) exp = i else break + } + if (exp > 0) { + val currentVal = bytes.divide(BigDecimal(powBI(base, exp)), DEFAULT_BYTES_FRACTION_DIGITS + 4, RoundingMode.HALF_UP) + if (currentVal >= carryThreshold && exp < UNITS.length) { + exp += 1 + return exp to core() + } + } + return exp to core() + } + + private fun parseSource(source: Any?, fromUnit: Any?, strict: Boolean, signatureSuffix: String): Pair { + var bytes: Double + var parsedUnitRaw = "" + when { + source.isJsNumber() -> { + val d = coerceNumber(source, NaN) + require(!d.isNaN() && d >= 0.0) { "Argument \"source\" ${source.jsBrief()} must be non-negative number for $signatureSuffix" } + bytes = d + } + else -> { + val s = coerceString(source, "0").trim() + val m = Regex("^(\\d+(?:\\.\\d+)?)\\s*([A-Za-z]*)$", IGNORE_CASE).find(s) + ?: throw IllegalArgumentException("Invalid bytes value: \"$source\"") + bytes = try { + Context.toNumber(m.groupValues[1]) + } catch (_: Throwable) { + throw IllegalArgumentException("Invalid bytes value: \"${m.groupValues[1]}\"") + } + parsedUnitRaw = m.groupValues[2] + } + } + val niceFromUnit = run parseFromUnit@{ + val incoming = coerceString(fromUnit, "").let { if (strict) it.trim() else it.toSiUnit() } + val parsed = if (strict) parsedUnitRaw.trim() else parsedUnitRaw.toSiUnit() + when { + incoming.isBlank() -> parsed + parsed.isBlank() -> incoming + incoming.equals(parsed, ignoreCase = !strict) -> incoming + else -> throw IllegalArgumentException("Ambiguous \"fromUnit\" values: [ $incoming, $parsed ]$signatureSuffix") + }.ifBlank { DEFAULT_BYTES_FROM_UNIT }.uppercase() + } + return bytes to niceFromUnit + } + + private fun powBI(base: BigInteger, exp: Int): BigInteger = when { + base == BI_IEC && exp in 0..UNITS.length -> POW_IEC[exp] + base == BI_SI && exp in 0..UNITS.length -> POW_SI[exp] + else -> base.pow(exp) + } + + private fun computeNumberValue( + bytesBD: BigDecimal, + baseBI: BigInteger, + toUnit: String, + fractionDigits: Int, + autoCarryThreshold: Long, + strict: Boolean, + signature: String, + ): BigDecimal = when { + strict -> when (toUnit) { + AUTO -> { + val (exp, _) = autoPickBig(bytesBD, baseBI, BigDecimal.valueOf(autoCarryThreshold)) + val value = if (exp == 0) bytesBD else bytesBD.divide(BigDecimal(powBI(baseBI, exp)), fractionDigits, RoundingMode.HALF_UP) + value.stripTrailingZeros() + } + else -> { + val coreRaw = toUnit.uppercase().removeSuffix("B") + val isIEC = coreRaw.endsWith("I") + when (val core = if (isIEC) coreRaw.removeSuffix("I") else coreRaw) { + "" -> bytesBD.stripTrailingZeros() + else -> { + val idx = UNITS.indexOf(core) + require(idx != -1) { "Argument \"toUnit\" ${toUnit.jsBrief()} is invalid${signature.toSignatureSuffix()}" } + val base = if (isIEC) BI_IEC else BI_SI + val value = bytesBD.divide(BigDecimal(powBI(base, idx + 1)), fractionDigits, RoundingMode.HALF_UP) + value.stripTrailingZeros() + } + } + } + } + else -> when (toUnit) { + AUTO -> { + val (exp, _) = autoPickBig(bytesBD, BI_IEC, BigDecimal.valueOf(autoCarryThreshold)) + val value = if (exp == 0) bytesBD else bytesBD.divide(BigDecimal(powBI(BI_IEC, exp)), fractionDigits, RoundingMode.HALF_UP) + value.stripTrailingZeros() + } + else -> when (val core = toUnit.removeSuffix("B")) { + "" -> bytesBD + else -> { + val idx = UNITS.indexOf(core) + require(idx != -1) { "Argument \"toUnit\" ${toUnit.jsBrief()} is invalid${signature.toSignatureSuffix()}" } + val value = bytesBD.divide(BigDecimal(powBI(BI_IEC, idx + 1)), fractionDigits, RoundingMode.HALF_UP) + value.stripTrailingZeros() + } + } + } + } + + private fun String.toSiUnit() = this.trim().uppercase().replace(Regex("I(?=B$)"), "") + + private fun String.toSignatureSuffix() = this.takeUnless { it.isBlank() }?.let { " for $it" } ?: "" + + private fun BigDecimal.toJavaScriptBigIntOrNumber(): Any = when (this) { + in MIN_SAFE_INT_IEEE754_BD..MAX_SAFE_INT_IEEE754_BD -> { + this.toDouble() + } + else -> this + } + + private interface RetrievalHandler { + fun retrieve(): Any + } + + private open class NumberRetrievalHandler( + private val source: Any? = null, /* Double */ + private val fromUnit: Any? = null, /* String */ + private val toUnit: Any? = null, /* String */ + private val fractionDigits: Any? = null, /* Int */ + private val autoCarryThreshold: Any? = null, /* Double */ + private val strict: Any? = null, /* Boolean */ + private val signature: String? = null, + ) : RetrievalHandler { + + override fun retrieve(): NumberRhinoStandardizationDetails { + val signatureSuffix = signature?.toSignatureSuffix() ?: "" + val niceStrict = coerceBoolean(strict, DEFAULT_BYTES_STRICT) + val niceToUnit = when { + niceStrict -> coerceString(toUnit, DEFAULT_BYTES_TO_UNIT).trim().uppercase() + else -> coerceString(toUnit, DEFAULT_BYTES_TO_UNIT).toSiUnit() + }.takeIf { it.isNotEmpty() } ?: DEFAULT_BYTES_TO_UNIT + + val (niceSource, niceFromUnit) = parseSource(source, fromUnit, niceStrict, signatureSuffix) + + val niceFractionDigits = coerceIntNumber(fractionDigits, DEFAULT_BYTES_FRACTION_DIGITS).also { + require(it >= 0) { "Argument \"fractionDigits\" ${fractionDigits.jsBrief()} must be non-negative$signatureSuffix" } + } + + require(autoCarryThreshold.isJsNullish() || niceToUnit.equals(AUTO, ignoreCase = true)) { + "Option \"autoCarryThreshold\" is only allowed when argument \"toUnit\" is \"$AUTO\"$signatureSuffix" + } + val niceAutoCarryThreshold = coerceLongNumber(autoCarryThreshold, DEFAULT_BYTES_AUTO_CARRY_THRESHOLD).also { + require(it > 0) { + "Option \"autoCarryThreshold\" ${autoCarryThreshold.jsBrief()} must be a positive finite number$signatureSuffix" + } + } + return NumberRhinoStandardizationDetails(niceSource, niceFromUnit, niceToUnit, niceFractionDigits, niceAutoCarryThreshold, niceStrict) + } + + } + + private class StringRetrievalHandler( + source: Any?, + fromUnit: Any?, + toUnit: Any?, + private val useIecIdentifier: Any?, + private val useSpace: Any?, + fractionDigits: Any?, + private val trimTrailingZero: Any?, + autoCarryThreshold: Any?, + strict: Any?, + private val signature: String? = null, + ) : NumberRetrievalHandler(source, fromUnit, toUnit, fractionDigits, autoCarryThreshold, strict, signature) { + + override fun retrieve(): StringRhinoStandardizationDetails { + val sp = super.retrieve() + + val niceSource = sp.niceSource + val niceFromUnit = sp.niceFromUnit + val niceToUnit = sp.niceToUnit + val niceFractionDigits = sp.niceFractionDigits + val niceAutoCarryThreshold = sp.niceAutoCarryThreshold + val niceStrict = sp.niceStrict + val signatureSuffix = signature?.toSignatureSuffix() ?: "" + + require(!niceStrict || useIecIdentifier.isJsNullish()) { + "Argument \"useIecIdentifier\" ${useIecIdentifier.jsBrief()} must be nullish when in strict mode$signatureSuffix" + } + + val niceUseSpace = coerceBoolean(useSpace, DEFAULT_BYTES_USE_SPACE) + + val niceTrimTrailingZero = coerceBoolean(trimTrailingZero, DEFAULT_BYTES_TRIM_TRAILING_ZERO) + val niceUseIecIdentifier = if (useIecIdentifier.isJsNullish()) null else Context.toBoolean(useIecIdentifier) + + return StringRhinoStandardizationDetails(niceSource, niceFromUnit, niceToUnit, niceUseIecIdentifier, niceUseSpace, niceFractionDigits, niceTrimTrailingZero, niceAutoCarryThreshold, niceStrict) + } + } + + private data class UnitConversionDetails(val bytesBD: BigDecimal, val baseBI: BigInteger, val toUnit: String) + + private open class NumberRhinoStandardizationDetails( + open val niceSource: Double, + open val niceFromUnit: String, + open val niceToUnit: String, + open val niceFractionDigits: Int, + open val niceAutoCarryThreshold: Long, + open val niceStrict: Boolean, + ) + + private class StringRhinoStandardizationDetails( + override val niceSource: Double, + override val niceFromUnit: String, + override val niceToUnit: String, + val niceUseIecIdentifier: Boolean?, + val niceUseSpace: Boolean, + override val niceFractionDigits: Int, + val niceTrimTrailingZero: Boolean, + override val niceAutoCarryThreshold: Long, + override val niceStrict: Boolean, + ) : NumberRhinoStandardizationDetails(niceSource, niceFromUnit, niceToUnit, niceFractionDigits, niceAutoCarryThreshold, niceStrict) + + enum class Tough { STRICT, LOOSE, NONE } + +} diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/formatter/Bytes.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/formatter/Bytes.kt new file mode 100644 index 00000000..9bc9ab8e --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/formatter/Bytes.kt @@ -0,0 +1,233 @@ +@file:Suppress("MayBeConstant") + +package org.autojs.autojs.runtime.api.augment.formatter + +import org.autojs.autojs.annotation.RhinoSingletonFunctionInterface +import org.autojs.autojs.extension.AnyExtensions.isJsBoolean +import org.autojs.autojs.extension.AnyExtensions.isJsNullish +import org.autojs.autojs.extension.AnyExtensions.isJsNumber +import org.autojs.autojs.extension.AnyExtensions.isJsObject +import org.autojs.autojs.extension.AnyExtensions.isJsString +import org.autojs.autojs.extension.AnyExtensions.jsBrief +import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire +import org.autojs.autojs.runtime.api.augment.Augmentable +import org.autojs.autojs.runtime.api.augment.Invokable +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.AUTO +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.DEFAULT_BYTES_STRICT +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.IEC_DIV +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.SI_DIV +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.Tough +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.Tough.LOOSE +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.Tough.NONE +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.Tough.STRICT +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes.UNITS +import org.autojs.autojs.util.RhinoUtils.coerceBoolean +import org.mozilla.javascript.ScriptableObject +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes as CoreBytes + +object Bytes : Augmentable(), Invokable { + + override val selfAssignmentProperties = listOf( + "UNITS" to UNITS, + "AUTO" to AUTO, + "IEC_DIV" to IEC_DIV, + "SI_DIV" to SI_DIV, + ) + + override val selfAssignmentFunctions = listOf( + ::strict.name, + ::loose.name, + ) + + override fun invoke(vararg args: Any?): String = ensureArgumentsLengthInRange(args, 1..4) { call(args) } + + @JvmStatic + @RhinoSingletonFunctionInterface + fun call(args: Array, tough: Tough = NONE): String = ensureArgumentsLengthInRange(args, 1..4) { argList -> + val (arg0, arg1, arg2, arg3) = argList + + when (argList.size) { + 4 -> when { + arg3.isJsObject() -> { + val opts = arg3 as ScriptableObject + listOf("source", "fromUnit", "toUnit", "options").forBytesString( + source = arg0, + fromUnit = opts.inquire("fromUnit", arg1), + toUnit = opts.inquire("toUnit", arg2), + useIecIdentifier = opts.inquire("useIecIdentifier"), + useSpace = opts.inquire("useSpace"), + fractionDigits = opts.inquire("fractionDigits"), + trimTrailingZero = opts.inquire("trimTrailingZero"), + autoCarryThreshold = opts.inquire("autoCarryThreshold"), + strict = opts.inquire("strict"), + tough = tough, + ) + } + arg3.isJsBoolean() -> { + listOf("source", "fromUnit", "toUnit", "useIecIdentifier").forBytesString( + source = arg0, + fromUnit = arg1, + toUnit = arg2, + useIecIdentifier = arg3, + strict = tough == STRICT, + ) + } + arg3.isJsNumber() -> { + listOf("source", "fromUnit", "toUnit", "fractionDigits").forBytesString( + source = arg0, + fromUnit = arg1, + toUnit = arg2, + fractionDigits = arg3, + strict = tough == STRICT, + ) + } + else -> throw IllegalArgumentException("Invalid argument[3] ${arg3.jsBrief()} for ${Formatter.key}.bytes") + } + 3 -> when { + arg2.isJsObject() -> { + val opts = arg2 as ScriptableObject + listOf("source", "toUnit", "options").forBytesString( + source = arg0, + fromUnit = opts.inquire("fromUnit"), + toUnit = opts.inquire("toUnit", arg1), + fractionDigits = opts.inquire("fractionDigits"), + useIecIdentifier = opts.inquire("useIecIdentifier"), + useSpace = opts.inquire("useSpace"), + trimTrailingZero = opts.inquire("trimTrailingZero"), + autoCarryThreshold = opts.inquire("autoCarryThreshold"), + strict = opts.inquire("strict"), + tough = tough, + ) + } + arg2.isJsString() -> { + listOf("source", "fromUnit", "toUnit").forBytesString( + source = arg0, + fromUnit = arg1, + toUnit = arg2, + strict = tough == STRICT, + ) + } + arg2.isJsBoolean() -> { + listOf("source", "toUnit", "useIecIdentifier").forBytesString( + source = arg0, + toUnit = arg1, + useIecIdentifier = arg2, + strict = tough == STRICT, + ) + } + arg2.isJsNumber() -> { + listOf("source", "toUnit", "fractionDigits").forBytesString( + source = arg0, + toUnit = arg1, + fractionDigits = arg2, + strict = tough == STRICT, + ) + } + else -> throw IllegalArgumentException("Invalid argument[2] ${arg2.jsBrief()} for ${Formatter.key}.bytes") + } + 2 -> when { + arg1.isJsObject() -> { + val opts = arg1 as ScriptableObject + listOf("source", "options").forBytesString( + source = arg0, + fromUnit = opts.inquire("fromUnit"), + toUnit = opts.inquire("toUnit"), + fractionDigits = opts.inquire("fractionDigits"), + useIecIdentifier = opts.inquire("useIecIdentifier"), + useSpace = opts.inquire("useSpace"), + trimTrailingZero = opts.inquire("trimTrailingZero"), + autoCarryThreshold = opts.inquire("autoCarryThreshold"), + strict = opts.inquire("strict"), + tough = tough, + ) + } + arg1.isJsString() -> { + listOf("source", "toUnit").forBytesString( + source = arg0, + toUnit = arg1, + strict = tough == STRICT, + ) + } + arg1.isJsBoolean() -> { + listOf("source", "useIecIdentifier").forBytesString( + source = arg0, + useIecIdentifier = arg1, + strict = tough == STRICT, + ) + } + arg1.isJsNumber() -> { + listOf("source", "fractionDigits").forBytesString( + source = arg0, + fractionDigits = arg1, + strict = tough == STRICT, + ) + } + else -> throw IllegalArgumentException("Invalid argument[1] ${arg1.jsBrief()} for ${Formatter.key}.bytes") + } + 1 -> listOf("source").forBytesString( + source = arg0, + strict = tough == STRICT, + ) + else -> throw IllegalArgumentException("Invalid arguments length ${argList.size} for ${Formatter.key}.bytes") + } + } + + @JvmStatic + @RhinoSingletonFunctionInterface + fun strict(args: Array): String = ensureArgumentsLengthInRange(args, 1..4) { + call(args, STRICT) + } + + @JvmStatic + @RhinoSingletonFunctionInterface + fun loose(args: Array): String = ensureArgumentsLengthInRange(args, 1..4) { + call(args, LOOSE) + } + + private fun List.forBytesString( + source: Any? = null, + fromUnit: Any? = null, + toUnit: Any? = null, + useIecIdentifier: Any? = null, + useSpace: Any? = null, + fractionDigits: Any? = null, + trimTrailingZero: Any? = null, + autoCarryThreshold: Any? = null, + strict: Any? = null, + tough: Tough = NONE, + ): String { + val (niceStrict, signature) = when (tough) { + STRICT -> { + val signature = "${Formatter.key}.bytes.strict(${this.joinToString(", ")})" + require(strict.isJsNullish()) { + "Option \"strict\" ${strict.jsBrief()} must be nullish when in strict mode for $signature" + } + true to signature + } + LOOSE -> { + val signature = "${Formatter.key}.bytes.loose(${this.joinToString(", ")})" + require(strict.isJsNullish()) { + "Option \"strict\" ${strict.jsBrief()} must be nullish when in loose mode for $signature" + } + false to signature + } + NONE -> { + val signature = "${Formatter.key}.bytes(${this.joinToString(", ")})" + coerceBoolean(strict, DEFAULT_BYTES_STRICT) to signature + } + } + return CoreBytes.stringRhino( + source, + fromUnit, + toUnit, + useIecIdentifier, + useSpace, + fractionDigits, + trimTrailingZero, + autoCarryThreshold, + niceStrict, + signature, + ) + } + +} diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/formatter/Formatter.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/formatter/Formatter.kt new file mode 100644 index 00000000..b49637c2 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/formatter/Formatter.kt @@ -0,0 +1,9 @@ +package org.autojs.autojs.runtime.api.augment.formatter + +import org.autojs.autojs.runtime.api.augment.Augmentable + +object Formatter : Augmentable() { + + override val key = "fmt" + +} diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/s13n/S13n.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/s13n/S13n.kt index d7b5cbc2..dc386ae1 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/s13n/S13n.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/s13n/S13n.kt @@ -1,10 +1,15 @@ +@file:Suppress("MayBeConstant") + package org.autojs.autojs.runtime.api.augment.s13n +import org.autojs.autojs.annotation.RhinoFunctionBody import org.autojs.autojs.annotation.RhinoSingletonFunctionInterface import org.autojs.autojs.extension.AnyExtensions.isJsNullish import org.autojs.autojs.extension.AnyExtensions.isJsObject +import org.autojs.autojs.extension.AnyExtensions.isJsString import org.autojs.autojs.extension.AnyExtensions.jsBrief import org.autojs.autojs.extension.ScriptableExtensions.prop +import org.autojs.autojs.extension.ScriptableObjectExtensions.inquire import org.autojs.autojs.runtime.api.augment.Augmentable import org.autojs.autojs.runtime.api.augment.colors.Colors import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException @@ -25,6 +30,7 @@ import kotlin.math.roundToInt import kotlin.reflect.full.declaredMemberFunctions import kotlin.text.RegexOption.IGNORE_CASE import android.graphics.Point as AndroidPoint +import org.autojs.autojs.runtime.api.augment.converter.core.Bytes as CoreBytes import org.opencv.core.Point as OpencvPoint object S13n : Augmentable() { @@ -60,6 +66,7 @@ object S13n : Augmentable() { ::throwable.name, ::point.name, ::time.name, + ::bytes.name, ) @JvmStatic @@ -139,7 +146,7 @@ object S13n : Augmentable() { } ?: when { arg1.isJsObject() -> { val options = arg1 as ScriptableObject - time(arrayOf(source, options.prop("fromUnit"), options.prop("toUnit"))) + time(arrayOf(source, options.inquire("fromUnit"), options.inquire("toUnit"))) } else -> { val num = coerceNumber(source, NaN).also { require(!it.isNaN()) { "Failed to make ${arg0.jsBrief()} a number time being" } } @@ -159,6 +166,47 @@ object S13n : Augmentable() { } } + @JvmStatic + @RhinoSingletonFunctionInterface + fun bytes(args: Array): Any = ensureArgumentsLengthInRange(args, 1..2) { argList -> + val (arg0, arg1) = argList + + when (argList.size) { + 2 -> when { + arg1.isJsString() -> listOf("source", "fromUnit").forBytes(source = arg0, fromUnit = arg1) + else -> throw WrappedIllegalArgumentException("Invalid argument[1] ${arg1.jsBrief()} for $key.bytes") + } + 1 -> listOf("source").forBytes(source = arg0) + else -> throw WrappedIllegalArgumentException("Invalid arguments length ${argList.size} for $key.bytes") + } + } + + @JvmStatic + @JvmOverloads + @RhinoFunctionBody + fun bytesRhino( + source: Any? = null, /* Double */ + fromUnit: Any? = null, /* String */ + funcSignature: List = emptyList(), + ): Any { + val signature = when { + funcSignature.isNotEmpty() -> "$key.bytes(${funcSignature.joinToString(", ")})" + else -> "$key.bytes()" + } + return CoreBytes.numberRhino( + source = source, + fromUnit = fromUnit, + toUnit = "B", + fractionDigits = 0, + signature = signature, + ) + } + + private fun List.forBytes( + source: Any? = null, /* Double */ + fromUnit: Any? = null, /* String */ + ) = bytesRhino(source, fromUnit, this) + private fun getTimeUnitSourceObject(unit: Any?): TimeUnit = when (unit) { is TimeUnit -> unit is String -> timeUnitRex.entries.find { it.value.matches(unit) }?.let { diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/util/Inspect.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/util/Inspect.kt index 5a1f6248..336a4532 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/util/Inspect.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/util/Inspect.kt @@ -2,11 +2,8 @@ package org.autojs.autojs.runtime.api.augment.util import org.autojs.autojs.annotation.RhinoFunctionBody import org.autojs.autojs.core.automator.UiObjectCollection -import org.autojs.autojs.extension.AnyExtensions.isJsBoolean import org.autojs.autojs.extension.AnyExtensions.isJsNonNullObject import org.autojs.autojs.extension.AnyExtensions.isJsNullish -import org.autojs.autojs.extension.AnyExtensions.isJsNumber -import org.autojs.autojs.extension.AnyExtensions.isJsString import org.autojs.autojs.extension.AnyExtensions.jsBrief import org.autojs.autojs.extension.ArrayExtensions.toNativeObject import org.autojs.autojs.extension.FlexibleArray.Companion.component1 @@ -261,7 +258,13 @@ object Inspect : Augmentable(), Invokable { private fun formatValue(ctx: Ctx, `val`: Any?, recurseTimes: Int?): String { var value = `val` - while (value is Wrapper) value = value.unwrap() + while (value is Wrapper) { + val unwrapped = value.unwrap() + if (unwrapped is Number) { + return callToStringFunction(value as Scriptable) + } + value = unwrapped + } if (value is TopLevel) return "[object ${value.className}]" @@ -579,7 +582,7 @@ object Inspect : Augmentable(), Invokable { // For some reason, typeof null is "object", so special case here. ctx.stylize("null", "null") } - value.isJsString() -> { + value is String -> { val content = Context.toString(js_json_stringify(value)) .removeSurrounding("\"") .replace(Regex("'"), "\\\'") @@ -590,16 +593,15 @@ object Inspect : Augmentable(), Invokable { // ! to make a string quoted by single quotation marks. // ! zh-CN: // ! 我认为将字符串用单引号包裹起来并不是一个好主意. - // ! PS: 我认为不应该翻译为 "我不认为...". :) // ! // # ctx.stylize("'$content'", "string") ctx.stylize(content, "string") } - value.isJsNumber() -> { + value is Number -> { ctx.stylize(value.toString(), "number") } - value.isJsBoolean() -> { + value is Boolean -> { ctx.stylize(Context.toString(value), "boolean") } else -> null diff --git a/app/src/main/java/org/autojs/autojs/util/RhinoUtils.kt b/app/src/main/java/org/autojs/autojs/util/RhinoUtils.kt index cdb3e548..4c7d2e2d 100644 --- a/app/src/main/java/org/autojs/autojs/util/RhinoUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/RhinoUtils.kt @@ -51,6 +51,7 @@ import org.mozilla.javascript.Wrapper import org.mozilla.javascript.json.JsonParser import java.io.Serializable import java.lang.reflect.InvocationTargetException +import java.math.BigInteger import kotlin.math.floor import kotlin.math.roundToInt import kotlin.math.roundToLong @@ -66,6 +67,23 @@ object RhinoUtils { const val DEFAULT_CALLER = 0x03 const val DEFAULT_CONSTRUCTOR = 0x04 + /** [Long]: 2^53 - 1. */ + const val MAX_SAFE_INT_IEEE754_L = 9_007_199_254_740_991L + /** [Long]: -(2^53 - 1). */ + const val MIN_SAFE_INT_IEEE754_L = -9_007_199_254_740_991L + /** [Double]: 2^53 - 1. */ + const val MAX_SAFE_INT_IEEE754_D = 9_007_199_254_740_991.0 + /** [Double]: -(2^53 - 1). */ + const val MIN_SAFE_INT_IEEE754_D = -9_007_199_254_740_991.0 + /** [java.math.BigDecimal]: 2^53 - 1. */ + val MAX_SAFE_INT_IEEE754_BD = MAX_SAFE_INT_IEEE754_D.toBigDecimal() + /** [java.math.BigDecimal]: -(2^53 - 1). */ + val MIN_SAFE_INT_IEEE754_BD = MIN_SAFE_INT_IEEE754_D.toBigDecimal() + /** [java.math.BigInteger]: 2^53 - 1. */ + val MAX_SAFE_INT_IEEE754_BI = MAX_SAFE_INT_IEEE754_L.toBigInteger() + /** [java.math.BigInteger]: -(2^53 - 1). */ + val MIN_SAFE_INT_IEEE754_BI = MIN_SAFE_INT_IEEE754_L.toBigInteger() + private val TAG = RhinoUtils::class.java.simpleName @JvmStatic @@ -274,8 +292,8 @@ object RhinoUtils { @JvmStatic fun unwrap(o: Any?): Any? = when (o) { - is String -> o - is ConsString -> o.toString() + is String, is ConsString -> Context.toString(o) + is BigInteger -> o is Number -> Context.toNumber(o) is Boolean -> Context.toBoolean(o) is Wrapper -> unwrap(o.unwrap())