Files
Aislo/resources/tester/test_est_rea_parity.py

654 lines
26 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""REA 저장 샘플과 웹앱 엔진의 대조. 원본 프로그램 실행 시험이 아님."""
import json
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
HELPER = Path(__file__).with_name("helper_est_native.cjs")
EXPECTED = json.loads(
(ROOT / "resources/tester/fixtures/est_rea_expected.json").read_text(encoding="utf-8")
)
SAMPLE = json.loads((ROOT / "tools/est-test/sample.json").read_text(encoding="utf-8"))
def run(command, *args):
return json.loads(
subprocess.check_output(["node", str(HELPER), command, *args], text=True, encoding="utf-8")
)
def test_machine_stored_components_heads_and_titles():
result = run("machine")
assert result["errors"] == []
count = 0
for expected in EXPECTED["machine"]["rows"]:
if "stored" not in expected:
continue
row = result["rows"][expected["slot"]]
if isinstance(expected["stored"], dict):
for key, value in expected["stored"].items():
assert abs((row.get(key) or 0) - (value or 0)) < 1e-6, (
expected["slot"],
key,
row.get(key),
value,
)
else:
assert abs(row[expected["field"]] - expected["stored"]) < 1e-6, expected
count += 1
by_code = {row["Code"]: row for row in result["titles"]}
for expected in SAMPLE["tables"]["MachTitleDat"]:
actual = by_code[expected["Code"]]
for key in ["NC", "GC", "JC", "TC"]:
assert actual[key] == expected[key]
count += 1
assert count == 48
def test_numeric_helpers_distinguish_rounding_modes():
assert run(
"math",
"[EstMath.even(2.5),EstMath.even(3.5),EstMath.floating(-1.25,1,1),EstMath.fixed(1.23999,2)]",
) == [2, 4, -1.3, 1.24]
def test_large_currency_boundary_and_unit_conversion_caption():
assert run("math", "EstMath.floating(172068*0.1*756,1,2)") == 13008340.8
result = run(
"math",
"""EstRows.head([
{Index:1,Code:'H'},
{Index:0,GN:1,NU:14341},
{Index:0,GN:0.1,LinkIndex:68,Size:'소계 / 10단자'}
],{HeadDigits:0})""",
)
assert result["rows"][0]["NC"] == 1434
assert result["rows"][2]["NU"] == 14341
assert result["rows"][2]["NC"] == 1434
def test_procurement_zero_place_keeps_integer_total():
row = run(
"math",
"EstRows.procurement({Index:3,LinkIndex:57,GN:0.54,Gangub:0,OCheck:1,SCheck:3},[{LinkIndex:5,GC:30029967,TC:30029967}])",
)
assert row["JC"] == row["TC"] == 162161
def test_blob_reused_capacity_and_incomplete_payload_are_distinct():
result = run("blob-tail")
assert result["value"] == "abc"
assert "BLOB 총 길이 오류" in result["truncated"]
def test_procurement_exclusion_only_changes_industrial_safety_base():
result = run(
"math",
"""EstWonga.calculate([
{Gubun:'J',Name:'관급자재대',Cost:1000,NC:100,VSusic:'원금액:1,000, {조달수수료:100}'},
{Gubun:'S',Name:'산업안전보건관리비',VSusic:'J*0.1'},
{Gubun:'P',Name:'other',VSusic:'J*0.1'}
],{GangubExceptJodalRatio:true})""",
)
assert result["errors"] == []
assert [row["Cost"] for row in result["rows"]] == [1000, 90, 100]
def test_resource_factor_includes_trailing_multiplier():
row = run(
"math",
r"""EstText.calculate([
{Index:1,Code:'C',LineData:''},
{Index:0,LinkIndex:5,SCheck:-2,LineData:'\x0eM\x0e : \x03100\x03 * 2 = \x12200\x12 * 0.5 = \x12100\x12'}
],{},()=>({Code:'M',Name:'M',SourceKind:5,GC:100,TC:100})).rows[1]""",
)
assert row["ResourceFactor"] == 1
assert row["TC"] == 100
def test_direct_material_scope_is_the_marked_section():
result = run(
"math",
"""(() => {
const rows=[{Level:2,Name:'순직접공사비'},{Level:0,LinkIndex:5,GC:100},
{Level:2,Name:'사급자재대*'},{Level:0,LinkIndex:5,GC:200},{LinkIndex:54}];
return [EstNative.materialBase(rows,{GCToWonga:false}),EstNative.materialBase(rows,{GCToWonga:true})];
})()""",
)
assert result == [200, 300]
def test_practical_fire_projects_keep_stored_values_and_report_bad_source_formula():
files = list((ROOT / "resources/knowledge/original/실무문서").rglob("*.estx"))
assert len(files) >= 3
for file in files:
output = subprocess.check_output(
["node", str(HELPER.with_name("helper_est_archive.cjs")), str(file)],
text=True,
encoding="utf-8",
timeout=120,
)
result = json.loads(output)
assert result["differences"] == [], file.name
# The original text has an unclosed parenthesis in this transport
# calculation; preserve its stored price and show the source error.
assert all(
error.startswith("D0000000194:") and "괄호" in error for error in result["errors"]
), (file.name, result["errors"])
def test_head_41_stored_checks():
result = run("head")
assert result["errors"] == []
rows = result["rows"]
for expected in EXPECTED["head_est"]["head"]:
actual = rows[expected["source_slot"]]
for key, value in expected["stored"].items():
assert abs((actual.get(key) or 0) - value) < 1e-6, (expected, key, actual.get(key))
assert len(EXPECTED["head_est"]["head"]) == 41
def test_estimate_37_stored_checks():
result = run("estimate")
assert result["errors"] == []
for expected in EXPECTED["head_est"]["est"]:
row = result["rows"][expected["slot"]]
for key, value in expected["stored"].items():
assert abs((row.get(key) or 0) - value) < 1e-6, (
expected["slot"],
key,
row.get(key),
value,
)
assert len(EXPECTED["head_est"]["est"]) == 37
def test_linedata_118_results_and_8_titles():
result = run("text")
assert result["errors"] == []
assert len(result["checks"]) == 118
for item in result["checks"]:
assert abs(item["stored"] - item["calculated"]) < 1e-6, item
for expected in EXPECTED["linedata"]["rows"]:
if expected["kind"] == "title":
actual = result["rows"][expected["slot"]]
for key, value in expected["stored"].items():
assert actual[key] == value
def test_wonga_all_23_formulas_and_total_fix():
result = run("wonga")
assert result["errors"] == []
for expected in EXPECTED["wonga"]["results"]:
row = result["rows"][expected["row_position"]]
assert row["Cost"] == float(expected["stored"]), (
row["Gubun"],
row["Cost"],
expected["stored"],
)
assert len(EXPECTED["wonga"]["results"]) == 23
assert result["total"] == 79603000
assert [step["difference"] for step in result["adjustments"]] == [324, -32, 3, 0]
def test_entire_refresh_pipeline_preserves_stored_outputs():
result = run("refresh")
assert result["calculation"]["errors"] == []
for table in ["MachMainDat", "HeadMainDat", "EstMainDat"]:
for index, (before, after) in enumerate(
zip(SAMPLE["tables"][table], result["tables"][table])
):
for key in ["NC", "GC", "JC", "SC", "TC"]:
assert abs((before.get(key) or 0) - (after.get(key) or 0)) < 1e-6, (
table,
index,
key,
)
for table in ["CostTitleDat", "HeadTitleDat", "MachTitleDat"]:
lookup = {row["Code"]: row for row in result["tables"][table]}
for before in SAMPLE["tables"][table]:
for key in ["NC", "GC", "JC", "SC", "TC"]:
assert abs((before.get(key) or 0) - (lookup[before["Code"]].get(key) or 0)) < 1e-6
def test_recalculation_after_price_edit_is_idempotent():
first, second = run("repeat")
assert first["calculation"]["errors"] == second["calculation"]["errors"] == []
assert first["tables"]["CostTitleDat"] == second["tables"]["CostTitleDat"]
assert first["tables"]["EstMainDat"] == second["tables"]["EstMainDat"]
def test_progress_accepted_rounds_annual_allocation_and_resources():
result = run("business")
first, second = result["first"], result["second"]
assert first["errors"] == second["errors"] == []
# c914d4 rounds ratio-derived quantities to the contract quantity's digits.
assert first["rows"][0]["GumGN"] == 3
assert first["rows"][0]["TotalGN"] == 1.5
assert first["total"] == 330
assert second["rows"][0]["JunGN"] == 1.5
assert second["rows"][0]["GumGN"] == 2.5
assert second["rows"][0]["TotalGN"] == 4
assert second["rows"][0]["TotalTC"] == 800
assert first["rows"][-1]["GumTC"] == 660
assert first["rows"][-1]["SaTC"] == first["rows"][-1]["TotalTC"] == 330
assert second["rows"][-1]["JunTC"] == 330
assert second["rows"][-1]["TotalTC"] == 880
assert result["years"]["errors"] == []
assert [r["project"]["calculation"]["total"] for r in result["years"]["results"]] == [880, 1320]
assert result["resources"]["errors"] == []
assert result["resources"]["rows"][0]["quantity"] == 20
escalation = result["escalation"]
assert escalation["errors"] == []
assert escalation["rows"][0]["EscExcluded"] == 3
assert escalation["rows"][0]["EscApplied"] == 7
assert escalation["contractTotal"] == 2200
assert escalation["adjustedTotal"] == 2640
assert escalation["excludedTotal"] == 660
assert escalation["applied"] == 1540
# REA 10a8fa8: investigation amount 1,980 is the denominator, not contract 2,200.
assert escalation["designTotal"] == 1980
assert escalation["rate"] == 22.22
assert escalation["adjustment"] == 342
assert [
next(row["TC"] for row in item["tables"]["EstMainDat"] if row.get("LinkIndex") == 54)
for item in result["originals"]
] == [2120, 2520, 2520]
assert result["bid"]["calculation"]["direct"]["TC"] == 1800
assert result["bidAgain"]["tables"]["EstMainDat"] == result["bid"]["tables"]["EstMainDat"]
assert result["bidResources"]["errors"] == []
assert result["bidResources"]["rows"][0]["quantity"] == 18
assert result["bidResources"]["rows"][0]["amount"] == 1800
assert result["convertedResources"]["errors"] == []
assert result["convertedResources"]["rows"][0]["quantity"] == 10
assert result["convertedResources"]["rows"][0]["amount"] == 1000
assert result["equipmentResources"]["errors"] == []
assert result["equipmentResources"]["rows"][0]["quantity"] == 20
assert result["equipmentResources"]["rows"][0]["amount"] == 2000
def test_estx_import_reads_original_tables_options_and_templates(tmp_path):
import zipfile
bundle = next((ROOT / "resources/knowledge/original").glob("*/EST Plus 2/*/05_*/sample_bundle"))
archive = tmp_path / "sample.estx"
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as output:
for file in bundle.iterdir():
if file.is_file():
output.write(file, file.name)
result = run("import", str(archive))
assert result["schema"] == "est-web-2"
assert len(result["tables"]["EstMainDat"]) == len(SAMPLE["tables"]["EstMainDat"])
assert len(result["originalOptions"]) == 17
assert len(result["wongaTemplates"]) == 4
assert result["options"]["TotalFix"] is True
assert result["options"]["GCToWonga"] is False
assert result["options"]["CostValueTrunc"] is False
assert result["options"]["MidCostDiget"] == 2
assert result["originalArchive"]
def test_source_price_edit_changes_downstream_costs():
first, _ = run("repeat")
original = run("refresh")
assert first["calculation"]["direct"]["TC"] != original["calculation"]["direct"]["TC"]
assert first["tables"]["Wonga"] != original["tables"]["Wonga"]
def test_extended_original_rows_cover_conversion_fractions_and_fuel():
fixture = json.loads(
(ROOT / "resources/tester/fixtures/est_native_extended.json").read_text(encoding="utf-8")
)
result = run("extended")
for name, table in [("head", "HeadMainDat"), ("machine", "MachMainDat")]:
assert result[name]["errors"] == []
for index, (before, after) in enumerate(
zip(fixture["tables"][table], result[name]["rows"])
):
for key in ["NC", "GC", "JC", "SC", "TC"]:
assert abs((before.get(key) or 0) - (after.get(key) or 0)) < 1e-6, (
table,
index,
key,
)
assert result["text"]["errors"] == []
for check in result["text"]["checks"]:
assert abs(check["stored"] - check["calculated"]) < 1e-6, check
def test_installed_main_archive_all_stored_values_when_available():
import os
import pytest
archive = Path(
os.environ.get("EST_REA_MAIN", "C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx")
)
if not archive.is_file():
pytest.skip("Original EST main.estx is not installed; portable fixtures still run")
output = subprocess.check_output(
["node", str(HELPER.with_name("helper_est_archive.cjs")), str(archive)],
text=True,
encoding="utf-8",
timeout=120,
)
result = json.loads(output)
assert result["errors"] == []
assert result["differences"] == []
assert all(item["checked"] == item["matched"] for item in result["counts"].values())
assert sum(item["checked"] for item in result["counts"].values()) > 100000
def test_original_quantity_sheets_and_three_saved_formula_disagreements():
sheets = run("quantity")
assert len(sheets) == 75
# Original editor formulas disagree with MulList saved numbers in three
# places. Keep both visible instead of changing the formulas to replay them.
expected = {
("N0000000083", "r000004"): (286, 0.35 * 1.3 * 220),
("N0000000086", "r000004"): (0, 1 * 1),
("N0000000106", "r000000"): (138.72, 1.25 * 2 * 68 * 1.02),
}
differences = set()
checked = 0
for sheet in sheets:
assert sheet["errors"] == []
for before, after in zip(sheet["original"], sheet["items"]):
checked += 1
key = (sheet["Code"], before["Code"])
if key in expected:
assert before["GN"] == expected[key][0]
assert abs(after["GN"] - expected[key][1]) < 1e-6
differences.add(key)
else:
assert abs((before.get("GN") or 0) - (after.get("GN") or 0)) < 1e-6, key
assert checked == 449
assert differences == set(expected)
def test_legacy_project_preserves_running_cost_basis_and_final_total():
result = run(
"math",
"""EstNative.calculate(EstModel.legacy({
sources:[{id:'s',kind:'재료비',price:10.129}],
units:[{id:'u',lines:[{sourceId:'s',quantity:2,digits:2}]}],
estimate:[{unitId:'u',quantity:3}],
costs:[{base:'직접공사비',rate:10},{base:'누계',rate:5}]
})).calculation""",
)
assert result["errors"] == []
assert result["direct"]["TC"] == 60
assert result["total"] == 69
def test_manual_calculation_server_preserves_stored_amounts():
result = run("manual")
assert result["tables"]["EstMainDat"][1]["GN"] == SAMPLE["tables"]["EstMainDat"][1]["GN"] + 100
for table in ["EstMainDat", "HeadMainDat", "Wonga"]:
for original, saved in zip(SAMPLE["tables"][table], result["tables"][table]):
for key in ["TC", "NC", "GC", "JC", "SC", "Cost"]:
assert saved.get(key) == original.get(key)
def test_text_fraction_spacing_ascii_multiplication_and_tree_units():
result = run(
"math",
r"""EstText.calculate([
{Index:1,Code:'T'},
{LineData:' n = 6.55 Cms = 37.4 A = 1 N = 1'},
{LineData:' L = 50m 25kg/1회'},
{LineData:' ○ 거리 Z=17m, 7m마다 1인으로 하면'},
{LineData:' n × Cms 6.55 × 37.4'},
{LineData:' t1 = ────── = ─────── = \x1211.67\x12'},
{LineData:' 60×0.35 60×0.35'},
{LineData:' D=0.2xAxN+2 = \x122.2\x12'},
{LineData:' 길이 = L + Z = \x1267\x12'},
{LineData:' * 윤 주 : 0.94m (0.65×0.65×3.14÷4)÷2 = \x120.17\x12㎥'},
{LineData:' 노무비: \x03235204\x03 × 3인 / 125주 = \x125644.8\x12',SCheck:-1}
])""",
)
assert result["errors"] == []
assert [row["TC"] for row in result["rows"] if "TC" in row][1:] == [
11.67,
2.2,
67,
0.17,
5644.8,
]
def test_text_material_only_subtotal_does_not_reuse_prior_labor():
result = run(
"math",
r"""EstText.calculate([
{Index:1,Code:'T'},
{LineData:' \x1a노무비: 100\x1a',SCheck:-1,Level:-1},
{LineData:' \x1a재료비: 20\x1a',SCheck:-2,Level:-1},
{LineData:' \x1a계:\x1a \x12120\x12',SCheck:-5,Level:-1},
{LineData:' 다음 재료 구획',Level:1},
{LineData:' \x1a재료비: 30\x1a',SCheck:-2,Level:-1},
{LineData:' \x1a계:\x1a \x1230\x12',SCheck:-5,Level:-1}
])""",
)
assert result["errors"] == []
assert result["rows"][3]["TC"] == 120
assert result["rows"][6]["TC"] == 30
def test_original_text_layouts_and_selected_group_subtotals():
results = run("text-layout")
assert len(results) == 4
checked = 0
for result in results:
assert result["errors"] == [], result["code"]
for check in result["checks"]:
assert abs(check["stored"] - check["calculated"]) < 1e-6, (result["code"], check)
checked += 1
assert checked > 100
def test_text_preserves_braced_signs_and_comments_inside_parentheses():
result = run(
"math",
r"""EstText.calculate([
{Index:1,Code:'T'},
{LineData:' hm={-}6.76m {?} H={-}7.5m {계획수심}'},
{LineData:' d = hm-H = \x120.74\x12'},
{LineData:' L = 4인÷(4.8개{?}×8hr) = \x120.1\x12'},
{LineData:' q = 1 × 2조/회 = \x122\x12 t=15분 〔접이안시간〕'},
{LineData:' 전력비 = 0.74hr × 52.1 = \x1238.5\x12 원/본',SCheck:-3}
])""",
)
assert result["errors"] == []
assert result["rows"][2]["TC"] == 0.74
assert result["rows"][3]["TC"] == 0.1
assert result["rows"][4]["TC"] == 2
assert result["rows"][5]["TC"] == 38.5
def test_source_code_edit_updates_links_and_preserves_original_contract():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();
p.tables.StuffcDat=[{PkID:1,Code:'M',Name:'material',ValueIndex:4,InputTC:100},{PkID:2,Code:'OTHER',ValueIndex:4,InputTC:10}];
p.tables.HeadMainDat=[{PkID:1,Index:1,Code:'H',Name:'head'},{PkID:2,Index:0,Code:'M',TitleCode:'H',LinkIndex:5,GN:2}];
p.tables.HeadTitleDat=[{Code:'H',Name:'head'}];
p.tables.EstMainDat=[{PkID:1,Code:'H',LinkIndex:103,GN:10},{LinkIndex:54}];
let q=EstBusiness.baseline(EstNative.calculate(p));
const before=JSON.stringify(q.baseline);
EstModel.editCode(q,'StuffcDat',0,'M2');
EstModel.editCode(q,'HeadMainDat',0,'H2');
q.tables.StuffcDat[0].InputTC=120;
q=EstNative.calculate(q);
const original=EstBusiness.original(q,2);
let duplicate=false;try{EstModel.editCode(q,'StuffcDat',0,'OTHER')}catch{duplicate=true;}
return {q,original,preserved:before===JSON.stringify(q.baseline),duplicate};
})()""",
)
project = result["q"]
assert project["calculation"]["errors"] == []
assert project["calculation"]["direct"]["TC"] == 2400
assert project["tables"]["HeadMainDat"][1]["Code"] == "M2"
assert project["tables"]["HeadMainDat"][1]["TitleCode"] == "H2"
assert project["tables"]["EstMainDat"][0]["Code"] == "H2"
assert project["tables"]["HeadTitleDat"][0]["Code"] == "H2"
assert result["original"]["tables"]["EstMainDat"][-1]["TC"] == 2400
assert result["preserved"] and result["duplicate"]
def test_text_fraction_columns_preserve_numbered_variables_and_unmarked_result():
result = run(
"math",
r"""EstText.calculate([
{Index:1,Code:'T'},
{LineData:' L11=0.35 L21=0.35 V51=15 V52=15 N=2'},
{LineData:' L11 L21'},
{LineData:' t23=(──+──)×60 = \x122.8\x12'},
{LineData:' V51 V52'},
{LineData:' t23 + t23'},
{LineData:' Tc = ────────────'},
{LineData:' 1 · N'},
{LineData:' Q=Tc×2 = \x125.6\x12'},
{LineData:' to1 = 0.6 min/m2 to2 = 3 min/m2'},
{LineData:' T=2×30×3.5×to2/60=\x1210.5\x12'}
])""",
)
assert result["errors"] == []
assert result["rows"][3]["TC"] == 2.8
assert result["rows"][8]["TC"] == 5.6
assert result["rows"][10]["TC"] == 10.5
def test_original_cost_truncation_and_intermediate_precision_options():
result = run(
"math",
r"""(()=>{
const rows=[{Index:1,Code:'T'},
{LineData:' 노무비: \x03100.19\x03 = \x12100.1\x12',SCheck:-1}];
const intermediate=[{Index:1,Code:'I'},
{LineData:' 노무비: \x0359020\x03 / 81.8 = \x12721.51\x12 ×0.5=\x12360.7\x12',SCheck:-1}];
return {normal:EstText.calculate(rows),whole:EstText.calculate(rows,{CostValueTrunc:true}),
mid1:EstText.calculate(intermediate,{MidCostDiget:1}),mid2:EstText.calculate(intermediate,{MidCostDiget:2}),
override:EstText.calculate([rows[0],{...rows[1],Precision:2}],{CostValueTrunc:true})};
})()""",
)
assert all(item["errors"] == [] for item in result.values())
assert result["normal"]["rows"][1]["TC"] == 100.1
assert result["whole"]["rows"][1]["TC"] == 100
assert result["override"]["rows"][1]["TC"] == 100.19
assert result["mid1"]["checks"][-1]["calculated"] == 721.5
assert result["mid2"]["checks"][-1]["calculated"] == 721.51
def test_source_price_refresh_recalculates_text_subtotal_and_percentage_reference():
result = run(
"math",
r"""(()=>{
const p=EstNative.empty();
p.tables.StuffcDat=[{Code:'M',Name:'material',ValueIndex:4,InputTC:150}];
p.tables.CostMainDat=[{Index:1,Code:'C'},
{Level:1,LineData:' material section'},
{Index:0,Code:'M',LinkIndex:5,SCheck:-2,LineData:' \x0ematerial\x0e: \x03100\x03 ×2 = \x12200\x12'},
{Index:-20,LinkIndex:61,SCheck:-2,LineData:' material subtotal: \x0c200\x0c'},
{LinkIndex:58,SCheck:-3,OCheck:-2,LineData:' \x0c200\x0c ×5% = \x1210\x12'},
{Level:1,LineData:' total'},
{Level:-1,SCheck:-2,LineData:' \x1a재료비:200\x1a'},
{Level:-1,SCheck:-3,LineData:' \x1a경비:10\x1a'},
{Level:-1,SCheck:-5,LineData:' \x1a계:\x1a \x12210\x12'}];
p.tables.CostTitleDat=[{Code:'C'}];
return EstNative.calculate(p);
})()""",
)
assert result["calculation"]["errors"] == []
rows = result["tables"]["CostMainDat"]
assert rows[2]["TC"] == 300
assert rows[3]["TC"] == 300
assert "\x0c300\x0c" in rows[3]["LineData"]
assert rows[4]["TC"] == 15
assert rows[0]["TC"] == 315
def test_selected_text_group_uses_distinct_rows_with_equal_original_amounts():
result = run(
"math",
r"""EstText.calculate([
{Index:1,Code:'C'},
{Code:'M',LinkIndex:5,SCheck:-2,LineData:' \x0ematerial\x0e: \x03100\x03 = \x06A\x06\x12100\x12'},
{Code:'L',LinkIndex:6,SCheck:-1,LineData:' \x0elabor\x0e: \x03100\x03 = \x06A\x06\x12100\x12'},
{LinkIndex:62,SCheck:-3,LineData:' \x07A\x07\x1a100 + 100\x1a = \x12200\x12 ×10% = \x1220\x12'}
],{},row=>row.Code==='M'?{GC:150,TC:150}:{NC:200,TC:200})""",
)
assert result["errors"] == []
assert result["rows"][3]["TC"] == 35
assert "150 + 200" in result["rows"][3]["LineData"]
def test_text_bid_rate_row_preserves_native_component_walk_and_currency_precision():
result = run(
"math",
r"""EstText.calculate([
{Index:1,Code:'C'},
{Level:1,LineData:' bid section'},
{SCheck:-1,LineData:' 노무비: 100 = \x12100\x12'},
{SCheck:-1,LineData:' 노무비: 50 = \x1250\x12'},
{SCheck:-2,LineData:' 재료비: 20 = \x1220\x12'},
{SCheck:-2,LineData:' 재료비: 30 = \x1230\x12'},
{SCheck:-3,LineData:' 경비: 12.3456 = \x1212.3456\x12',Precision:4},
{LinkIndex:52,LineData:' 낙찰률 \x1590\x15 %'}
])""",
)
assert result["errors"] == []
assert result["rows"][-1]["NC"] == result["rows"][0]["NC"] == 90
assert result["rows"][0]["GC"] == 45
assert result["rows"][0]["JC"] == 11.111
assert result["rows"][0]["TC"] == 146.111
def test_text_group_markers_are_editable_and_missing_group_is_reported():
result = run(
"math",
r"""(()=>{
const line='\x06A\x06\x12100\x12 + \x07A\x07';
return {shown:EstText.display(line),roundtrip:EstText.encode(EstText.display(line))===line,
missing:EstText.calculate([{Index:1,Code:'C'},
{LinkIndex:62,SCheck:-3,LineData:'\x07A\x07\x1a100\x1a = \x12100\x12 ×10% = \x1210\x12'}])};
})()""",
)
assert result["roundtrip"]
assert "⟦구간지정⟧A⟦구간지정⟧" in result["shown"]
assert "⟦구간합계⟧A⟦구간합계⟧" in result["shown"]
assert len(result["missing"]["errors"]) == 1
assert "A" in result["missing"]["rows"][1]["Error"]
def test_fixed_text_formula_clears_the_previous_row_error():
result = run(
"math",
r"""(()=>{
const bad=EstText.calculate([{Index:1,Code:'C'},
{LineData:' Q=(1+2 = \x123\x12'}]);
bad.rows[1].LineData=' Q=1+2 = \x123\x12';
return {bad:bad.errors,good:EstText.calculate(bad.rows)};
})()""",
)
assert len(result["bad"]) == 1
assert result["good"]["errors"] == []
assert "Error" not in result["good"]["rows"][1]
def test_text_total_without_result_marker_evaluates_its_expression():
result = run(
"math",
r"""EstText.calculate([
{Index:1,Code:'C'},
{Level:-1,SCheck:-1,LineData:' \x1a노무비:10 + 10\x1a'},
{Level:-1,SCheck:-2,LineData:' \x1a재료비:\x1a'},
{Level:-1,SCheck:-5,LineData:' \x1a계:\x1a \x1220\x12'}
])""",
)
assert result["errors"] == []
assert result["rows"][0]["TC"] == 20
assert result["rows"][1]["TC"] == 20
assert result["rows"][2]["TC"] == 0