fix(est): preserve precision and parse source notes
This commit is contained in:
@@ -135,3 +135,125 @@ def test_subtotal_includes_price_only_rows_and_updates_truncated_final_amount():
|
||||
149380,
|
||||
158440,
|
||||
]
|
||||
|
||||
|
||||
def test_operator_followed_by_spaces_does_not_start_a_new_assignment():
|
||||
result = run(
|
||||
"math",
|
||||
r"""[1700,2000].map(weight=>EstText.calculate([
|
||||
{Index:1,Code:'forest',LineData:'forest'},
|
||||
{LineData:` N = 54.93 q = 250 W = ${weight}`},
|
||||
{LineData:' Q = N × q ÷ W = 54.93 × 250 ÷ 1,700 = \x128.08\x12 ㎥/hr'},
|
||||
{LineData:' V = Q × 2 = \x1216.16\x12'}
|
||||
]))""",
|
||||
)
|
||||
assert all(item["errors"] == [] for item in result)
|
||||
assert [item["rows"][2]["TC"] for item in result] == [8.08, 6.87]
|
||||
assert [item["rows"][3]["TC"] for item in result] == [16.16, 13.74]
|
||||
|
||||
|
||||
def test_curly_wrapped_results_and_explanatory_calculations_keep_their_text():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'forest',LineData:'forest'},
|
||||
{LineData:' {0.91m×0.91m×0.6m = \x120.49686\x12㎥}'},
|
||||
{LineData:' {0.49686 ×2.6Ton = \x121.29\x12}'},
|
||||
{LineData:' ※ ㎡당 중량 1.6개 ×1,325÷1,000 = \x122.12\x12Ton/㎡'}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert [row["TC"] for row in result["rows"][1:]] == [0.49686, 1.29, 2.12]
|
||||
assert result["rows"][1]["LineData"].strip().startswith("{")
|
||||
assert result["rows"][1]["LineData"].endswith("}")
|
||||
|
||||
|
||||
def test_won_unit_glyphs_are_units_instead_of_operators():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'money',LineData:'money'},
|
||||
{LineData:' 0.0347㎥ / 2.5 × 0 \\/㎥ = \x120\x12'},
|
||||
{LineData:' 0.0347㎥ / 2.5 × 2500 \\/㎥ = \x1234.7\x12'},
|
||||
{LineData:' 2㎥ × 2500₩/㎥ = \x125000\x12'}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert [row["TC"] for row in result["rows"][1:]] == [0, 34.7, 5000]
|
||||
|
||||
|
||||
def test_range_conditions_are_not_assigned_arbitrary_values():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'range',LineData:'range'},
|
||||
{LineData:' 앙카핀 (D=13, L=450~600mm) 1.0㎡ : 별도계상'},
|
||||
{LineData:' H = 0~4m : (기계80%, 인력20%)'},
|
||||
{LineData:' Q = 2 × 3 = \x126\x12'},
|
||||
{LineData:' V = H × 2 = \x128\x12'}
|
||||
])""",
|
||||
)
|
||||
assert len(result["errors"]) == 1
|
||||
assert "참조 항목 없음: H" in result["errors"][0]
|
||||
assert result["rows"][3]["TC"] == 6
|
||||
|
||||
|
||||
def test_quantity_subtotal_keeps_six_digits_before_pricing():
|
||||
result = run(
|
||||
"math",
|
||||
r"""[4.4,5].map(length=>EstText.calculate([
|
||||
{Index:1,Code:'sign',LineData:'sign'},
|
||||
{Level:2,LineData:'각재 수량'},
|
||||
{Diget:7,DigetIndex:1,LineData:` 0.09 ×0.09 ×${length}m ×2개 = \x120.07128\x12`},
|
||||
{Diget:7,DigetIndex:1,LineData:' 0.06 ×0.06 ×2.28m ×5개 = \x120.04104\x12'},
|
||||
{Diget:7,DigetIndex:1,LineData:' 0.06 ×0.06 ×2.28m ×7개 = \x120.057456\x12'},
|
||||
{Diget:7,DigetIndex:1,LinkIndex:61,LineData:' 소 계 : Q1 = \x0c0.169776\x0c ㎥/개소'},
|
||||
{SCheck:-2,LinkIndex:5,LineData:' 재료비 : \x03577,842\x03 × Q1 = 577,842 × 0.169776 = \x1298,103.7\x12'}
|
||||
]))""",
|
||||
)
|
||||
assert all(item["errors"] == [] for item in result)
|
||||
assert [item["rows"][5]["TC"] for item in result] == [0.169776, 0.179496]
|
||||
assert [item["rows"][6]["TC"] for item in result] == [98103.7, 103720.3]
|
||||
|
||||
|
||||
def test_declaration_can_follow_an_hour_unit_with_a_single_space():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'transport',LineData:'transport'},
|
||||
{LineData:' L = 1.0Km{?} V1 = 5.5Km/hr V2 = 9.3Km/hr t=35분'},
|
||||
{LineData:' Q = t × 2 = \x1270\x12'}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert result["rows"][2]["TC"] == 70
|
||||
|
||||
|
||||
def test_fullwidth_unit_slash_and_ground_level_annotations():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'annotations',LineData:'annotations'},
|
||||
{LineData:' Q = 23.32 ㎥/hr'},
|
||||
{LineData:' D1 = {-}6.6m [평균지반고]'},
|
||||
{LineData:' D2 = {-}18.6m [기초터파기 평균 저면 EL.]'},
|
||||
{LineData:' V = Q + D1 - D2 = \x1235.32\x12',Precision:2}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert result["rows"][4]["TC"] == 35.32
|
||||
|
||||
|
||||
def test_spaced_metre_unit_is_not_a_second_variable_declaration():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'spaced_unit',LineData:'spaced_unit'},
|
||||
{LineData:' L7 = 4.5m'},
|
||||
{LineData:' Q2 = L7 / 0.8 M = 4.5 / 0.8 = \x125.63\x12 개'},
|
||||
{SCheck:-2,LineData:' 재료비 : \x033,000\x03 × Q2 = 3,000 × 5.63 = \x1216,890\x12'}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert result["rows"][2]["TC"] == 5.63
|
||||
assert result["rows"][3]["TC"] == 16890
|
||||
|
||||
@@ -36,11 +36,13 @@ Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요
|
||||
|
||||
설계변경 비교는 웹에서 확정한 당초 사본이 없을 때 원본 파일의 당초 수량·금액을 사용합니다. 별도 당초 확정 사본이 있으면 그 사본이 우선합니다. 원본 당초 전체 자료와 원가 서식의 활성 선택을 자동 추정하지는 않습니다.
|
||||
|
||||
원본 산근의 `배`·`번`·`줄`·`set`·`kg/set`·`㎜`, 괄호 뒤 면적·체적 단위와 별표·따옴표로 감싼 변수 선언도 계산합니다. 측량설계 저장값 대조는 80,659/80,682 일치·해석 오류 5건, 가시설은 13,978/13,979 일치·해석 오류 4건입니다. 범위 조건과 누락된 참조 등은 임의 값으로 대체하지 않습니다.
|
||||
원본 산근의 `배`·`번`·`줄`·`set`·`kg/set`·`㎜`, 괄호 뒤 면적·체적 단위와 별표·따옴표로 감싼 변수 선언도 계산합니다. 연산자 뒤 공백과 새 선언을 구분하며, `\\/㎥`·`₩/㎥` 금액 단위와 전각 단위 구분자도 읽습니다. 측량설계 저장값 대조는 80,671/80,682 일치·해석 오류 0, 산림토목은 15,049/15,055 일치·해석 오류 0, 가시설은 13,978/13,979 일치·해석 오류 3건입니다. 범위 조건과 누락된 참조 등은 임의 값으로 대체하지 않습니다.
|
||||
|
||||
소계는 원본처럼 정밀도를 유지하며 합산한 뒤 행의 자릿수를 적용합니다. 수량 `0.169776`을 4자리로 미리 줄이지 않습니다. 원본 산식과 저장 표지가 다른 경우는 구분합니다. 예를 들어 상수공사 원문의 `{ 소요살수량 1.7 ÷ 0.95 = ⟦결과⟧1.7⟦결과⟧9 Ton/㎥ }`는 저장 표지 1.7과 재계산 1.79가 다릅니다.
|
||||
|
||||
구간 합계와 소계는 결과 금액뿐 아니라 단가만 적힌 지정 행도 포함합니다. 구간 합계의 표지와 최종 합계 표지를 구분하며, 이미 정수로 절사된 최종 합계도 앞 성분 금액과의 합산 관계가 확인되면 변경액을 다시 집계합니다. 단가 550→600 변경이 구간 비율 27.5→30, 최종 합계 27→30, 내역 270→300으로 전달되는 것을 저장·취소까지 확인했습니다.
|
||||
|
||||
이 보완 후 해양 자료는 소계 표지까지 포함해 134,133/134,208 저장값 일치·해석 오류 99건입니다. 계산·이력 전용 시험 64개가 통과했으며, 남은 불일치와 원본 실행 미확인은 별개로 유지합니다.
|
||||
이 보완 후 해양 자료는 소계 표지까지 포함해 134,133/134,208 저장값 일치·해석 오류 95건입니다. 계산·이력 전용 시험 72개가 통과했으며, 남은 불일치와 원본 실행 미확인은 별개로 유지합니다.
|
||||
|
||||
**전체 원본 동등성은 아직 확보하지 못했습니다.** 기성·물가변동·연차 등은 분석 분기와 합성 시나리오를 시험했으며 원본 업무 샘플 대조는 없습니다. 원본 화면 실행 비교는 못했습니다. 원본 페이지·도면 배치, CAD/VEC0650 편집, 원본 인쇄 서식, Excel/XML/JDL 변환, ESTX 수정 저장은 미구현입니다. 원본 대체품의 완료 판정으로 보지 않습니다.
|
||||
|
||||
|
||||
@@ -18,19 +18,21 @@ const EstText = (() => {
|
||||
.replace(/[;;]/g, ":");
|
||||
function expression(text) {
|
||||
return clean(
|
||||
String(text).replace(
|
||||
/\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\{[^}]*\}|〔[^〕]*〕|[[^]]*]|\([가-힣][^()]*\)|\(\s*\d+(?:\.\d+)?\s*[˚°]\s*\)|<[^>]*>/g,
|
||||
"",
|
||||
),
|
||||
String(text)
|
||||
.replace(///g, "/")
|
||||
.replace(
|
||||
/\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\{[^}]*\}|〔[^〕]*〕|[[^]]*]|\[[가-힣][^\]]*\]|\([가-힣][^()]*\)|\(\s*\d+(?:\.\d+)?\s*[˚°]\s*\)|<[^>]*>/g,
|
||||
"",
|
||||
),
|
||||
)
|
||||
.replace(
|
||||
/(?<![A-Za-z0-9_가-힣.])(\d[\d,.]*|\.\d+)\s*(?:공\/?㎥|공|단|면|겹|경간|개월|월|년|인|㎥|㎢|㎠|㎡|hr|sec|min|set|kwh|kw|km|ton|kg|㎏|㎞|ℓ|회|개소|개|대|분|초|본|장|매|병|조|식|일|시간|원|포|동|주|톤|배|번|줄|μ|₩|EA|㎜|㎝|mm|cm|m3|m2|m|g|L|a)(?:\/(?:개소|경간|개|대|본|인|분|포|매|주|톤|공|회|조|span|set|ton|\$|hr|일|㎥|㎡|m3|m2|m|a|EA))?/gi,
|
||||
/(?<![A-Za-z0-9_가-힣.])(\d[\d,.]*|\.\d+)\s*(?:공\/?㎥|공|단|면|겹|경간|개월|월|년|인|㎥|㎢|㎠|㎡|hr|sec|min|set|kwh|kw|km|ton|kg|㎏|㎞|ℓ|회|개소|개|대|분|초|본|장|매|병|조|식|일|시간|원|포|동|주|톤|배|번|줄|μ|₩|₩|\\|EA|㎜|㎝|mm|cm|m3|m2|m|g|L|a)(?:\/(?:개소|경간|개|대|본|인|분|포|매|주|톤|공|회|조|span|set|ton|\$|hr|일|㎥|㎡|m3|m2|m|a|EA))?/gi,
|
||||
"$1",
|
||||
)
|
||||
.replace(/(?<=\d)\s*\/\s*(?:일|㎥|㎡|hr)(?=\s|$|[×*+)])/gi, "")
|
||||
.replace(/[】]〕]\s*$/, "")
|
||||
.replace(/(?<=\))\s*(?:개|인|본|면|단|회|㎥|㎡)(?:\/(?:㎡|㎥|hr))?/g, "")
|
||||
.replace(/^\s*[-*•]?\s*[가-힣][가-힣\s]*\s+(?=[\d.(])/, "")
|
||||
.replace(/^\s*[-*•※]?\s*[가-힣㎡㎥][가-힣㎡㎥\s]*\s+(?=[\d.(])/, "")
|
||||
.replace(/[①-⑳]/g, "")
|
||||
.replace(/=\s*$/, "");
|
||||
}
|
||||
@@ -180,7 +182,8 @@ const EstText = (() => {
|
||||
? EstMath.precision(row)
|
||||
: { digits: 1, method: 2 };
|
||||
const subtotal = floating(
|
||||
sumCurrency(values),
|
||||
// f251d5-f251df accumulates Extended values before applying the row precision.
|
||||
values.reduce((total, value) => total + value, 0),
|
||||
options.CostValueTrunc && sc < 0 ? 0 : precision.digits,
|
||||
precision.method,
|
||||
);
|
||||
@@ -318,9 +321,9 @@ const EstText = (() => {
|
||||
? []
|
||||
: [
|
||||
...assignmentText.matchAll(
|
||||
/(?:^\s*| {2,}|[,,]\s*|(?<=[가-힣])\s+|[::]\s*|['"]\s*)([A-Za-z][A-Za-z0-9]*)\s*=/g,
|
||||
/(?:^\s*| {2,}|[,,]\s*|(?<=[가-힣])\s+|(?<=hr)\s+|[::]\s*|['"]\s*)([A-Za-z][A-Za-z0-9]*)\s*=/g,
|
||||
),
|
||||
];
|
||||
].filter((match) => !/[+*/÷×−-]\s*$/.test(assignmentText.slice(0, match.index)));
|
||||
for (let j = 0; j < assignments.length; j++) {
|
||||
const assignment = assignments[j],
|
||||
end = assignments[j + 1]?.index ?? text.length;
|
||||
@@ -336,6 +339,10 @@ const EstText = (() => {
|
||||
continue;
|
||||
}
|
||||
const result = body.match(marker);
|
||||
if (!result && /\d\s*[~~]\s*\d/.test(body)) {
|
||||
delete variables[assignment[1]];
|
||||
continue;
|
||||
}
|
||||
const parts = body.split("=");
|
||||
body = parts[0];
|
||||
body = expression(body)
|
||||
@@ -385,6 +392,7 @@ const EstText = (() => {
|
||||
.filter((x) => x.trim())
|
||||
.pop();
|
||||
if (body.includes(":")) body = body.slice(body.indexOf(":") + 1);
|
||||
if (/^\s*\{[\s\S]*\}\s*$/.test(text)) body = body.replace(/^\s*\{/, "");
|
||||
body = expression(body)
|
||||
.replace(/\s*([×÷·+*/−-])\s*/g, "$1")
|
||||
.trim()
|
||||
|
||||
Reference in New Issue
Block a user