93 lines
3.9 KiB
PHP
93 lines
3.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace app\adminapi\service;
|
|
|
|
/** Repeated JSON keys only: no text summarization, row filtering or value coercion. */
|
|
final class AiLosslessSourceEncoding
|
|
{
|
|
public const VERSION = 'clinical-row-tables-v1';
|
|
|
|
public static function encode(array $snapshot): string
|
|
{
|
|
$paths = [];
|
|
$data = self::pack($snapshot, [], $paths);
|
|
return json_encode([
|
|
'encoding' => self::VERSION,
|
|
'reading_rule' => 'table_paths指定的节点使用columns和rows:每行各值按columns的同序字段对应。'
|
|
. '这是完整原始记录的无损表示,不是摘要;所有行、日期、数值、原文、null和空项均保留。'
|
|
. '表外对象照常读取;历史记录不能当成本次查体,空项不能当成患者否认或正常。',
|
|
'table_paths' => $paths,
|
|
'data' => $data,
|
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION
|
|
| JSON_INVALID_UTF8_SUBSTITUTE | JSON_THROW_ON_ERROR);
|
|
}
|
|
|
|
/** Reversible verification helper. Table paths also disambiguate literal columns/rows objects. */
|
|
public static function decode(string $encoded): array
|
|
{
|
|
$wire = json_decode($encoded, true, 512, JSON_THROW_ON_ERROR);
|
|
if (($wire['encoding'] ?? null) !== self::VERSION || !is_array($wire['table_paths'] ?? null)
|
|
|| !is_array($wire['data'] ?? null)) {
|
|
throw new \InvalidArgumentException('Invalid clinical row encoding');
|
|
}
|
|
$tables = [];
|
|
foreach ($wire['table_paths'] as $path) {
|
|
if (!is_array($path) || !array_is_list($path)) throw new \InvalidArgumentException('Invalid table path');
|
|
$tables[serialize($path)] = true;
|
|
}
|
|
return self::unpack($wire['data'], [], $tables);
|
|
}
|
|
|
|
private static function pack($node, array $path, array &$paths)
|
|
{
|
|
if (!is_array($node)) return $node;
|
|
if (array_is_list($node) && count($node) > 1 && is_array($node[0]) && !array_is_list($node[0])) {
|
|
$columns = array_keys($node[0]);
|
|
$sameShape = true;
|
|
foreach ($node as $row) {
|
|
if (!is_array($row) || array_keys($row) !== $columns) { $sameShape = false; break; }
|
|
}
|
|
if ($sameShape) {
|
|
$paths[] = $path;
|
|
$rows = [];
|
|
foreach ($node as $r => $row) {
|
|
$values = [];
|
|
foreach (array_values($row) as $c => $value) {
|
|
$values[] = self::pack($value, [...$path, 'rows', $r, $c], $paths);
|
|
}
|
|
$rows[] = $values;
|
|
}
|
|
return ['columns' => $columns, 'rows' => $rows];
|
|
}
|
|
}
|
|
$result = [];
|
|
foreach ($node as $key => $value) $result[$key] = self::pack($value, [...$path, $key], $paths);
|
|
return $result;
|
|
}
|
|
|
|
private static function unpack($node, array $path, array $tables)
|
|
{
|
|
if (!is_array($node)) return $node;
|
|
if (isset($tables[serialize($path)])) {
|
|
if (!is_array($node['columns'] ?? null) || !is_array($node['rows'] ?? null)) {
|
|
throw new \InvalidArgumentException('Invalid clinical row table');
|
|
}
|
|
$result = [];
|
|
foreach ($node['rows'] as $r => $row) {
|
|
if (!is_array($row) || count($row) !== count($node['columns'])) {
|
|
throw new \InvalidArgumentException('Invalid clinical row width');
|
|
}
|
|
$values = [];
|
|
foreach ($row as $c => $value) $values[] = self::unpack($value, [...$path, 'rows', $r, $c], $tables);
|
|
$result[] = array_combine($node['columns'], $values);
|
|
}
|
|
return $result;
|
|
}
|
|
$result = [];
|
|
foreach ($node as $key => $value) $result[$key] = self::unpack($value, [...$path, $key], $tables);
|
|
return $result;
|
|
}
|
|
}
|