arxo-codegen 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arxo_codegen/__init__.py +5 -0
- arxo_codegen/__main__.py +7 -0
- arxo_codegen/active-outcomes.json +6837 -0
- arxo_codegen/check.py +158 -0
- arxo_codegen/check_active_canaries.py +262 -0
- arxo_codegen/check_api.py +76 -0
- arxo_codegen/check_conformance.py +414 -0
- arxo_codegen/check_matrix.py +131 -0
- arxo_codegen/check_packages.py +281 -0
- arxo_codegen/check_semantic_probes.py +293 -0
- arxo_codegen/check_xlsx.py +320 -0
- arxo_codegen/consumer_runner.py +316 -0
- arxo_codegen/core_support.py +203 -0
- arxo_codegen/coverage.py +341 -0
- arxo_codegen/deliver.py +180 -0
- arxo_codegen/examples/xlsx/pricing.lawir.json +409 -0
- arxo_codegen/examples/xlsx/pricing.xlsx.json +52 -0
- arxo_codegen/excel_plan.py +602 -0
- arxo_codegen/harness/verify/ci/gates/backends/check_backends.py +1248 -0
- arxo_codegen/harness/verify/ci/gates/backends/l1_fixtures.py +463 -0
- arxo_codegen/inert-nodes.json +62 -0
- arxo_codegen/inert.py +91 -0
- arxo_codegen/inputs.py +59 -0
- arxo_codegen/package.py +565 -0
- arxo_codegen/plan.py +4053 -0
- arxo_codegen/recertify_registry.py +100 -0
- arxo_codegen/runtime-cert.json +5259 -0
- arxo_codegen/runtime_cert.py +233 -0
- arxo_codegen/survey.py +272 -0
- arxo_codegen/target_runner.py +101 -0
- arxo_codegen/targets/bsl/cert.bsl +113 -0
- arxo_codegen/targets/bsl/emit.py +356 -0
- arxo_codegen/targets/bsl/json.bsl +243 -0
- arxo_codegen/targets/bsl/runner.bsl +210 -0
- arxo_codegen/targets/bsl/runtime.bsl +858 -0
- arxo_codegen/targets/cs/Cert.cs +90 -0
- arxo_codegen/targets/cs/Runner.cs +154 -0
- arxo_codegen/targets/cs/Runtime.cs +243 -0
- arxo_codegen/targets/cs/emit.py +223 -0
- arxo_codegen/targets/dart/cert.dart +99 -0
- arxo_codegen/targets/dart/emit.py +206 -0
- arxo_codegen/targets/dart/json.dart +166 -0
- arxo_codegen/targets/dart/runner.dart +154 -0
- arxo_codegen/targets/dart/runtime.dart +293 -0
- arxo_codegen/targets/erl/cert.erl +64 -0
- arxo_codegen/targets/erl/emit.py +216 -0
- arxo_codegen/targets/erl/rt.erl +238 -0
- arxo_codegen/targets/erl/runner.erl +130 -0
- arxo_codegen/targets/ex/cert.exs +62 -0
- arxo_codegen/targets/ex/emit.py +204 -0
- arxo_codegen/targets/ex/runner.exs +124 -0
- arxo_codegen/targets/ex/runtime.ex +227 -0
- arxo_codegen/targets/go/apiBase.go +483 -0
- arxo_codegen/targets/go/apicheck.go +97 -0
- arxo_codegen/targets/go/cert.go +218 -0
- arxo_codegen/targets/go/emit.py +1806 -0
- arxo_codegen/targets/go/evaluate.go +1826 -0
- arxo_codegen/targets/go/runner.go +38 -0
- arxo_codegen/targets/go/runtime.go +2612 -0
- arxo_codegen/targets/java/Cert.java +91 -0
- arxo_codegen/targets/java/Json.java +148 -0
- arxo_codegen/targets/java/Runner.java +151 -0
- arxo_codegen/targets/java/Runtime.java +243 -0
- arxo_codegen/targets/java/emit.py +198 -0
- arxo_codegen/targets/kt/Cert.kt +67 -0
- arxo_codegen/targets/kt/Json.kt +103 -0
- arxo_codegen/targets/kt/Runner.kt +126 -0
- arxo_codegen/targets/kt/Runtime.kt +196 -0
- arxo_codegen/targets/kt/emit.py +195 -0
- arxo_codegen/targets/lua/cert.lua +82 -0
- arxo_codegen/targets/lua/emit.py +250 -0
- arxo_codegen/targets/lua/json.lua +133 -0
- arxo_codegen/targets/lua/runner.lua +167 -0
- arxo_codegen/targets/lua/runtime.lua +541 -0
- arxo_codegen/targets/php/cert.php +84 -0
- arxo_codegen/targets/php/emit.py +229 -0
- arxo_codegen/targets/php/json.php +144 -0
- arxo_codegen/targets/php/runner.php +189 -0
- arxo_codegen/targets/php/runtime.php +660 -0
- arxo_codegen/targets/py/api_base.py +334 -0
- arxo_codegen/targets/py/apicheck.py +51 -0
- arxo_codegen/targets/py/cert.py +140 -0
- arxo_codegen/targets/py/emit.py +1140 -0
- arxo_codegen/targets/py/evaluate.py +980 -0
- arxo_codegen/targets/py/runner.py +38 -0
- arxo_codegen/targets/py/runtime.py +1634 -0
- arxo_codegen/targets/rb/cert.rb +73 -0
- arxo_codegen/targets/rb/emit.py +203 -0
- arxo_codegen/targets/rb/runner.rb +130 -0
- arxo_codegen/targets/rb/runtime.rb +255 -0
- arxo_codegen/targets/rs/Cargo.lock +142 -0
- arxo_codegen/targets/rs/Cargo.toml +16 -0
- arxo_codegen/targets/rs/cert.rs +106 -0
- arxo_codegen/targets/rs/emit.py +233 -0
- arxo_codegen/targets/rs/prefetch.rs +1 -0
- arxo_codegen/targets/rs/runner.rs +181 -0
- arxo_codegen/targets/rs/runtime.rs +448 -0
- arxo_codegen/targets/scala/Cert.scala +77 -0
- arxo_codegen/targets/scala/Json.scala +140 -0
- arxo_codegen/targets/scala/Runner.scala +121 -0
- arxo_codegen/targets/scala/Runtime.scala +269 -0
- arxo_codegen/targets/scala/emit.py +193 -0
- arxo_codegen/targets/swift/cert.swift +105 -0
- arxo_codegen/targets/swift/emit.py +231 -0
- arxo_codegen/targets/swift/json.swift +208 -0
- arxo_codegen/targets/swift/runner.swift +133 -0
- arxo_codegen/targets/swift/runtime.swift +528 -0
- arxo_codegen/targets/ts/apiBase.ts +250 -0
- arxo_codegen/targets/ts/apicheck.ts +37 -0
- arxo_codegen/targets/ts/cert.ts +96 -0
- arxo_codegen/targets/ts/emit.py +1214 -0
- arxo_codegen/targets/ts/evaluate.ts +894 -0
- arxo_codegen/targets/ts/package-lock.json +45 -0
- arxo_codegen/targets/ts/package.json +9 -0
- arxo_codegen/targets/ts/runner.ts +31 -0
- arxo_codegen/targets/ts/runtime.ts +1369 -0
- arxo_codegen/targets/xlsx/README.ru.md +176 -0
- arxo_codegen/targets/xlsx/emit.py +198 -0
- arxo_codegen/temporal.py +59 -0
- arxo_codegen/test_alias_types.py +171 -0
- arxo_codegen/test_api_contract.py +89 -0
- arxo_codegen/test_calendar_policy.py +48 -0
- arxo_codegen/test_check.py +49 -0
- arxo_codegen/test_check_active_canaries.py +96 -0
- arxo_codegen/test_check_conformance.py +132 -0
- arxo_codegen/test_check_semantic_probes.py +85 -0
- arxo_codegen/test_check_xlsx.py +52 -0
- arxo_codegen/test_consumer_runner.py +175 -0
- arxo_codegen/test_core_support.py +80 -0
- arxo_codegen/test_e0212_decision_time.py +63 -0
- arxo_codegen/test_head_aggregate.py +71 -0
- arxo_codegen/test_inert.py +143 -0
- arxo_codegen/test_instant_types.py +113 -0
- arxo_codegen/test_key_conflict_scope.py +92 -0
- arxo_codegen/test_key_conflict_targets.py +17 -0
- arxo_codegen/test_package_layout.py +163 -0
- arxo_codegen/test_plan_language_version.py +44 -0
- arxo_codegen/test_positions_refusal.py +43 -0
- arxo_codegen/test_recertify_registry.py +77 -0
- arxo_codegen/test_ruby_target.py +41 -0
- arxo_codegen/test_runtime_cert.py +83 -0
- arxo_codegen/test_secondary_key_conflict_scope.py +17 -0
- arxo_codegen/test_stage_r.py +108 -0
- arxo_codegen/test_standalone_inputs.py +71 -0
- arxo_codegen/test_support.py +150 -0
- arxo_codegen/test_survey_inputs.py +115 -0
- arxo_codegen-0.4.0.dist-info/LICENSE +202 -0
- arxo_codegen-0.4.0.dist-info/METADATA +1673 -0
- arxo_codegen-0.4.0.dist-info/RECORD +152 -0
- arxo_codegen-0.4.0.dist-info/WHEEL +5 -0
- arxo_codegen-0.4.0.dist-info/entry_points.txt +13 -0
- arxo_codegen-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# Харнесс сертификации рантайма: исполняет runtime-cert.json и печатает,
|
|
3
|
+
# что получилось. Ожидания сверяют ворота, харнесс их не читает.
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "runtime"
|
|
6
|
+
|
|
7
|
+
def lit(x)
|
|
8
|
+
return x if x.is_a?(String)
|
|
9
|
+
return RT.dec(x["dec"]) if x.key?("dec")
|
|
10
|
+
return RT.money(x["money"], x["cur"]) if x.key?("money")
|
|
11
|
+
return x["text"] if x.key?("text")
|
|
12
|
+
return RT.date(x["date"]) if x.key?("date")
|
|
13
|
+
return RT.sym(x["sym"]) if x.key?("sym")
|
|
14
|
+
return x["row"].map { |v| lit(v) } if x.key?("row")
|
|
15
|
+
raise "литерал фикстуры: #{x}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Отображение с ЯВНЫМ масштабом: 2.00 и 2 — разные записи одного значения.
|
|
19
|
+
def digits(u, s)
|
|
20
|
+
neg = u.negative?
|
|
21
|
+
body = u.abs.to_s.rjust(s + 1, "0")
|
|
22
|
+
text = s.zero? ? body : body[0...-s] + "." + body[-s..]
|
|
23
|
+
(neg ? "-" : "") + text
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render(v)
|
|
27
|
+
case v
|
|
28
|
+
when nil, true, false, String then v
|
|
29
|
+
when RT::Money then digits(v.u, v.s) + " " + v.cur
|
|
30
|
+
when RT::Date then v.v
|
|
31
|
+
when RT::Sym then v.id
|
|
32
|
+
else digits(v.u, v.s)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run(c)
|
|
37
|
+
a = c["args"].map { |x| lit(x) }
|
|
38
|
+
case c["op"]
|
|
39
|
+
when "dec" then RT.dec(a[0])
|
|
40
|
+
when "money" then RT.money(a[0], a[1])
|
|
41
|
+
when "date" then RT.date(a[0])
|
|
42
|
+
when "sym" then RT.sym(a[0])
|
|
43
|
+
when "mul" then RT.mul(a[0], a[1])
|
|
44
|
+
when "add" then RT.add(a[0], a[1])
|
|
45
|
+
when "sub" then RT.sub(a[0], a[1])
|
|
46
|
+
when "div" then RT.div(a[0], a[1])
|
|
47
|
+
when "mod" then RT.mod(a[0], a[1])
|
|
48
|
+
when "min" then RT.min(a[0], a[1])
|
|
49
|
+
when "max" then RT.max(a[0], a[1])
|
|
50
|
+
when "abs" then RT.abs(a[0])
|
|
51
|
+
when "pow" then RT.pow(a[0], a[1])
|
|
52
|
+
when "round" then RT.round(a[0], a[1], a[2])
|
|
53
|
+
when "cmp" then RT.cmp(a[0], a[1], a[2])
|
|
54
|
+
when "eq" then RT.eq(a[0], a[1])
|
|
55
|
+
when "keyEq" then RT.key(a[0]) == RT.key(a[1])
|
|
56
|
+
when "legalTime" then RT.legal_time
|
|
57
|
+
when "setLegalTime"
|
|
58
|
+
RT.set_legal_time(a[0])
|
|
59
|
+
nil
|
|
60
|
+
else raise "неизвестная операция фикстуры: #{c['op']}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
fixture = JSON.parse(File.read(ARGV[0], encoding: "utf-8"))
|
|
65
|
+
out = fixture["cases"].map do |c|
|
|
66
|
+
got = begin
|
|
67
|
+
render(run(c))
|
|
68
|
+
rescue RT::Refused => e
|
|
69
|
+
{"refused" => e.message}
|
|
70
|
+
end
|
|
71
|
+
{"id" => c["id"], "got" => got}
|
|
72
|
+
end
|
|
73
|
+
puts JSON.generate(out)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Эмиттер Ruby. Тот же план, другой синтаксис и тот же контракт рантайма."""
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
# Версия плана, которую этот эмиттер умеет печатать. Другая версия — отказ:
|
|
11
|
+
# незнакомое поле плана печаталось бы молча, а это другой ответ, не меньший.
|
|
12
|
+
SUPPORTED_PLAN = "law-backend-plan/0.7"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_plan(plan: dict) -> None:
|
|
16
|
+
v = plan.get("planVersion")
|
|
17
|
+
if v != SUPPORTED_PLAN:
|
|
18
|
+
raise ValueError(f"версия плана не поддержана эмиттером: {v!r} (поддержана {SUPPORTED_PLAN})")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def term(t: dict) -> str:
|
|
22
|
+
k = t["k"]
|
|
23
|
+
if k == "var":
|
|
24
|
+
return f'r{t["row"]}[{t["col"]}]'
|
|
25
|
+
if k == "lit":
|
|
26
|
+
if t["t"] == "money":
|
|
27
|
+
return f'RT.money("{t["v"]}", "{t["cur"]}")'
|
|
28
|
+
if t["t"] == "dec":
|
|
29
|
+
return f'RT.dec("{t["v"]}")'
|
|
30
|
+
if t["t"] == "date":
|
|
31
|
+
return f'RT.date("{t["v"]}")'
|
|
32
|
+
if t["t"] == "enum":
|
|
33
|
+
return f'RT.sym("{t["v"]}")'
|
|
34
|
+
return json.dumps(t["v"], ensure_ascii=False)
|
|
35
|
+
if k == "bin":
|
|
36
|
+
return f'RT.{t["op"]}({term(t["l"])}, {term(t["r"])})'
|
|
37
|
+
if k == "call":
|
|
38
|
+
return f'RT.{t["fn"]}(' + ", ".join(term(a) for a in t["args"]) + ')'
|
|
39
|
+
raise AssertionError(k)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def emit(plan: dict) -> str:
|
|
43
|
+
check_plan(plan)
|
|
44
|
+
L = ['require_relative "runtime"', "", "module Model", " TABLES = {"]
|
|
45
|
+
for t in plan["tables"]:
|
|
46
|
+
L.append(f' {json.dumps(t["pred"])} => [],')
|
|
47
|
+
L.append(" }.freeze")
|
|
48
|
+
readers: dict[str, set[str]] = {}
|
|
49
|
+
for rule in plan["rules"]:
|
|
50
|
+
for atom in rule.get("atoms") or []:
|
|
51
|
+
readers.setdefault(atom["pred"], set()).add(rule["head"]["pred"])
|
|
52
|
+
for check in rule.get("checks") or []:
|
|
53
|
+
readers.setdefault(check["pred"], set()).add(rule["head"]["pred"])
|
|
54
|
+
L.append(" # E-0210/E-0242: forward readers of a keyed relation.")
|
|
55
|
+
L.append(" KEY_READERS = " + repr({key: sorted(value) for key, value in sorted(readers.items())}).replace(": ", " => ") + ".freeze")
|
|
56
|
+
L.append(" # Объявленные типы параметров: раннер сверяет с ними аргументы дела (§189).")
|
|
57
|
+
L.append(" PARAMS = {")
|
|
58
|
+
for t in plan["tables"]:
|
|
59
|
+
L.append(f' {json.dumps(t["pred"])} => {json.dumps(t["params"], ensure_ascii=False)},')
|
|
60
|
+
L.append(" }.freeze")
|
|
61
|
+
L.append(" # Константы §54: тип для проверки §189 и значение для разыменования аргументов дела.")
|
|
62
|
+
L.append(" CONSTS = {")
|
|
63
|
+
for cid, c in plan["consts"].items():
|
|
64
|
+
L.append(f' {json.dumps(cid)} => [{json.dumps(c["type"])}, {term(c["term"])}],')
|
|
65
|
+
L.append(" }.freeze")
|
|
66
|
+
L.append(" # Наследование сущностей: подтип подходит параметру базового типа.")
|
|
67
|
+
L.append(" BASES = " + json.dumps(plan["bases"], ensure_ascii=False).replace(": ", " => ") + ".freeze")
|
|
68
|
+
L.append(" # Ключи отношений §45.1: позиции столбцов каждого ключа.")
|
|
69
|
+
L.append(" KEYS = {")
|
|
70
|
+
for t in plan["tables"]:
|
|
71
|
+
if t["keys"]:
|
|
72
|
+
L.append(f' {json.dumps(t["pred"])} => {json.dumps(t["keys"])},')
|
|
73
|
+
L.append(" }.freeze")
|
|
74
|
+
L.append(" @seen = {}")
|
|
75
|
+
L.append("")
|
|
76
|
+
L.append(" def self.add_row(urn, row)")
|
|
77
|
+
L.append(" k = [urn, RT.key(row)]")
|
|
78
|
+
L.append(" return false if @seen[k]")
|
|
79
|
+
L.append(" @seen[k] = true")
|
|
80
|
+
L.append(" TABLES[urn] << row")
|
|
81
|
+
L.append(" true")
|
|
82
|
+
L.append(" end")
|
|
83
|
+
L.append("")
|
|
84
|
+
for r in plan["rules"]:
|
|
85
|
+
L.append(f' # {r["id"]}')
|
|
86
|
+
L.append(f' def self.rule_{r["name"]}')
|
|
87
|
+
L.append(" grew = false")
|
|
88
|
+
if r["window"]:
|
|
89
|
+
if r["window"].get("start"):
|
|
90
|
+
L.append(f' return false if RT.legal_time < "{r["window"]["start"]}"')
|
|
91
|
+
if r["window"].get("end"):
|
|
92
|
+
L.append(f' return false if RT.legal_time >= "{r["window"]["end"]}"')
|
|
93
|
+
ind = " "
|
|
94
|
+
for i, a in enumerate(r["atoms"]):
|
|
95
|
+
L.append(f'{ind}TABLES[{json.dumps(a["pred"])}].dup.each do |r{i}|')
|
|
96
|
+
ind += " "
|
|
97
|
+
for (ai, aj, _at), (bi, bj, _bt) in r["joins"]:
|
|
98
|
+
L.append(f'{ind}next unless RT.cmp(r{ai}[{aj}], "eq", r{bi}[{bj}])')
|
|
99
|
+
for f in r["filters"]:
|
|
100
|
+
L.append(f'{ind}next unless RT.eq(r{f["row"]}[{f["col"]}], {term(f["term"])})')
|
|
101
|
+
for g in r["guards"]:
|
|
102
|
+
L.append(f'{ind}next unless RT.cmp({term(g["l"])}, "{g["op"]}", {term(g["r"])})')
|
|
103
|
+
args = ", ".join(term(a) for a in r["head"]["args"])
|
|
104
|
+
L.append(f'{ind}grew = true if add_row({json.dumps(r["head"]["pred"])}, [{args}])')
|
|
105
|
+
for _ in r["atoms"]:
|
|
106
|
+
ind = ind[:-2]
|
|
107
|
+
L.append(ind + "end")
|
|
108
|
+
L.append(" grew")
|
|
109
|
+
L.append(" end")
|
|
110
|
+
L.append("")
|
|
111
|
+
names = [r["name"] for r in plan["rules"]]
|
|
112
|
+
L.append(" def self.saturate")
|
|
113
|
+
L.append(" grew = true")
|
|
114
|
+
L.append(" while grew")
|
|
115
|
+
if names:
|
|
116
|
+
L.append(" grew = [" + ", ".join(f"rule_{n}" for n in names) + "].any?")
|
|
117
|
+
else:
|
|
118
|
+
L.append(" grew = false")
|
|
119
|
+
L.append(" end")
|
|
120
|
+
L.append(" end")
|
|
121
|
+
L.append("")
|
|
122
|
+
L.append(" def self.register(urn, row)")
|
|
123
|
+
L.append(" add_row(urn, row)")
|
|
124
|
+
L.append(" end")
|
|
125
|
+
L.append("")
|
|
126
|
+
L.append(" # KEY_CONFLICT §45.1/§196: группы строк одной таблицы с одинаковой проекцией на ключ; строки уникальны по ключу строки, поэтому группа из двух и более строк — различные accepted-кортежи. Считается после насыщения.")
|
|
127
|
+
L.append(" def self.key_conflicts")
|
|
128
|
+
L.append(" n = 0")
|
|
129
|
+
L.append(" KEYS.each do |urn, keys|")
|
|
130
|
+
L.append(" keys.each do |key|")
|
|
131
|
+
L.append(" groups = Hash.new(0)")
|
|
132
|
+
L.append(" TABLES[urn].each { |row| groups[RT.key(key.map { |i| row[i] })] += 1 }")
|
|
133
|
+
L.append(" n += groups.values.count { |c| c > 1 }")
|
|
134
|
+
L.append(" end")
|
|
135
|
+
L.append(" end")
|
|
136
|
+
L.append(" n")
|
|
137
|
+
L.append(" end")
|
|
138
|
+
L.append("")
|
|
139
|
+
L.append(" def self.key_conflict_predicates")
|
|
140
|
+
L.append(" out = {}")
|
|
141
|
+
L.append(" KEYS.each do |urn, keys|")
|
|
142
|
+
L.append(" keys.each do |key|")
|
|
143
|
+
L.append(" groups = Hash.new(0)")
|
|
144
|
+
L.append(" TABLES[urn].each { |row| groups[RT.key(key.map { |i| row[i] })] += 1 }")
|
|
145
|
+
L.append(" out[urn] = true if groups.values.any? { |c| c > 1 }")
|
|
146
|
+
L.append(" end")
|
|
147
|
+
L.append(" end")
|
|
148
|
+
L.append(" out.keys")
|
|
149
|
+
L.append(" end")
|
|
150
|
+
L.append("")
|
|
151
|
+
for f in plan["facts"]:
|
|
152
|
+
L.append(f' add_row({json.dumps(f["pred"])}, [' + ", ".join(term(a) for a in f["args"]) + '])')
|
|
153
|
+
L.append("end")
|
|
154
|
+
return "\n".join(L) + "\n"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# --- дескриптор цели ---------------------------------------------------------
|
|
158
|
+
HERE = Path(__file__).resolve().parent
|
|
159
|
+
EXT = "rb"
|
|
160
|
+
MODEL = "model.rb"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def available():
|
|
164
|
+
rb = shutil.which("ruby")
|
|
165
|
+
if not rb:
|
|
166
|
+
return False, "нет ruby в PATH"
|
|
167
|
+
try:
|
|
168
|
+
version = subprocess.check_output([rb, "--version"], text=True, stderr=subprocess.STDOUT).strip()
|
|
169
|
+
except (OSError, subprocess.CalledProcessError) as exc:
|
|
170
|
+
return False, f"ruby не запускается: {exc}"
|
|
171
|
+
match = re.search(r"\bruby\s+(\d+)\.(\d+)", version, flags=re.IGNORECASE)
|
|
172
|
+
if match is None:
|
|
173
|
+
return False, f"не прочитана версия ruby: {version[:120]}"
|
|
174
|
+
if tuple(map(int, match.groups())) < (3, 0):
|
|
175
|
+
return False, f"Ruby >= 3.0 нужен для синтаксиса рантайма (найден {match.group(0)})"
|
|
176
|
+
return True, f"{rb} ({match.group(0)})"
|
|
177
|
+
|
|
178
|
+
MAINS = {"runner": "runner.rb", "cert": "cert.rb"}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def layout(proj, main: str) -> list:
|
|
182
|
+
proj = Path(proj)
|
|
183
|
+
proj.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
out = []
|
|
185
|
+
for name in ("runtime.rb", MAINS[main]):
|
|
186
|
+
shutil.copyfile(HERE / name, proj / name)
|
|
187
|
+
out.append(proj / name)
|
|
188
|
+
return out
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def build(proj):
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def run(proj, main: str):
|
|
196
|
+
return ["ruby", str(Path(proj) / MAINS[main])], None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
sys.path.insert(0, str(HERE.parents[1]))
|
|
201
|
+
import plan as P
|
|
202
|
+
ir = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
203
|
+
Path(sys.argv[2]).write_text(emit(P.build_plan(ir)), encoding="utf-8")
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# Раннер Ruby-бэкенда: тот же вход и тот же ответ, что у runner.py.
|
|
3
|
+
#
|
|
4
|
+
# Протокол выхода общий для всех целей: ответ — JSON и код 0; названный отказ
|
|
5
|
+
# среза — JSON {"refused": текст} и код 3, текст побайтово одинаков между
|
|
6
|
+
# целями; любой другой ненулевой код — крах, который ворота валят.
|
|
7
|
+
require "json"
|
|
8
|
+
require_relative "runtime"
|
|
9
|
+
require_relative "model"
|
|
10
|
+
|
|
11
|
+
ISSUES = []
|
|
12
|
+
|
|
13
|
+
def arg(a)
|
|
14
|
+
kind = a["kind"]
|
|
15
|
+
return a["id"] if kind == "entity_ref"
|
|
16
|
+
if kind == "const_ref"
|
|
17
|
+
# §54: разыменование предшествует исполнению; член перечисления §38 либо
|
|
18
|
+
# имя без объявления остаётся идентичностью, как в ключе атома оракула.
|
|
19
|
+
c = Model::CONSTS[a["id"]]
|
|
20
|
+
return c.nil? ? RT.sym(a["id"].to_s) : c[1]
|
|
21
|
+
end
|
|
22
|
+
raise RT::Refused, "аргумент вне среза раннера: #{kind}" unless kind == "value"
|
|
23
|
+
t = (a["type"] || {})["name"]
|
|
24
|
+
case t
|
|
25
|
+
when "urn:law:std#Money" then RT.money(a["value"].to_s, a["currency"])
|
|
26
|
+
when "urn:law:std#Decimal", "urn:law:std#Integer" then RT.dec(a["value"].to_s)
|
|
27
|
+
when "urn:law:std#Text" then a["value"].to_s
|
|
28
|
+
when "urn:law:std#Date" then RT.date(a["value"].to_s)
|
|
29
|
+
else raise RT::Refused, "аргумент вне среза раннера: #{t}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Тип аргумента, если известен: у value — из узла, у const_ref — из объявления
|
|
34
|
+
# константы; entity_ref типа не несёт (как у оракула).
|
|
35
|
+
def arg_type(a)
|
|
36
|
+
return (a["type"] || {})["name"] if a["kind"] == "value"
|
|
37
|
+
if a["kind"] == "const_ref"
|
|
38
|
+
c = Model::CONSTS[a["id"]]
|
|
39
|
+
return c.nil? ? nil : c[0]
|
|
40
|
+
end
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Совпадение типа с наследованием сущностей вверх от фактического.
|
|
45
|
+
def conforms(actual, expected)
|
|
46
|
+
seen = {}
|
|
47
|
+
cur = actual
|
|
48
|
+
while !cur.nil? && !seen[cur]
|
|
49
|
+
return true if cur == expected
|
|
50
|
+
seen[cur] = true
|
|
51
|
+
cur = Model::BASES[cur]
|
|
52
|
+
end
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Арность и типы аргументов утверждения против сигнатуры §189 (E-0046).
|
|
57
|
+
def well_typed(lit)
|
|
58
|
+
params = Model::PARAMS[lit["predicate"]]
|
|
59
|
+
return true if params.nil?
|
|
60
|
+
args = lit["args"] || []
|
|
61
|
+
if args.length != params.length
|
|
62
|
+
ISSUES << "TYPE_ERROR"
|
|
63
|
+
return false
|
|
64
|
+
end
|
|
65
|
+
params.zip(args).each do |expected, a|
|
|
66
|
+
actual = arg_type(a)
|
|
67
|
+
next if actual.nil? || conforms(actual, expected)
|
|
68
|
+
ISSUES << "TYPE_ERROR"
|
|
69
|
+
return false
|
|
70
|
+
end
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Ключ атома у оракула §208 включает ТИП значения: несогласованный запрос
|
|
75
|
+
# атома не находит — NEITHER без issue.
|
|
76
|
+
def query_typed(lit)
|
|
77
|
+
params = Model::PARAMS[lit["predicate"]]
|
|
78
|
+
return false if params.nil?
|
|
79
|
+
args = lit["args"] || []
|
|
80
|
+
return false if args.length != params.length
|
|
81
|
+
params.zip(args).all? { |expected, a| arg_type(a).nil? || conforms(arg_type(a), expected) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def key_conflict_blocks(predicate)
|
|
85
|
+
# E-0210/E-0242: only the forward closure of a conflicted relation blocks.
|
|
86
|
+
tainted = {}
|
|
87
|
+
Model.key_conflict_predicates.each { |source| tainted[source] = true }
|
|
88
|
+
waiting = tainted.keys
|
|
89
|
+
until waiting.empty?
|
|
90
|
+
source = waiting.pop
|
|
91
|
+
(Model::KEY_READERS[source] || []).each do |consumer|
|
|
92
|
+
next if tainted[consumer]
|
|
93
|
+
tainted[consumer] = true
|
|
94
|
+
waiting << consumer
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
tainted.key?(predicate)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def main
|
|
101
|
+
kase = JSON.parse(File.read(ARGV[0], encoding: "utf-8"))
|
|
102
|
+
query = JSON.parse(File.read(ARGV[1], encoding: "utf-8"))
|
|
103
|
+
RT.set_legal_time(((kase["context"] || {})["legalTime"] || "").to_s)
|
|
104
|
+
kase["assertions"].each do |a|
|
|
105
|
+
lit = a["literal"]
|
|
106
|
+
raise RT::Refused, "§113: отрицательный факт вне L0-среза" unless lit["polarity"] == "positive"
|
|
107
|
+
next unless well_typed(lit)
|
|
108
|
+
next unless Model::PARAMS.key?(lit["predicate"])
|
|
109
|
+
Model.register(lit["predicate"], lit["args"].map { |x| arg(x) })
|
|
110
|
+
end
|
|
111
|
+
Model.saturate
|
|
112
|
+
Model.key_conflicts.times { ISSUES << "KEY_CONFLICT" }
|
|
113
|
+
raise RT::Refused, "вид запроса вне среза раннера: #{query['kind']}" unless query["kind"] == "truth"
|
|
114
|
+
hit = false
|
|
115
|
+
if query_typed(query["literal"])
|
|
116
|
+
want = query["literal"]["args"].map { |x| arg(x) }
|
|
117
|
+
rows = Model::TABLES[query["literal"]["predicate"]] || []
|
|
118
|
+
hit = rows.any? { |r| r.length == want.length && r.each_with_index.all? { |v, i| RT.eq(v, want[i]) } }
|
|
119
|
+
end
|
|
120
|
+
# Открытый мир: отсутствие строки — это NEITHER, а не FALSE_ONLY.
|
|
121
|
+
puts JSON.generate({"queryId" => query["queryId"], "truthStatus" => hit ? "TRUE_ONLY" : "NEITHER",
|
|
122
|
+
"evaluationStatus" => key_conflict_blocks(query["literal"]["predicate"]) ? "NON_EXECUTABLE" : "COMPUTED", "issues" => ISSUES})
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
begin
|
|
126
|
+
main
|
|
127
|
+
rescue RT::Refused => e
|
|
128
|
+
puts JSON.generate({"refused" => e.message})
|
|
129
|
+
exit 3
|
|
130
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Рантайм L0 для Ruby. Тот же контракт, что у runtime.py: точная десятичная
|
|
2
|
+
# арифметика §58 на встроенных целых, деньги с валютой, названный режим
|
|
3
|
+
# округления, календарный день, член перечисления.
|
|
4
|
+
module RT
|
|
5
|
+
# Названный отказ рантайма: то, что срез не считает, а не сбой. Раннер
|
|
6
|
+
# печатает его как JSON с кодом выхода 3; любое другое исключение — крах.
|
|
7
|
+
class Refused < StandardError; end
|
|
8
|
+
|
|
9
|
+
Dec = Struct.new(:u, :s)
|
|
10
|
+
Money = Struct.new(:u, :s, :cur)
|
|
11
|
+
# Календарный день §2.15 в канонической записи ISO YYYY-MM-DD: порядок
|
|
12
|
+
# записи и есть порядок дня. Не String: у ключа атома и у сравнения свой вид.
|
|
13
|
+
Date = Struct.new(:v)
|
|
14
|
+
# Член перечисления §38 либо иная ссылка const_ref без объявления: значение —
|
|
15
|
+
# stable qualified identity, равенство по ней (§57), порядок не определён.
|
|
16
|
+
Sym = Struct.new(:id)
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def dec(lit)
|
|
21
|
+
lit = lit.to_s
|
|
22
|
+
i, f = lit.split(".", 2)
|
|
23
|
+
f ||= ""
|
|
24
|
+
neg = i.start_with?("-")
|
|
25
|
+
digits = (neg ? i[1..] : i) + f
|
|
26
|
+
Dec.new(Integer((neg ? "-" : "") + digits, 10), f.length)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def money(lit, cur)
|
|
30
|
+
d = dec(lit)
|
|
31
|
+
Money.new(d.u, d.s, cur)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def date(lit) = Date.new(lit.to_s)
|
|
35
|
+
def sym(id) = Sym.new(id.to_s)
|
|
36
|
+
|
|
37
|
+
def num?(v) = v.is_a?(Dec) || v.is_a?(Money)
|
|
38
|
+
def money?(v) = v.is_a?(Money)
|
|
39
|
+
|
|
40
|
+
# §48/§58: сложение, вычитание, min/max требуют совпадения видов; у денег — и валюты.
|
|
41
|
+
def same!(a, b, op)
|
|
42
|
+
raise Refused, "§58: виды операндов не совпадают: #{op}" unless num?(a) && num?(b) && money?(a) == money?(b)
|
|
43
|
+
raise Refused, "§58: разные валюты: #{op}" if money?(a) && a.cur != b.cur
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def pair(a, b)
|
|
47
|
+
s = [a.s, b.s].max
|
|
48
|
+
[a.u * 10**(s - a.s), b.u * 10**(s - b.s), s]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def make(u, s, like) = money?(like) ? Money.new(u, s, like.cur) : Dec.new(u, s)
|
|
52
|
+
|
|
53
|
+
# Целое значение: дробная часть нулевая (защита в глубину под статикой плана).
|
|
54
|
+
def integral?(v) = v.is_a?(Dec) && (v.u % 10**v.s).zero?
|
|
55
|
+
def to_int(v) = v.u / 10**v.s
|
|
56
|
+
|
|
57
|
+
def add(a, b)
|
|
58
|
+
same!(a, b, "add")
|
|
59
|
+
x, y, s = pair(a, b)
|
|
60
|
+
make(x + y, s, a)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def sub(a, b)
|
|
64
|
+
same!(a, b, "sub")
|
|
65
|
+
x, y, s = pair(a, b)
|
|
66
|
+
make(x - y, s, a)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Правило скаляра §49: безразмерное масштабирует размерное; масштаб результата —
|
|
70
|
+
# сумма масштабов. Два размерных не перемножаются.
|
|
71
|
+
def mul(a, b)
|
|
72
|
+
raise Refused, "§58: виды операндов не совпадают: mul" unless num?(a) && num?(b)
|
|
73
|
+
raise Refused, "§58: произведение двух размерных величин не определено" if money?(a) && money?(b)
|
|
74
|
+
make(a.u * b.u, a.s + b.s, money?(a) ? a : b)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Точное десятичное частное num/den при 10-гладком знаменателе (§18), иначе nil.
|
|
78
|
+
def exact(num, den)
|
|
79
|
+
g = num.gcd(den)
|
|
80
|
+
g = 1 if g.zero?
|
|
81
|
+
num /= g
|
|
82
|
+
den /= g
|
|
83
|
+
if den.negative?
|
|
84
|
+
num = -num
|
|
85
|
+
den = -den
|
|
86
|
+
end
|
|
87
|
+
twos = fives = 0
|
|
88
|
+
while (den % 2).zero?
|
|
89
|
+
den /= 2
|
|
90
|
+
twos += 1
|
|
91
|
+
end
|
|
92
|
+
while (den % 5).zero?
|
|
93
|
+
den /= 5
|
|
94
|
+
fives += 1
|
|
95
|
+
end
|
|
96
|
+
return nil unless den == 1
|
|
97
|
+
k = [twos, fives].max
|
|
98
|
+
[num * 2**(k - twos) * 5**(k - fives), k]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Таблица делимых §58 в срезе: Decimal/Decimal, Money/Decimal, Money/Money одной
|
|
102
|
+
# валюты. Integer/Integer даёт Rational — вне среза, отвергается планом статически.
|
|
103
|
+
def div(a, b)
|
|
104
|
+
raise Refused, "§58: деление вне таблицы делимых" unless num?(a) && num?(b)
|
|
105
|
+
raise Refused, "деление на ноль — evaluation error (§58)" if b.u.zero?
|
|
106
|
+
like = nil
|
|
107
|
+
if money?(a) && money?(b)
|
|
108
|
+
raise Refused, "CURRENCY_MISMATCH: §58 делит Money на Money только при тождественной валюте" if a.cur != b.cur
|
|
109
|
+
elsif money?(a)
|
|
110
|
+
like = a
|
|
111
|
+
elsif money?(b)
|
|
112
|
+
raise Refused, "§58: деление вне таблицы делимых"
|
|
113
|
+
end
|
|
114
|
+
q = exact(a.u * 10**b.s, b.u * 10**a.s)
|
|
115
|
+
if q.nil?
|
|
116
|
+
raise Refused, "INEXACT_DIVISION: округление только явной политикой (§50/§18)" unless like.nil?
|
|
117
|
+
raise Refused, "§58: частное не 10-гладкое — Rational вне среза"
|
|
118
|
+
end
|
|
119
|
+
like.nil? ? Dec.new(q[0], q[1]) : Money.new(q[0], q[1], like.cur)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Евклидов остаток §58: 0 <= r < |b| при любых знаках операндов.
|
|
123
|
+
def mod(a, b)
|
|
124
|
+
raise Refused, "§58: mod определён только для Integer" unless integral?(a) && integral?(b)
|
|
125
|
+
y = to_int(b)
|
|
126
|
+
raise Refused, "остаток от деления на ноль — evaluation error (§58)" if y.zero?
|
|
127
|
+
Dec.new(to_int(a) % y.abs, 0)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def min(a, b)
|
|
131
|
+
same!(a, b, "min")
|
|
132
|
+
x, y, = pair(a, b)
|
|
133
|
+
x <= y ? a : b
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def max(a, b)
|
|
137
|
+
same!(a, b, "max")
|
|
138
|
+
x, y, = pair(a, b)
|
|
139
|
+
x >= y ? a : b
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def abs(v)
|
|
143
|
+
raise Refused, "§58: abs — не числовой вид" unless num?(v)
|
|
144
|
+
make(v.u.abs, v.s, v)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
POW_EXP_LIMIT = 4096
|
|
148
|
+
POW_MIN = -(2**63)
|
|
149
|
+
POW_MAX = 2**63 - 1
|
|
150
|
+
|
|
151
|
+
# §58 (E-0075): только Integer pow Integer; порядок проверок — знак, предел,
|
|
152
|
+
# вычисление, диапазон — часть контракта.
|
|
153
|
+
def pow(base, exp)
|
|
154
|
+
raise Refused, "§58: pow определён только для Integer" unless integral?(base) && integral?(exp)
|
|
155
|
+
e = to_int(exp)
|
|
156
|
+
raise Refused, "§58: pow — показатель отрицателен" if e.negative?
|
|
157
|
+
raise Refused, "RESOURCE_LIMIT: pow — показатель превышает 4096" if e > POW_EXP_LIMIT
|
|
158
|
+
r = to_int(base)**e
|
|
159
|
+
raise Refused, "RESOURCE_LIMIT: pow — результат вне 64-битного диапазона" if r < POW_MIN || r > POW_MAX
|
|
160
|
+
Dec.new(r, 0)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def round(m, digits, mode)
|
|
164
|
+
raise Refused, "§50: режим округления не реализован: #{mode}" unless mode == "HALF_UP"
|
|
165
|
+
raise Refused, "§50: round — не числовой вид" unless num?(m)
|
|
166
|
+
n = digits.is_a?(Integer) ? digits : digits.u / 10**digits.s
|
|
167
|
+
return make(m.u * 10**(n - m.s), n, m) if m.s <= n
|
|
168
|
+
drop = 10**(m.s - n)
|
|
169
|
+
neg = m.u.negative?
|
|
170
|
+
a = m.u.abs
|
|
171
|
+
q, r = a.divmod(drop)
|
|
172
|
+
up = r * 2 >= drop ? q + 1 : q # HALF_UP — от нуля
|
|
173
|
+
make(neg ? -up : up, n, m)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
ORDER = {"lt" => ->(x, y) { x < y }, "le" => ->(x, y) { x <= y }, "gt" => ->(x, y) { x > y },
|
|
177
|
+
"ge" => ->(x, y) { x >= y }, "eq" => ->(x, y) { x == y }, "ne" => ->(x, y) { x != y }}.freeze
|
|
178
|
+
|
|
179
|
+
def cmp(a, op, b)
|
|
180
|
+
if a.is_a?(Sym) || b.is_a?(Sym)
|
|
181
|
+
same_sym = a.is_a?(Sym) && b.is_a?(Sym) && a.id == b.id
|
|
182
|
+
return same_sym if op == "eq"
|
|
183
|
+
return !same_sym if op == "ne"
|
|
184
|
+
raise Refused, "§57: вид enum не упорядочен"
|
|
185
|
+
end
|
|
186
|
+
if a.is_a?(Date) || b.is_a?(Date)
|
|
187
|
+
if a.is_a?(Date) && b.is_a?(Date)
|
|
188
|
+
f = ORDER[op]
|
|
189
|
+
raise Refused, "сравнение вне среза рантайма: #{op}" if f.nil?
|
|
190
|
+
return f.call(a.v, b.v)
|
|
191
|
+
end
|
|
192
|
+
return false if op == "eq"
|
|
193
|
+
return true if op == "ne"
|
|
194
|
+
raise Refused, "§57: Date сравнивается только с Date"
|
|
195
|
+
end
|
|
196
|
+
if a.is_a?(String) || b.is_a?(String)
|
|
197
|
+
return a == b if op == "eq"
|
|
198
|
+
return a != b if op == "ne"
|
|
199
|
+
raise Refused, "порядок на Text вне среза рантайма: #{op}"
|
|
200
|
+
end
|
|
201
|
+
raise Refused, "§49: сравнение денег с безразмерным" if money?(a) != money?(b)
|
|
202
|
+
raise Refused, "§49: разные валюты" if money?(a) && a.cur != b.cur
|
|
203
|
+
x, y, = pair(a, b)
|
|
204
|
+
f = ORDER[op]
|
|
205
|
+
raise Refused, "сравнение вне среза рантайма: #{op}" if f.nil?
|
|
206
|
+
f.call(x, y)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def eq(a, b)
|
|
210
|
+
return a.is_a?(Sym) && b.is_a?(Sym) && a.id == b.id if a.is_a?(Sym) || b.is_a?(Sym)
|
|
211
|
+
return a.is_a?(Date) && b.is_a?(Date) && a.v == b.v if a.is_a?(Date) || b.is_a?(Date)
|
|
212
|
+
return a == b if a.is_a?(String) || b.is_a?(String)
|
|
213
|
+
return false if money?(a) != money?(b)
|
|
214
|
+
return false if money?(a) && a.cur != b.cur
|
|
215
|
+
x, y, = pair(a, b)
|
|
216
|
+
x == y
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def norm(d)
|
|
220
|
+
u = d.u
|
|
221
|
+
s = d.s
|
|
222
|
+
while s.positive? && (u % 10).zero?
|
|
223
|
+
u /= 10
|
|
224
|
+
s -= 1
|
|
225
|
+
end
|
|
226
|
+
"#{u}e-#{s}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Разделитель и экран внутри текста экранируются: иначе ["a|e:b","c"] и
|
|
230
|
+
# ["a","b|e:c"] склеивались в один ключ, и второй факт терялся.
|
|
231
|
+
def esc(text) = text.gsub("\\", "\\\\\\\\").gsub("|", "\\\\|")
|
|
232
|
+
|
|
233
|
+
def key(row)
|
|
234
|
+
row.map do |v|
|
|
235
|
+
case v
|
|
236
|
+
when String then "e:" + esc(v)
|
|
237
|
+
when Date then "t:" + v.v
|
|
238
|
+
when Sym then "c:" + esc(v.id)
|
|
239
|
+
when Money then "m:" + v.cur + ":" + norm(v)
|
|
240
|
+
else "d:" + norm(v)
|
|
241
|
+
end
|
|
242
|
+
end.join("|")
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
@legal_time = ""
|
|
246
|
+
|
|
247
|
+
def set_legal_time(t)
|
|
248
|
+
@legal_time = t.to_s
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def legal_time
|
|
252
|
+
raise Refused, "legalTime не подан делом" if @legal_time.empty?
|
|
253
|
+
@legal_time
|
|
254
|
+
end
|
|
255
|
+
end
|