mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
refactor(plist): unify number source association
This commit is contained in:
1 parent
b7cae1ad8c
commit
ded6542d93
3 files changed
+112
-35
No files matched your search
@@ -45,32 +45,51 @@ export const createPlistNumberCollector = () => {
|
||||
};
|
||||
};
|
||||
|
||||
/** One association established from source literals, before adapter projection. */
|
||||
type PlistNumberSource =
|
||||
| { readonly kind: "integer"; readonly decimal: string }
|
||||
| { readonly kind: "real" }
|
||||
| { readonly kind: "unknown" };
|
||||
|
||||
/**
|
||||
* Associate a decoded number with its source type. A retained JSON literal can
|
||||
* narrow integer candidates that otherwise collide after numeric rounding.
|
||||
* Missing or incomplete metadata never establishes a source type.
|
||||
*/
|
||||
export const resolvePlistNumberSource = (
|
||||
value: number,
|
||||
literals: PlistNumberLiterals | undefined,
|
||||
sourceDecimal?: string,
|
||||
): PlistNumberSource => {
|
||||
if (literals === undefined || literals.incomplete) return { kind: "unknown" };
|
||||
const decimals = literals.integers.get(value);
|
||||
const real = literals.reals.has(value);
|
||||
const integer =
|
||||
sourceDecimal === undefined
|
||||
? decimals !== undefined
|
||||
: decimals?.has(sourceDecimal) === true;
|
||||
if (real) return { kind: integer ? "unknown" : "real" };
|
||||
if (!integer) return { kind: "unknown" };
|
||||
const decimal =
|
||||
sourceDecimal ?? (decimals?.size === 1 ? [...decimals][0] : undefined);
|
||||
return decimal === undefined
|
||||
? { kind: "unknown" }
|
||||
: { kind: "integer", decimal };
|
||||
};
|
||||
|
||||
/** Preserve unambiguous exact integers; disclose rounded or unclassified numbers. */
|
||||
export const createPlistNumberProjection = (literals: PlistNumberLiterals) => {
|
||||
let exactIntegerCount = 0;
|
||||
let ambiguousNumberCount = 0;
|
||||
const mapNumber = (item: number): JsonValue => {
|
||||
if (Number.isSafeInteger(item) || !Number.isInteger(item)) return item;
|
||||
if (literals.incomplete) {
|
||||
ambiguousNumberCount += 1;
|
||||
return item;
|
||||
}
|
||||
const decimals = literals.integers.get(item);
|
||||
if (decimals === undefined) {
|
||||
if (!literals.reals.has(item)) ambiguousNumberCount += 1;
|
||||
return item;
|
||||
}
|
||||
const [decimal] = decimals;
|
||||
if (
|
||||
decimals.size !== 1 ||
|
||||
decimal === undefined ||
|
||||
literals.reals.has(item)
|
||||
) {
|
||||
ambiguousNumberCount += 1;
|
||||
const source = resolvePlistNumberSource(item, literals);
|
||||
if (source.kind !== "integer") {
|
||||
if (source.kind === "unknown") ambiguousNumberCount += 1;
|
||||
return item;
|
||||
}
|
||||
exactIntegerCount += 1;
|
||||
return { $plist_type: "integer", decimal };
|
||||
return { $plist_type: "integer", decimal: source.decimal };
|
||||
};
|
||||
const map = (item: JsonValue): JsonValue => {
|
||||
if (typeof item === "number") return mapNumber(item);
|
||||
|
||||
@@ -3,6 +3,9 @@ import { buildBinary, type PlistValue } from "plist";
|
||||
import { MAX_JSON_DEPTH, jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import { plistUidValue } from "../../domain/apple/plistUid.js";
|
||||
import { createPlistNumberProjection } from "../../domain/apple/plistNumbers.js";
|
||||
import { projectPlistValue } from "../../domain/apple/plistValue.js";
|
||||
import { parseXmlPropertyList } from "../../domain/propertyListKeys.js";
|
||||
import {
|
||||
decodeBinaryPlistScalars,
|
||||
scanBinaryPlistScalars,
|
||||
@@ -907,3 +910,63 @@ it("retains nested own dictionary names consistently in native JSON, XML and bin
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"integer",
|
||||
"<integer>9007199254740992</integer>",
|
||||
"9007199254740992",
|
||||
{ $plist_type: "integer", decimal: "9007199254740992" },
|
||||
],
|
||||
[
|
||||
"real",
|
||||
"<real>9007199254740992</real>",
|
||||
"9007199254740992",
|
||||
9007199254740992,
|
||||
],
|
||||
[
|
||||
"integer/real collision",
|
||||
"<integer>9007199254740992</integer><real>9007199254740992</real>",
|
||||
"9007199254740992,9007199254740992",
|
||||
9007199254740992,
|
||||
],
|
||||
] as const)(
|
||||
"shares %s association across native JSON, XML and archive projection",
|
||||
(_label, elements, json, expected) => {
|
||||
const xml = `<plist><array>${elements}</array></plist>`;
|
||||
const nativeJson = readPlistJson(`[${json}]`).project(xml);
|
||||
const nativeXml = parsePlistXml(xml);
|
||||
if (!nativeJson.ok) throw nativeJson.error;
|
||||
if (!nativeXml.ok) throw nativeXml.error;
|
||||
const decoded = parseXmlPropertyList(xml);
|
||||
const numbers = createPlistNumberProjection(decoded.numberLiterals);
|
||||
const archive = numbers.project(projectPlistValue(decoded.value).value);
|
||||
const values = json.split(",").map(() => expected);
|
||||
expect(nativeJson.value.value).toEqual(values);
|
||||
expect(nativeXml.value.value).toEqual(values);
|
||||
expect(archive).toEqual(values);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains JSON source spelling when XML integers collide after rounding", () => {
|
||||
const xml =
|
||||
"<plist><array><integer>9007199254740992</integer><integer>9007199254740993</integer></array></plist>";
|
||||
const json = readPlistJson("[9007199254740992,9007199254740993]").project(
|
||||
xml,
|
||||
);
|
||||
const converted = parsePlistXml(xml);
|
||||
if (!json.ok) throw json.error;
|
||||
if (!converted.ok) throw converted.error;
|
||||
expect(json.value.value).toEqual([
|
||||
{ $plist_type: "integer", decimal: "9007199254740992" },
|
||||
{ $plist_type: "integer", decimal: "9007199254740993" },
|
||||
]);
|
||||
expect(converted.value.value).toEqual([9007199254740992, 9007199254740992]);
|
||||
expect(json.value.limitations).toEqual([
|
||||
expect.stringContaining("2 integer value(s)"),
|
||||
]);
|
||||
expect(converted.value.limitations).toEqual([
|
||||
expect.stringContaining("XML conversion"),
|
||||
expect.stringContaining("2 number(s)"),
|
||||
]);
|
||||
});
|
||||
+13
-18
@@ -23,6 +23,7 @@ import { err, ok, type Result } from "../../domain/result.js";
|
||||
import {
|
||||
incompletePlistNumberLimitation,
|
||||
createPlistNumberProjection,
|
||||
resolvePlistNumberSource,
|
||||
type PlistNumberLiterals,
|
||||
} from "../../domain/apple/plistNumbers.js";
|
||||
import {
|
||||
@@ -201,17 +202,17 @@ const projectPlistJsonAttempt = (
|
||||
return negativeZeroReal();
|
||||
}
|
||||
if (item instanceof PlistJsonIntegerLiteral) {
|
||||
const integer =
|
||||
literals?.incomplete === false &&
|
||||
literals.integers.get(item.value)?.has(item.digits) === true;
|
||||
const real =
|
||||
literals?.incomplete === false && literals.reals.has(item.value);
|
||||
if (integer && !real) {
|
||||
const source = resolvePlistNumberSource(
|
||||
item.value,
|
||||
literals,
|
||||
item.digits,
|
||||
);
|
||||
if (source.kind === "integer") {
|
||||
exactIntegerCount += 1;
|
||||
return { $plist_type: "integer", decimal: item.digits };
|
||||
return { $plist_type: "integer", decimal: source.decimal };
|
||||
}
|
||||
// An integral <real> decodes exactly; only its JSON spelling is integral.
|
||||
if (real && !integer) return item.value;
|
||||
if (source.kind === "real") return item.value;
|
||||
ambiguousNumberCount += 1;
|
||||
return item.value;
|
||||
}
|
||||
@@ -332,20 +333,14 @@ const projectPlistXml = (
|
||||
let exactIntegerCount = 0;
|
||||
let ambiguousNumberCount = 0;
|
||||
const value = mapJsonNumbers(projected.value, (item) => {
|
||||
const decimals = literals.integers.get(item);
|
||||
if (decimals === undefined) return item;
|
||||
const [decimal] = decimals;
|
||||
if (
|
||||
literals.incomplete ||
|
||||
decimals.size !== 1 ||
|
||||
decimal === undefined ||
|
||||
literals.reals.has(item)
|
||||
) {
|
||||
if (!literals.integers.has(item)) return item;
|
||||
const source = resolvePlistNumberSource(item, literals);
|
||||
if (source.kind !== "integer") {
|
||||
ambiguousNumberCount += 1;
|
||||
return item;
|
||||
}
|
||||
exactIntegerCount += 1;
|
||||
return { $plist_type: "integer", decimal };
|
||||
return { $plist_type: "integer", decimal: source.decimal };
|
||||
});
|
||||
return ok({
|
||||
value,
|
||||
|
||||
Reference in new issue
Block a user