33 lines
4.7 KiB
Python
33 lines
4.7 KiB
Python
import json,re,hashlib,zipfile,warnings
|
||
from pathlib import Path
|
||
import openpyxl
|
||
warnings.filterwarnings('ignore',category=UserWarning)
|
||
root=Path(__file__).resolve().parent.parent
|
||
source=root/'교각수량산출.xlsx';w=openpyxl.load_workbook(source,data_only=False);v=openpyxl.load_workbook(source,data_only=True)
|
||
data={s.title:{c.coordinate:{'value':c.value,'cached':v[s.title][c.coordinate].value,'format':c.number_format} for row in s for c in row if c.value is not None} for s in w}
|
||
variables=[]
|
||
for r in range(18,47):
|
||
variables.append(dict(key=f'r{r}',row=r,name=w['3.2 관련변수'][f'G{r}'].value,symbol=w['3.2 관련변수'][f'L{r}'].value,unit='%' if r==28 else '개' if r in (39,42) else 'm'))
|
||
def merged_label(sheet,column,row):
|
||
cell=sheet.cell(row,column)
|
||
for merged in sheet.merged_cells.ranges:
|
||
if merged.min_row<=row<=merged.max_row and merged.min_col<=column<=merged.max_col:return sheet.cell(merged.min_row,merged.min_col).value
|
||
return cell.value
|
||
rows=[]
|
||
for r in list(range(6,32))+list(range(50,77)):
|
||
s=w['3.3 수량산출_P1'];b=merged_label(s,2,r);e=merged_label(s,5,r);g=merged_label(s,7,r)
|
||
rows.append(dict(row=r,cell=f'W{r}',name=' / '.join(str(x).replace('\n',' ') for x in [b,e,g] if x),unit=s[f'J{r}'].value,description=s[f'K{r}'].value))
|
||
meta=dict(sha256=hashlib.sha256(source.read_bytes()).hexdigest(),sheets=[dict(name=s.title,state=s.sheet_state,rows=s.max_row,cols=s.max_column,formulas=sum(c.data_type=='f' for row in s for c in row),merged=len(s.merged_cells.ranges),print_area=str(s.print_area),hidden_rows=[i for i,d in s.row_dimensions.items() if d.hidden],hidden_columns=[i for i,d in s.column_dimensions.items() if d.hidden]) for s in w],names={k:str(x.attr_text) for k,x in w.defined_names.items()},external_links=len(w._external_links),macros=any('vbaProject' in n for n in zipfile.ZipFile(source).namelist()))
|
||
(root/'template-data.ts').write_text('export type Cell={value:string|number|boolean|null;cached:string|number|boolean|null;format:string};\nexport const template:Record<string,Record<string,Cell>>='+json.dumps(data,ensure_ascii=False,separators=(',',':'),default=str)+';\nexport const variables='+json.dumps(variables,ensure_ascii=False)+';\nexport const quantityRows='+json.dumps(rows,ensure_ascii=False)+';\nexport const templateHash='+json.dumps(meta['sha256'])+';\n',encoding='utf8')
|
||
(root/'docs/template-analysis.json').write_text(json.dumps(meta,ensure_ascii=False,indent=2),encoding='utf8')
|
||
md=['# 원본 수량산출서 분석','',f"원본 SHA256: `{meta['sha256']}`",'', '## 시트 구성','']
|
||
for s in meta['sheets']:md.append(f"- {s['name']}: {s['rows']}행 × {s['cols']}열, 수식 {s['formulas']}개, 병합 {s['merged']}개, {s['state']}")
|
||
md+=['','## 최소 입력과 원본 수식 매핑','','|변수|기호|단위|입력셀(P1)|','|---|---|---|---|']
|
||
for x in variables:md.append(f"|{x['name']}|{x['symbol']}|{x['unit']}|3.2 관련변수!N{x['row']}|")
|
||
md+=['','## 수량항목','','|항목|필요 입력(직접 참조)|실제 수식/고정값|원본 위치|','|---|---|---|---|']
|
||
for r in rows:
|
||
f=data['3.3 수량산출_P1'][r['cell']]['value'];refs=sorted(set(re.findall(r'\$?BG\$?\d+',str(f))))
|
||
md.append(f"|{r['name']}|{', '.join(refs)}|`{f}`|3.3 수량산출_P1!{r['cell']}|")
|
||
md+=['','## 원본의 중요한 제약','','- 설명식과 실제 수식이 불일치하는 부분을 수정하지 않는다. W51은 표시 Hcol 설명과 달리 Zb-H1-H2-Zs+Zf를 사용한다. 원본 P1 값은 236.80068785698427㎡이다. 기둥 실제 높이와 일치한다고 간주하지 않는다.','- AO8은 풍화암 BG10을 사용하고 AO12는 연암 BG11을 사용한다. 그대로 재현한다.','- W50 설명에는 2(Ac1+Ac2+Ac3)이 있으나 실제 수식은 해당 부분에 계수 2가 없다. W58과 공유되는 곡률 ASIN 식을 그대로 보존한다.','- W63의 규격 표기는 T=300mm이지만 수식은 0.05m를 사용한다. 규격 텍스트와 계산 기준을 검토해야 한다.','- W67은 수식이 아닌 철근량 직접값, K68/K69/K74는 시험/시설 횟수 직접값이다. 자동 수량으로 가장하지 않는다.','- 실제 수식에 ROUND 계열은 없다. 원값으로 계산 후 원본 표시형식(상세 주로 소수3자리, 집계 주로2자리)만 적용한다.','- 13시트 모두 visible, 데이터 유효성 규칙 없음, 외부통합문서 링크와 매크로 없음. 이름 정의 3개는 원본 보존하며 계산 수식에서 사용되지 않는다.','- 토공 심도는 0~8m 구간만 있다. 8m 초과는 원본 적용 범위 밖이다.','- 단일 원형기둥·단일 직사각형기초·균일 말뚝의 곡률 두부보 산식이다. 다기둥/꺾인/비대칭 구조의 수량을 임의 배수로 확장하지 않는다.']
|
||
(root/'docs/ANALYSIS.md').write_text('\n'.join(md),encoding='utf8')
|