fix(est): parse marine quantity formulas
This commit is contained in:
@@ -244,6 +244,80 @@ def test_fullwidth_unit_slash_and_ground_level_annotations():
|
||||
assert result["rows"][4]["TC"] == 35.32
|
||||
|
||||
|
||||
def test_marine_units_and_bullet_are_treated_as_labels():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'marine'},
|
||||
{LineData:' q = 2인 ÷12구멍 = \x120.17\x12 인/구멍'},
|
||||
{LineData:' ⊙ 480분 ÷ 88.42 = \x125.43\x12 본/일'},
|
||||
{LineData:' 노무비 : \x03156,379\x03 ÷ \x11122.66\x11 × 1척 = \x121,274.8\x12', SCheck:-1},
|
||||
{LineData:' ◎ 목재길이 L = 5.2m/EA × 3열 × 2단 × 3구역 = \x1293.6\x12 m'}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert [row["TC"] for row in result["rows"][1:]] == [0.17, 5.43, 1274.8, 93.6]
|
||||
|
||||
|
||||
def test_arrow_to_sketch_is_not_a_fraction():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'sketch'},
|
||||
{LineData:' L = 5.2m ⇒ ───────'},
|
||||
{LineData:' Q = L × 2 = \x1210.4\x12'}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert result["rows"][2]["TC"] == 10.4
|
||||
|
||||
|
||||
def test_greek_variables_and_implicit_multiplication_in_marine_formula():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'greek'},
|
||||
{LineData:' α = 1.0'},
|
||||
{LineData:' β = 0.6'},
|
||||
{LineData:' ι = 14.0 N = 7'},
|
||||
{LineData:' Tb = 0.08×α×β×ι (N + 2)'},
|
||||
{LineData:' Tc = Tb × 2 = \x1212.10\x12'}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert result["rows"][5]["TC"] == 12.1
|
||||
|
||||
|
||||
def test_parenthesized_variable_label_assigns_explicit_value():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'label'},
|
||||
{LineData:' 기타시간 (Te) : 10분'},
|
||||
{LineData:' Tc = Te × 2 = \x1220\x12'}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert result["rows"][2]["TC"] == 20
|
||||
|
||||
|
||||
def test_braced_arithmetic_and_survey_point_units():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'brace'},
|
||||
{LineData:' Q1 = { 6.0M×0.0002EA/M + 2.3M×0.0005EA/M + 1M×0.0005EA/M }'},
|
||||
{LineData:' Q = Q1 × 10000 = \x1228.5\x12'},
|
||||
{LineData:' 노무비 : \x03295,138\x03×3지점÷3지점×3인×4회 = \x123,541,656\x12',SCheck:-1},
|
||||
{LineData:' B{적재량} = 1500/11.5{5㎥×2.3} = \x12130.43\x12'}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert result["rows"][2]["TC"] == 28.5
|
||||
assert result["rows"][3]["TC"] == 3541656
|
||||
assert result["rows"][4]["TC"] == 130.43
|
||||
|
||||
|
||||
def test_spaced_metre_unit_is_not_a_second_variable_declaration():
|
||||
result = run(
|
||||
"math",
|
||||
|
||||
@@ -55,6 +55,8 @@ Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요
|
||||
|
||||
이 보완 후 해양 자료는 소계 표지까지 포함해 134,133/134,208 저장값 일치·해석 오류 95건입니다. 계산·이력·연차 전용 시험 74개가 통과했으며, 남은 불일치와 원본 실행 미확인은 별개로 유지합니다.
|
||||
|
||||
이후 해양 자료에 나타나는 단위·도식 표기, 그리스 문자 변수, 괄호 앞 생략 곱셈과 중괄호 산식을 처리해 저장값 134,159/134,208 일치·해석 오류 37건으로 개선했습니다. 원본에서 선언값과 숫자 대입식의 부호가 충돌하거나 변수가 누락된 사례는 임의로 수정하지 않았습니다.
|
||||
|
||||
2026-11-06 지정 범위인 설계변경·기성·물가변동·연차의 웹앱 구현을 완료했습니다. 설계변경은 당초 보존·재계산 0/1/2와 신규·삭제·변경 비교를 제공합니다. 삭제된 당초/기성 행 번호를 새 행에 재사용하지 않습니다.
|
||||
|
||||
기성은 수량·금액·비율·비목별 입력, 사정 승인, 전회·금회·누계 원가를 계산합니다. 확정 회차는 당시 계약 자료를 보존하고 서버 저장 시 결과를 재검산합니다. 전회 수정은 이후 회차의 확정도 해제하며, 전회 미확정 상태에서 다음 회차를 확정할 수 없습니다.
|
||||
|
||||
@@ -9,12 +9,13 @@ const EstExpression = (() => {
|
||||
.replace(/%/g, "%")
|
||||
.replace(/√\s*\(/g, "SQRT(")
|
||||
.replace(/π/g, "PI")
|
||||
.replace(/(?<=[\d\u0370-\u03ff])\s*(?=\()/g, "*")
|
||||
.replace(/(?<=[\d.)])\s*[xX]\s*(?=[\d.(A-Za-z])/g, "*")
|
||||
.replace(/(?<=[A-Z])x(?=[A-Z])/g, "*")
|
||||
.replace(/(?<=\d),(?=\d{3}(?:\D|$))/g, "");
|
||||
const tokens = [];
|
||||
const pattern =
|
||||
/\s+|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?|\[[^\]]+\]|[A-Za-z_가-힣][A-Za-z_0-9가-힣.]*|[+*/^%(),:~=-]/y;
|
||||
/\s+|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?|\[[^\]]+\]|[A-Za-z_가-힣\u0370-\u03ff][A-Za-z_0-9가-힣\u0370-\u03ff.]*|[+*/^%(),:~=-]/y;
|
||||
let at = 0;
|
||||
while (at < text.length) {
|
||||
pattern.lastIndex = at;
|
||||
|
||||
@@ -16,9 +16,19 @@ const EstText = (() => {
|
||||
String(text)
|
||||
.replace(/\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\([가-힣][^()]*\)/g, "")
|
||||
.replace(/[;;]/g, ":");
|
||||
const mathBraces = (text) =>
|
||||
String(text).replace(/\{([^{}]*)\}/g, (_, content, offset, source) =>
|
||||
(!source.slice(0, offset).trimEnd() ||
|
||||
/[=+×÷*/(]$/.test(source.slice(0, offset).trimEnd())) &&
|
||||
/\d/.test(content) &&
|
||||
/[+×÷*/]/.test(content) &&
|
||||
!/[가-힣]/.test(content)
|
||||
? `(${content})`
|
||||
: "",
|
||||
);
|
||||
function expression(text) {
|
||||
return clean(
|
||||
String(text)
|
||||
mathBraces(text)
|
||||
.replace(///g, "/")
|
||||
.replace(
|
||||
/\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\{[^}]*\}|〔[^〕]*〕|[[^]]*]|\[[가-힣][^\]]*\]|\([가-힣][^()]*\)|\(\s*\d+(?:\.\d+)?\s*[˚°]\s*\)|<[^>]*>/g,
|
||||
@@ -26,13 +36,14 @@ const EstText = (() => {
|
||||
),
|
||||
)
|
||||
.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|knot|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(/^\s*[⊙◎]\s*/, "")
|
||||
.replace(/[①-⑳]/g, "")
|
||||
.replace(/=\s*$/, "");
|
||||
}
|
||||
@@ -254,7 +265,14 @@ const EstText = (() => {
|
||||
let componentDeclaration;
|
||||
try {
|
||||
if (selectionError) throw Error(selectionError);
|
||||
if ((matched || /^\s*[A-Za-z][A-Za-z0-9]*\s*=/.test(text)) && /─/.test(text)) {
|
||||
if (!matched)
|
||||
for (const m of text.matchAll(/\(([A-Za-z][A-Za-z0-9]*)\)\s*[::]\s*([-+]?\d[\d,.]*)/g))
|
||||
variables[m[1]] = numeric(m[2]);
|
||||
if (
|
||||
(matched || /^\s*[A-Za-z][A-Za-z0-9]*\s*=/.test(text)) &&
|
||||
/─/.test(text) &&
|
||||
!(!matched && /⇒.*─/.test(text))
|
||||
) {
|
||||
const above = clean(rows[index - 1]?.LineData),
|
||||
below = clean(rows[index + 1]?.LineData);
|
||||
const columns = (value) =>
|
||||
@@ -321,15 +339,16 @@ const EstText = (() => {
|
||||
? []
|
||||
: [
|
||||
...assignmentText.matchAll(
|
||||
/(?:^\s*| {2,}|[,,]\s*|(?<=[가-힣])\s+|(?<=hr)\s+|[::]\s*|['"]\s*)([A-Za-z][A-Za-z0-9]*)\s*=/g,
|
||||
/(?:^\s*| {2,}|[,,]\s*|(?<=[가-힣])\s+|(?<=hr)\s+|[::]\s*|['"]\s*)([A-Za-z\u0370-\u03ff][A-Za-z0-9\u0370-\u03ff]*)\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;
|
||||
let body = text.slice(assignment.index + assignment[0].length, end);
|
||||
if (!matched && /⇒.*─/.test(body)) body = body.split("⇒")[0];
|
||||
if (!matched && /^\s*\*.*\*\s*$/.test(text)) body = body.replace(/\*\s*$/, "");
|
||||
body = body.replace(/\{[^}]*\}/g, "");
|
||||
body = mathBraces(body);
|
||||
if (/['"]\s*[A-Za-z][A-Za-z0-9]*\s*=/.test(assignment[0]))
|
||||
body = body.replace(/['"]\s*$/, "");
|
||||
if (!matched) body = body.split("{")[0];
|
||||
|
||||
Reference in New Issue
Block a user