excel-codegen 0.9.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.
- excel_codegen/__init__.py +160 -0
- excel_codegen/__main__.py +8 -0
- excel_codegen/cli.py +1250 -0
- excel_codegen/derived.py +465 -0
- excel_codegen/example_pack.py +107 -0
- excel_codegen/examples/README.md +69 -0
- excel_codegen/examples/abs_fpi/ABS_FPI_load_cases.xlsx +0 -0
- excel_codegen/examples/abs_fpi/ABS_FPI_load_cases_render.bat +22 -0
- excel_codegen/examples/abs_fpi/ABS_FPI_load_cases_render.sh +23 -0
- excel_codegen/examples/abs_fpi/README.md +121 -0
- excel_codegen/examples/abs_fpi/abs_fpi.yaml +310 -0
- excel_codegen/examples/abs_fpi/abs_fpi_external.xlsx +0 -0
- excel_codegen/examples/abs_fpi/abs_fpi_external.yaml +162 -0
- excel_codegen/examples/abs_fpi/abs_fpi_external_render.bat +22 -0
- excel_codegen/examples/abs_fpi/abs_fpi_external_render.sh +23 -0
- excel_codegen/examples/abs_fpi/abs_fpi_internal.xlsx +0 -0
- excel_codegen/examples/abs_fpi/abs_fpi_internal.yaml +266 -0
- excel_codegen/examples/abs_fpi/abs_fpi_internal_render.bat +22 -0
- excel_codegen/examples/abs_fpi/abs_fpi_internal_render.sh +23 -0
- excel_codegen/examples/abs_fpi/compose.yaml +20 -0
- excel_codegen/examples/abs_fpi/generated/COT1-d20.559-mu60.js +124 -0
- excel_codegen/examples/abs_fpi/generated/COT1-d20.559-mu60_summary.md +30 -0
- excel_codegen/examples/abs_fpi/generated/EXT-T15-mu0-kf-1.js +91 -0
- excel_codegen/examples/abs_fpi/generated/EXT-T15-mu0-kf-1_summary.md +23 -0
- excel_codegen/examples/abs_fpi/generated/EXT-T15-mu90-kf+1.js +91 -0
- excel_codegen/examples/abs_fpi/generated/EXT-T15-mu90-kf+1_summary.md +23 -0
- excel_codegen/examples/abs_fpi/generated/EXT-T20.559-mu90-kf+1.js +91 -0
- excel_codegen/examples/abs_fpi/generated/EXT-T20.559-mu90-kf+1_summary.md +23 -0
- excel_codegen/examples/abs_fpi/generated/WBT6-d15.059-mu0.js +124 -0
- excel_codegen/examples/abs_fpi/generated/WBT6-d15.059-mu0_summary.md +30 -0
- excel_codegen/examples/abs_fpi/generated/WBT6-d8-mu90.js +124 -0
- excel_codegen/examples/abs_fpi/generated/WBT6-d8-mu90_summary.md +30 -0
- excel_codegen/examples/abs_fpi/generated/WBT7-d20.559-mu90.js +124 -0
- excel_codegen/examples/abs_fpi/generated/WBT7-d20.559-mu90_summary.md +30 -0
- excel_codegen/examples/abs_fpi/templates/ext_body.js.j2 +91 -0
- excel_codegen/examples/abs_fpi/templates/ext_summary.md.j2 +23 -0
- excel_codegen/examples/abs_fpi/templates/int_body.js.j2 +124 -0
- excel_codegen/examples/abs_fpi/templates/int_summary.md.j2 +30 -0
- excel_codegen/examples/basic/example.yaml +97 -0
- excel_codegen/examples/basic/example_formula.yaml +86 -0
- excel_codegen/examples/basic/generated/uart_init_Case1.c +6 -0
- excel_codegen/examples/basic/generated/uart_init_Case2.c +6 -0
- excel_codegen/examples/basic/generated/uart_summary_Case1.md +4 -0
- excel_codegen/examples/basic/generated/uart_summary_Case2.md +4 -0
- excel_codegen/examples/basic/generated_formula/uart_init_Case1.c +5 -0
- excel_codegen/examples/basic/generated_formula/uart_init_Case2.c +5 -0
- excel_codegen/examples/basic/generated_formula/uart_pins_Case1.csv +1 -0
- excel_codegen/examples/basic/generated_formula/uart_pins_Case2.csv +1 -0
- excel_codegen/examples/basic/template.xlsx +0 -0
- excel_codegen/examples/basic/template_formula.xlsx +0 -0
- excel_codegen/examples/basic/template_formula_render.bat +22 -0
- excel_codegen/examples/basic/template_formula_render.sh +23 -0
- excel_codegen/examples/basic/template_render.bat +22 -0
- excel_codegen/examples/basic/template_render.sh +23 -0
- excel_codegen/examples/nastran/generated/case_control.deck +18 -0
- excel_codegen/examples/nastran/generated/cc_01_LC1.inc +8 -0
- excel_codegen/examples/nastran/generated/cc_02_LC2.inc +8 -0
- excel_codegen/examples/nastran/generated/cc_03_LC3.inc +8 -0
- excel_codegen/examples/nastran/generated/cc_04_COMB4.inc +8 -0
- excel_codegen/examples/nastran/generated/cc_05_LC5.inc +8 -0
- excel_codegen/examples/nastran/generated/cc_summary_01_LC1.md +1 -0
- excel_codegen/examples/nastran/generated/cc_summary_02_LC2.md +1 -0
- excel_codegen/examples/nastran/generated/cc_summary_03_LC3.md +1 -0
- excel_codegen/examples/nastran/generated/cc_summary_04_COMB4.md +1 -0
- excel_codegen/examples/nastran/generated/cc_summary_05_LC5.md +1 -0
- excel_codegen/examples/nastran/nastran_case_control.xlsx +0 -0
- excel_codegen/examples/nastran/nastran_case_control.yaml +141 -0
- excel_codegen/examples/nastran/nastran_case_control_render.bat +22 -0
- excel_codegen/examples/nastran/nastran_case_control_render.sh +23 -0
- excel_codegen/excel_io.py +1885 -0
- excel_codegen/formula.py +674 -0
- excel_codegen/formula_eval.py +685 -0
- excel_codegen/jinja_env.py +92 -0
- excel_codegen/models.py +1008 -0
- excel_codegen/renderer.py +558 -0
- excel_codegen/utils.py +225 -0
- excel_codegen-0.9.0.dist-info/METADATA +179 -0
- excel_codegen-0.9.0.dist-info/RECORD +82 -0
- excel_codegen-0.9.0.dist-info/WHEEL +5 -0
- excel_codegen-0.9.0.dist-info/entry_points.txt +2 -0
- excel_codegen-0.9.0.dist-info/licenses/LICENSE +21 -0
- excel_codegen-0.9.0.dist-info/top_level.txt +1 -0
excel_codegen/derived.py
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""派生参数(``derived:``):让一个参数直接引用**同 Case 的其他参数**与**全局参数**。
|
|
2
|
+
|
|
3
|
+
用途
|
|
4
|
+
----
|
|
5
|
+
* 中间量:``h_de = k_c * h_di``,模板里直接 ``{{ h_de }}``,不用把这行算式写进模板;
|
|
6
|
+
* 需要"看得到数值"的换算结果:参数表里就能读出它,不必等生成完代码再看;
|
|
7
|
+
* 链式:派生参数可以引用派生参数(``a -> b -> c``),工具按依赖顺序求值。
|
|
8
|
+
|
|
9
|
+
限制(**只按需求做这两条**)
|
|
10
|
+
--------------------------
|
|
11
|
+
* 只能引用 ``global`` 与**同一个 Case** 的 ``local``;不跨 Case、不跨工作簿;
|
|
12
|
+
* ``global`` 的派生参数不能引用 ``local``(那时还没有"当前 Case"这个概念)。
|
|
13
|
+
|
|
14
|
+
两条路径,一套语义
|
|
15
|
+
------------------
|
|
16
|
+
* **Python 侧**(快照渲染 / 导出文件 / 指纹):用 ``jinja_env`` 的环境求值,拿真正的值;
|
|
17
|
+
* **Excel 侧**(参数表里那一格):能翻译成 Excel 公式的就写公式(改输入自动重算),
|
|
18
|
+
翻译不了(用了 ``|`` 过滤器里没有对应的、条件表达式以外的花活……)就把算好的值写进去,
|
|
19
|
+
并告警"改输入后需要重跑 --write-excel"。
|
|
20
|
+
|
|
21
|
+
翻译覆盖的子集:``+ - * / **``、``%``(-> ``MOD``)、``~``(-> ``&``,字符串拼接)、
|
|
22
|
+
括号、数字/字符串/布尔常量、变量、``min/max/abs/round/int/float``、
|
|
23
|
+
``|round|abs|int|float``、比较运算、``and/or/not``、条件表达式(-> ``IF``)。
|
|
24
|
+
其余一律报错(不猜),由调用方降级。
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from collections.abc import Iterable, Mapping, MutableMapping, Sequence
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from jinja2 import Environment, TemplateError, TemplateSyntaxError, UndefinedError, meta, nodes
|
|
33
|
+
|
|
34
|
+
from .jinja_env import build_environment
|
|
35
|
+
from .models import ProjectConfig, VariableDef
|
|
36
|
+
from .utils import RenderError, to_text
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"DerivedError",
|
|
40
|
+
"DerivedNotTranslatable",
|
|
41
|
+
"derived_variables",
|
|
42
|
+
"evaluate_derived",
|
|
43
|
+
"expression_names",
|
|
44
|
+
"is_derived",
|
|
45
|
+
"is_translatable",
|
|
46
|
+
"ordered_derived",
|
|
47
|
+
"to_excel",
|
|
48
|
+
"untranslatable_names",
|
|
49
|
+
"validate_config",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DerivedError(RenderError):
|
|
54
|
+
"""派生参数写得不对:语法错、引用越界、循环引用、类型不对……"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DerivedNotTranslatable(DerivedError):
|
|
58
|
+
"""表达式本身没错,但超出"能翻译成 Excel 公式"的子集。
|
|
59
|
+
|
|
60
|
+
与 :class:`DerivedError` 分开,是为了让调用方**只**对这种情况降级
|
|
61
|
+
(往格子里写算好的值),而不是把语法错误也静默吞掉。
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def is_derived(variable: VariableDef) -> bool:
|
|
66
|
+
return bool(variable.derived)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def derived_variables(variables: Iterable[VariableDef]) -> list[VariableDef]:
|
|
70
|
+
return [variable for variable in variables if is_derived(variable)]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# --------------------------------------------------------------------------- #
|
|
74
|
+
# 表达式:名字与顺序
|
|
75
|
+
# --------------------------------------------------------------------------- #
|
|
76
|
+
def expression_names(expression: str, *, env: Environment | None = None) -> set[str]:
|
|
77
|
+
"""表达式里引用到的变量名。"""
|
|
78
|
+
environment = env or build_environment()
|
|
79
|
+
try:
|
|
80
|
+
ast = environment.parse("{{ " + expression + " }}")
|
|
81
|
+
except TemplateSyntaxError as exc:
|
|
82
|
+
raise DerivedError(f"派生表达式语法错误(第 {exc.lineno} 行):{exc.message};表达式:{expression!r}") from exc
|
|
83
|
+
return set(meta.find_undeclared_variables(ast))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def ordered_derived(
|
|
87
|
+
variables: Sequence[VariableDef],
|
|
88
|
+
*,
|
|
89
|
+
scope: str,
|
|
90
|
+
env: Environment | None = None,
|
|
91
|
+
) -> list[VariableDef]:
|
|
92
|
+
"""按依赖顺序排列派生参数;有环就报错(带上环上的名字)。"""
|
|
93
|
+
environment = env or build_environment()
|
|
94
|
+
derived = {variable.name: variable for variable in derived_variables(variables)}
|
|
95
|
+
if not derived:
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
deps = {
|
|
99
|
+
name: expression_names(variable.derived or "", env=environment) & derived.keys()
|
|
100
|
+
for name, variable in derived.items()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ordered: list[VariableDef] = []
|
|
104
|
+
done: set[str] = set()
|
|
105
|
+
while len(ordered) < len(derived):
|
|
106
|
+
ready = [name for name in derived if name not in done and deps[name] <= done]
|
|
107
|
+
if not ready:
|
|
108
|
+
cycle = _find_cycle(deps)
|
|
109
|
+
raise DerivedError(
|
|
110
|
+
f"{scope} 的派生参数存在循环引用:{' -> '.join(cycle)};"
|
|
111
|
+
"派生参数只能引用同 Case 的其他参数与全局参数,不能互相依赖成环"
|
|
112
|
+
)
|
|
113
|
+
for name in sorted(ready):
|
|
114
|
+
ordered.append(derived[name])
|
|
115
|
+
done.add(name)
|
|
116
|
+
return ordered
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _find_cycle(deps: Mapping[str, set[str]]) -> list[str]:
|
|
120
|
+
"""从依赖图里挑出一个环,用于报错信息。"""
|
|
121
|
+
visiting: list[str] = []
|
|
122
|
+
seen: set[str] = set()
|
|
123
|
+
|
|
124
|
+
def walk(node: str) -> list[str] | None:
|
|
125
|
+
if node in visiting:
|
|
126
|
+
return [*visiting[visiting.index(node) :], node]
|
|
127
|
+
if node in seen:
|
|
128
|
+
return None
|
|
129
|
+
visiting.append(node)
|
|
130
|
+
for dep in sorted(deps.get(node, ())):
|
|
131
|
+
found = walk(dep)
|
|
132
|
+
if found:
|
|
133
|
+
return found
|
|
134
|
+
visiting.pop()
|
|
135
|
+
seen.add(node)
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
for name in sorted(deps):
|
|
139
|
+
found = walk(name)
|
|
140
|
+
if found:
|
|
141
|
+
return found
|
|
142
|
+
return ["<未知>"]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# --------------------------------------------------------------------------- #
|
|
146
|
+
# 求值(Python 侧)
|
|
147
|
+
# --------------------------------------------------------------------------- #
|
|
148
|
+
def evaluate_derived(
|
|
149
|
+
variables: Sequence[VariableDef],
|
|
150
|
+
context: MutableMapping[str, Any],
|
|
151
|
+
*,
|
|
152
|
+
scope: str,
|
|
153
|
+
env: Environment | None = None,
|
|
154
|
+
forbidden: Mapping[str, str] | None = None,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""按依赖顺序求值派生参数,结果写进 ``context``(原地更新)。
|
|
157
|
+
|
|
158
|
+
:param scope: 出错信息里用的作用域描述,例如 ``"global"`` / ``"Case1 的 local"``
|
|
159
|
+
:param forbidden: ``{变量名: 说明}``;表达式引用到这些名字就报错
|
|
160
|
+
(用来实现"global 派生参数不能引用 local 变量")
|
|
161
|
+
"""
|
|
162
|
+
environment = env or build_environment()
|
|
163
|
+
forbidden = forbidden or {}
|
|
164
|
+
for variable in ordered_derived(variables, scope=scope, env=environment):
|
|
165
|
+
expression = variable.derived or ""
|
|
166
|
+
for name in sorted(expression_names(expression, env=environment)):
|
|
167
|
+
if name in forbidden:
|
|
168
|
+
raise DerivedError(
|
|
169
|
+
f"派生参数 {variable.name!r}({scope})引用了 {name!r}:{forbidden[name]};表达式:{expression!r}"
|
|
170
|
+
)
|
|
171
|
+
context[variable.name] = _evaluate(expression, context, env=environment, variable=variable)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _evaluate(
|
|
175
|
+
expression: str,
|
|
176
|
+
context: Mapping[str, Any],
|
|
177
|
+
*,
|
|
178
|
+
env: Environment,
|
|
179
|
+
variable: VariableDef,
|
|
180
|
+
) -> Any:
|
|
181
|
+
try:
|
|
182
|
+
compiled = env.compile_expression(expression, undefined_to_none=False)
|
|
183
|
+
except TemplateSyntaxError as exc:
|
|
184
|
+
raise DerivedError(
|
|
185
|
+
f"派生参数 {variable.name!r} 的表达式语法错误(第 {exc.lineno} 行):{exc.message};表达式:{expression!r}"
|
|
186
|
+
) from exc
|
|
187
|
+
try:
|
|
188
|
+
return compiled(**context)
|
|
189
|
+
except UndefinedError as exc:
|
|
190
|
+
raise DerivedError(
|
|
191
|
+
f"派生参数 {variable.name!r} 引用了取不到的变量:{exc.message or exc};"
|
|
192
|
+
f"表达式:{expression!r}"
|
|
193
|
+
"(派生参数只能引用 YAML 里定义的 global / 同 Case 的 local)"
|
|
194
|
+
) from exc
|
|
195
|
+
except TemplateError as exc:
|
|
196
|
+
raise DerivedError(f"派生参数 {variable.name!r} 求值失败:{exc};表达式:{expression!r}") from exc
|
|
197
|
+
except Exception as exc:
|
|
198
|
+
raise DerivedError(
|
|
199
|
+
f"派生参数 {variable.name!r} 求值失败:{type(exc).__name__}: {exc};表达式:{expression!r}"
|
|
200
|
+
) from exc
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# --------------------------------------------------------------------------- #
|
|
204
|
+
# 翻译成 Excel 公式
|
|
205
|
+
# --------------------------------------------------------------------------- #
|
|
206
|
+
_BINOPS = {"+": "+", "-": "-", "*": "*", "/": "/", "**": "^"}
|
|
207
|
+
#: Jinja 的比较运算用的是名字(gt / lteq …),不是符号
|
|
208
|
+
_COMPARE = {
|
|
209
|
+
"eq": "=",
|
|
210
|
+
"ne": "<>",
|
|
211
|
+
"gt": ">",
|
|
212
|
+
"lt": "<",
|
|
213
|
+
"gteq": ">=",
|
|
214
|
+
"lteq": "<=",
|
|
215
|
+
"==": "=",
|
|
216
|
+
"!=": "<>",
|
|
217
|
+
">=": ">=",
|
|
218
|
+
"<=": "<=",
|
|
219
|
+
">": ">",
|
|
220
|
+
"<": "<",
|
|
221
|
+
}
|
|
222
|
+
#: 表达式里的函数 -> Excel 函数。``int`` 用 ``TRUNC``:Python 的 ``int()`` 向零截断,
|
|
223
|
+
#: 而 Excel 的 ``INT()`` 是向下取整(``int(-2.5)`` 在两边会差 1)。
|
|
224
|
+
_FUNCS = {"min": "MIN", "max": "MAX", "abs": "ABS", "int": "TRUNC", "float": None}
|
|
225
|
+
_FILTERS = {"abs": "ABS", "int": "TRUNC", "float": None, "string": None}
|
|
226
|
+
|
|
227
|
+
#: 两种语言语义不同的运算:**故意不翻译**(翻译了就会"Excel 里看到的"与"导出的"不一致)
|
|
228
|
+
_NOT_TRANSLATABLE = {
|
|
229
|
+
"round": "Python 的 round() 是银行家舍入(round(2.5) == 2),Excel 的 ROUND() 是四舍五入(2.5 -> 3)",
|
|
230
|
+
"ceil": "Python 的 ceil() 与 Excel 的 CEILING 边界行为不同",
|
|
231
|
+
"floor": "Python 的 floor() 与 Excel 的 FLOOR 边界行为不同(负数)",
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def to_excel(
|
|
236
|
+
expression: str,
|
|
237
|
+
*,
|
|
238
|
+
name: str,
|
|
239
|
+
resolve,
|
|
240
|
+
env: Environment | None = None,
|
|
241
|
+
condition: bool = False,
|
|
242
|
+
) -> str:
|
|
243
|
+
"""把派生表达式翻译成 Excel 公式(不带前导 ``=``)。
|
|
244
|
+
|
|
245
|
+
:param resolve: ``变量名 -> Excel 引用`` 的回调(由调用方决定列/表名与相对/绝对)
|
|
246
|
+
:param condition: 这是不是一个**条件**表达式。为 ``True`` 时 ``and`` / ``or`` / ``not``
|
|
247
|
+
可以翻译成 ``AND`` / ``OR`` / ``NOT``(Python 的 ``and``/``or`` 有返回值语义,
|
|
248
|
+
与 Excel 不同,所以只在条件位置放行)。公式模式的行内 ``{% if %}`` 走这条路。
|
|
249
|
+
:raises DerivedError: 表达式超出可翻译子集(调用方应当降级为"写入算好的值")
|
|
250
|
+
"""
|
|
251
|
+
environment = env or build_environment()
|
|
252
|
+
try:
|
|
253
|
+
ast = environment.parse("{{ " + expression + " }}")
|
|
254
|
+
except TemplateSyntaxError as exc:
|
|
255
|
+
raise DerivedError(f"派生参数 {name!r} 的表达式语法错误:{exc.message};表达式:{expression!r}") from exc
|
|
256
|
+
body = getattr(ast, "body", [])
|
|
257
|
+
if len(body) != 1 or not isinstance(body[0], nodes.Output) or len(body[0].nodes) != 1:
|
|
258
|
+
raise DerivedError(f"派生参数 {name!r} 的表达式不是单个表达式:{expression!r}")
|
|
259
|
+
return _translate(body[0].nodes[0], name=name, resolve=resolve, env=environment, condition=condition)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _translate(node, *, name: str, resolve, env: Environment, condition: bool) -> str:
|
|
263
|
+
if isinstance(node, nodes.Const):
|
|
264
|
+
return _excel_literal(node.value)
|
|
265
|
+
if isinstance(node, nodes.Name):
|
|
266
|
+
return resolve(node.name)
|
|
267
|
+
if isinstance(node, nodes.Getattr):
|
|
268
|
+
# 属性访问(``x.value`` / ``x.prefix`` …):把属性名一并交给调用方 —— 只有它知道
|
|
269
|
+
# 该怎么把一个"取值 / 前缀"映射成单元格引用。老的回调只收一个参数,用 TypeError 兜住。
|
|
270
|
+
inner = node.node
|
|
271
|
+
if not isinstance(inner, nodes.Name):
|
|
272
|
+
raise DerivedNotTranslatable(f"表达式 {name!r} 里的 .{node.attr} 只能接在变量名后面")
|
|
273
|
+
try:
|
|
274
|
+
return resolve(inner.name, node.attr)
|
|
275
|
+
except TypeError as exc:
|
|
276
|
+
raise DerivedNotTranslatable(
|
|
277
|
+
f"表达式 {name!r} 里用了属性访问 .{node.attr},但当前上下文不支持"
|
|
278
|
+
"(属性访问只在公式模式的行内 {% if %} 条件里可用)"
|
|
279
|
+
) from exc
|
|
280
|
+
if isinstance(node, nodes.Concat):
|
|
281
|
+
return "&".join(
|
|
282
|
+
f"({_translate(item, name=name, resolve=resolve, env=env, condition=False)})" for item in node.nodes
|
|
283
|
+
)
|
|
284
|
+
# ⚠ and / or 必须排在 BinExpr **前面**:jinja2 里 ``And`` / ``Or`` 是 ``BinExpr`` 的子类,
|
|
285
|
+
# 先命中 BinExpr 分支的话它们会被当成"不支持的运算符"(这段曾经是死代码,见 0.6.0 修复)。
|
|
286
|
+
if isinstance(node, (nodes.And, nodes.Or)):
|
|
287
|
+
if not condition:
|
|
288
|
+
raise DerivedNotTranslatable(
|
|
289
|
+
f"派生参数 {name!r} 的 and/or 只能用在条件表达式里"
|
|
290
|
+
"(Python 的 and/or 返回值语义与 Excel 不同,避免静默偏差)"
|
|
291
|
+
)
|
|
292
|
+
function = "AND" if isinstance(node, nodes.And) else "OR"
|
|
293
|
+
left = _translate(node.left, name=name, resolve=resolve, env=env, condition=True)
|
|
294
|
+
right = _translate(node.right, name=name, resolve=resolve, env=env, condition=True)
|
|
295
|
+
return f"{function}({left},{right})"
|
|
296
|
+
if isinstance(node, nodes.BinExpr):
|
|
297
|
+
operator = getattr(node, "operator", None)
|
|
298
|
+
left = _translate(node.left, name=name, resolve=resolve, env=env, condition=False)
|
|
299
|
+
right = _translate(node.right, name=name, resolve=resolve, env=env, condition=False)
|
|
300
|
+
if operator == "%":
|
|
301
|
+
return f"MOD({left},{right})"
|
|
302
|
+
if operator == "//":
|
|
303
|
+
raise DerivedNotTranslatable(f"派生参数 {name!r} 用了 //(整除):Excel 没有等价运算,请改用 / 或 round()")
|
|
304
|
+
if operator not in _BINOPS:
|
|
305
|
+
raise DerivedNotTranslatable(f"派生参数 {name!r} 用了不支持的运算符 {operator!r}")
|
|
306
|
+
return f"({left}{_BINOPS[operator]}{right})"
|
|
307
|
+
if isinstance(node, nodes.Neg):
|
|
308
|
+
return f"-({_translate(node.node, name=name, resolve=resolve, env=env, condition=False)})"
|
|
309
|
+
if isinstance(node, nodes.Pos):
|
|
310
|
+
return f"({_translate(node.node, name=name, resolve=resolve, env=env, condition=False)})"
|
|
311
|
+
if isinstance(node, nodes.Not):
|
|
312
|
+
if not condition:
|
|
313
|
+
raise DerivedNotTranslatable(f"派生参数 {name!r} 的 not 只能用在条件表达式里")
|
|
314
|
+
inner_excel = _translate(node.node, name=name, resolve=resolve, env=env, condition=True)
|
|
315
|
+
return f"NOT({inner_excel})"
|
|
316
|
+
if isinstance(node, nodes.Compare):
|
|
317
|
+
return _translate_compare(node, name=name, resolve=resolve, env=env)
|
|
318
|
+
if isinstance(node, nodes.CondExpr):
|
|
319
|
+
test = _translate(node.test, name=name, resolve=resolve, env=env, condition=True)
|
|
320
|
+
yes = _translate(node.expr1, name=name, resolve=resolve, env=env, condition=False)
|
|
321
|
+
no = _translate(node.expr2, name=name, resolve=resolve, env=env, condition=False)
|
|
322
|
+
return f"IF({test},{yes},{no})"
|
|
323
|
+
if isinstance(node, nodes.Filter):
|
|
324
|
+
return _translate_filter(node, name=name, resolve=resolve, env=env)
|
|
325
|
+
if isinstance(node, nodes.Call):
|
|
326
|
+
return _translate_call(node, name=name, resolve=resolve, env=env)
|
|
327
|
+
raise DerivedNotTranslatable(
|
|
328
|
+
f"派生参数 {name!r} 的表达式超出可翻译范围({type(node).__name__}):"
|
|
329
|
+
"能翻译的子集见 docs/template_guide.md §15;"
|
|
330
|
+
"也可以让它留在 Python 侧(表里写入算好的值,改输入后重跑 --write-excel)"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _translate_compare(node, *, name: str, resolve, env: Environment) -> str:
|
|
335
|
+
parts: list[str] = []
|
|
336
|
+
left = _translate(node.expr, name=name, resolve=resolve, env=env, condition=False)
|
|
337
|
+
for operand in node.ops:
|
|
338
|
+
operator = getattr(operand, "op", None)
|
|
339
|
+
right = _translate(operand.expr, name=name, resolve=resolve, env=env, condition=False)
|
|
340
|
+
if operator not in _COMPARE:
|
|
341
|
+
raise DerivedNotTranslatable(f"派生参数 {name!r} 用了不支持的比较运算 {operator!r}")
|
|
342
|
+
parts.append(f"({left}{_COMPARE[operator]}{right})")
|
|
343
|
+
left = right
|
|
344
|
+
return parts[0] if len(parts) == 1 else "AND(" + ",".join(parts) + ")"
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _translate_filter(node, *, name: str, resolve, env: Environment) -> str:
|
|
348
|
+
if node.dyn_args is not None or node.dyn_kwargs is not None:
|
|
349
|
+
raise DerivedNotTranslatable(f"派生参数 {name!r} 的过滤器用了动态参数,无法翻译")
|
|
350
|
+
if node.name in _NOT_TRANSLATABLE:
|
|
351
|
+
raise DerivedNotTranslatable(
|
|
352
|
+
f"派生参数 {name!r} 用了 |{node.name}:{_NOT_TRANSLATABLE[node.name]},"
|
|
353
|
+
"所以不往 Excel 公式翻译(表里会写入 Python 算好的值)"
|
|
354
|
+
)
|
|
355
|
+
function = _FILTERS.get(node.name)
|
|
356
|
+
if function is None and node.name not in _FILTERS:
|
|
357
|
+
raise DerivedNotTranslatable(f"派生参数 {name!r} 用了过滤器 |{node.name},Excel 侧没有对应函数")
|
|
358
|
+
value = _translate(node.node, name=name, resolve=resolve, env=env, condition=False)
|
|
359
|
+
if function is None: # float / string:Excel 里就是原值
|
|
360
|
+
return f"({value})"
|
|
361
|
+
return f"{function}({value})"
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _translate_call(node, *, name: str, resolve, env: Environment) -> str:
|
|
365
|
+
if not isinstance(node.node, nodes.Name):
|
|
366
|
+
raise DerivedNotTranslatable(f"派生参数 {name!r} 调用了非函数对象,无法翻译")
|
|
367
|
+
if node.kwargs and any(kw.key for kw in node.kwargs):
|
|
368
|
+
raise DerivedNotTranslatable(f"派生参数 {name!r} 的函数用了关键字参数,无法翻译")
|
|
369
|
+
if node.dyn_args is not None or node.dyn_kwargs is not None:
|
|
370
|
+
raise DerivedNotTranslatable(f"派生参数 {name!r} 的函数用了 *args/**kwargs,无法翻译")
|
|
371
|
+
|
|
372
|
+
function_name = node.node.name
|
|
373
|
+
if function_name in _NOT_TRANSLATABLE:
|
|
374
|
+
raise DerivedNotTranslatable(
|
|
375
|
+
f"派生参数 {name!r} 用了 {function_name}():{_NOT_TRANSLATABLE[function_name]},"
|
|
376
|
+
"所以不往 Excel 公式翻译(表里会写入 Python 算好的值)"
|
|
377
|
+
)
|
|
378
|
+
if function_name not in _FUNCS:
|
|
379
|
+
raise DerivedNotTranslatable(
|
|
380
|
+
f"派生参数 {name!r} 调用了 {function_name}(),Excel 侧没有对应函数(支持 min/max/abs/int/float)"
|
|
381
|
+
)
|
|
382
|
+
arguments = [_translate(argument, name=name, resolve=resolve, env=env, condition=False) for argument in node.args]
|
|
383
|
+
excel_function = _FUNCS[function_name]
|
|
384
|
+
if excel_function is None: # float():Excel 里数字就是数字
|
|
385
|
+
if len(arguments) != 1:
|
|
386
|
+
raise DerivedNotTranslatable(f"派生参数 {name!r} 的 {function_name}() 只支持一个参数")
|
|
387
|
+
return f"({arguments[0]})"
|
|
388
|
+
if not arguments:
|
|
389
|
+
raise DerivedNotTranslatable(f"派生参数 {name!r} 的 {function_name}() 缺参数")
|
|
390
|
+
return f"{excel_function}({','.join(arguments)})"
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _excel_literal(value: Any) -> str:
|
|
394
|
+
if isinstance(value, bool):
|
|
395
|
+
return "TRUE" if value else "FALSE"
|
|
396
|
+
if isinstance(value, (int, float)):
|
|
397
|
+
return repr(value)
|
|
398
|
+
text = to_text(value)
|
|
399
|
+
return '"' + text.replace('"', '""') + '"'
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def is_translatable(variable: VariableDef, *, env: Environment | None = None) -> bool:
|
|
403
|
+
"""这个派生表达式能不能写成 Excel 公式(不能就只能往格子里写算好的值)。"""
|
|
404
|
+
if not is_derived(variable):
|
|
405
|
+
return True
|
|
406
|
+
try:
|
|
407
|
+
to_excel(variable.derived or "", name=variable.name, resolve=lambda _: "A1", env=env)
|
|
408
|
+
return True
|
|
409
|
+
except DerivedNotTranslatable:
|
|
410
|
+
return False
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def untranslatable_names(config: ProjectConfig, *, env: Environment | None = None) -> list[str]:
|
|
414
|
+
"""哪些派生参数翻译不成 Excel 公式(表里只能写入算好的值)。"""
|
|
415
|
+
environment = env or build_environment()
|
|
416
|
+
return [
|
|
417
|
+
variable.name
|
|
418
|
+
for variable in [*config.global_variables, *config.local_variables]
|
|
419
|
+
if is_derived(variable) and not is_translatable(variable, env=environment)
|
|
420
|
+
]
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def validate_config(config: ProjectConfig, *, env: Environment | None = None) -> list[str]:
|
|
424
|
+
"""配置期的派生参数检查:语法、引用范围、循环;返回警告(不能翻译成 Excel 公式的)。
|
|
425
|
+
|
|
426
|
+
:raises DerivedError: 表达式语法错 / 引用了不该引用的名字 / 循环引用 / 引用了未定义的名字
|
|
427
|
+
"""
|
|
428
|
+
environment = env or build_environment()
|
|
429
|
+
global_names = set(config.global_names)
|
|
430
|
+
local_names = set(config.local_names)
|
|
431
|
+
known = global_names | local_names
|
|
432
|
+
|
|
433
|
+
for variable in derived_variables(config.global_variables):
|
|
434
|
+
for name in sorted(expression_names(variable.derived or "", env=environment)):
|
|
435
|
+
if name in local_names:
|
|
436
|
+
raise DerivedError(
|
|
437
|
+
f"派生参数 {variable.name!r}(global)引用了 local 变量 {name!r}:"
|
|
438
|
+
"global 的派生参数不能引用 local 变量(那时还没有当前 Case)"
|
|
439
|
+
)
|
|
440
|
+
if name not in known:
|
|
441
|
+
raise DerivedError(
|
|
442
|
+
f"派生参数 {variable.name!r}(global)引用了未定义的变量 {name!r};"
|
|
443
|
+
"派生参数只能引用 YAML 里定义的 global / 同 Case 的 local"
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
for variable in derived_variables(config.local_variables):
|
|
447
|
+
for name in sorted(expression_names(variable.derived or "", env=environment)):
|
|
448
|
+
if name not in known:
|
|
449
|
+
raise DerivedError(
|
|
450
|
+
f"派生参数 {variable.name!r}(local)引用了未定义的变量 {name!r};"
|
|
451
|
+
"派生参数只能引用 YAML 里定义的 global / 同 Case 的 local"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
ordered_derived(config.global_variables, scope="global 的派生参数", env=environment)
|
|
455
|
+
ordered_derived(config.local_variables, scope="local 的派生参数", env=environment)
|
|
456
|
+
|
|
457
|
+
untranslatable = untranslatable_names(config, env=environment)
|
|
458
|
+
return (
|
|
459
|
+
[
|
|
460
|
+
"这些派生参数写不成 Excel 公式,参数表里只能写入算好的值"
|
|
461
|
+
f"(改输入后要重跑 --write-excel 刷新):{', '.join(untranslatable)}"
|
|
462
|
+
]
|
|
463
|
+
if untranslatable
|
|
464
|
+
else []
|
|
465
|
+
)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""随包发布的内置示例(``excel_codegen/examples/``):定位、列出、复制。
|
|
2
|
+
|
|
3
|
+
示例是**包数据** —— wheel 与 sdist 里都有一份,所以 ``pip install excel-codegen`` /
|
|
4
|
+
``uv tool install excel-codegen`` 装完、没有克隆仓库的人也能拿到:
|
|
5
|
+
|
|
6
|
+
excel-codegen examples # 看有哪些示例、各自演示什么
|
|
7
|
+
excel-codegen examples --copy ./demo # 拷出来直接用
|
|
8
|
+
|
|
9
|
+
它同时是仓库里的回归夹具(``abs_fpi/`` 的现场脚本与 ``tests/`` 都直接读这几个 YAML),
|
|
10
|
+
所以仓库根目录**不再**单独放一份 ``examples/``,免得两份漂移。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import shutil
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from .utils import CodeGenError
|
|
20
|
+
|
|
21
|
+
__all__ = ["EXAMPLES", "Example", "copy_examples", "examples_root"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Example:
|
|
26
|
+
"""一个内置示例:目录名 + 给人看的说明 + 两个常用路径(都相对示例根目录)。"""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
title: str
|
|
30
|
+
summary: str
|
|
31
|
+
entry: str
|
|
32
|
+
workbook: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
#: 示例清单。新增一个示例 = 在 ``examples/<name>/`` 放好文件 + 在这里登记;
|
|
36
|
+
#: ``tests/test_examples_command.py`` 会检查两边一一对应,漏登记会红。
|
|
37
|
+
EXAMPLES: tuple[Example, ...] = (
|
|
38
|
+
Example(
|
|
39
|
+
name="basic",
|
|
40
|
+
title="入门:公式模式与快照模式",
|
|
41
|
+
summary=(
|
|
42
|
+
"STM32 风格的 UART 配置。example_formula.yaml 是默认的公式模式"
|
|
43
|
+
"(改参数在 Excel 里自己重算,不用跑命令);example.yaml 显式写 engine: snapshot,"
|
|
44
|
+
"演示过滤器 / 循环 / 导出文件。"
|
|
45
|
+
),
|
|
46
|
+
entry="basic/example_formula.yaml",
|
|
47
|
+
workbook="basic/template_formula.xlsx",
|
|
48
|
+
),
|
|
49
|
+
Example(
|
|
50
|
+
name="nastran",
|
|
51
|
+
title="NASTRAN 工况控制:一行一个工况",
|
|
52
|
+
summary=(
|
|
53
|
+
"local_direction: vertical —— Local 表一行一个工况,下拉即增行;"
|
|
54
|
+
"语句留空就不输出,最后拼成 .inc / .deck 工况控制语句(指南 §19 / §20)。"
|
|
55
|
+
),
|
|
56
|
+
entry="nastran/nastran_case_control.yaml",
|
|
57
|
+
workbook="nastran/nastran_case_control.xlsx",
|
|
58
|
+
),
|
|
59
|
+
Example(
|
|
60
|
+
name="abs_fpi",
|
|
61
|
+
title="现场用例:ABS FPI 内外压 → GeniE",
|
|
62
|
+
summary=(
|
|
63
|
+
"两个规则集(外压 5A-3-2/5.5、内压 5A-3-2/5.7)可以各自单独生成一本工作簿,"
|
|
64
|
+
"也可以合成一本项目工作簿共用一张 Global 表;内压用成员表 Tank Data 把舱参数只写一遍。"
|
|
65
|
+
),
|
|
66
|
+
entry="abs_fpi/abs_fpi_internal.yaml",
|
|
67
|
+
workbook="abs_fpi/abs_fpi_internal.xlsx",
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def examples_root() -> Path:
|
|
73
|
+
"""内置示例的根目录(可编辑安装与 wheel 安装都在同一个相对位置)。"""
|
|
74
|
+
return Path(__file__).resolve().parent / "examples"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def find(name: str) -> Example:
|
|
78
|
+
for example in EXAMPLES:
|
|
79
|
+
if example.name == name:
|
|
80
|
+
return example
|
|
81
|
+
available = "、".join(item.name for item in EXAMPLES)
|
|
82
|
+
raise CodeGenError(f"没有这个示例:{name}(可选:{available})")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def copy_examples(dest: Path, *, only: str | None = None, force: bool = False) -> list[Path]:
|
|
86
|
+
"""把示例拷进 ``dest/<示例名>/``,返回拷出来的顶层目录列表。
|
|
87
|
+
|
|
88
|
+
:param only: 只拷这一个(默认全部)。
|
|
89
|
+
:param force: 目标目录已存在且非空时是否覆盖。
|
|
90
|
+
"""
|
|
91
|
+
selected = (find(only),) if only else EXAMPLES
|
|
92
|
+
root = examples_root()
|
|
93
|
+
if not root.is_dir(): # pragma: no cover - 只有被裁剪过的安装才会走到
|
|
94
|
+
raise CodeGenError(f"找不到内置示例目录:{root}(安装包可能不完整,重装一次试试)")
|
|
95
|
+
|
|
96
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
written: list[Path] = []
|
|
98
|
+
for example in selected:
|
|
99
|
+
src = root / example.name
|
|
100
|
+
if not src.is_dir(): # pragma: no cover - 由 tests/test_examples_command.py 守住
|
|
101
|
+
raise CodeGenError(f"内置示例 {example.name} 缺文件:{src}")
|
|
102
|
+
target = dest / example.name
|
|
103
|
+
if target.exists() and any(target.iterdir()) and not force:
|
|
104
|
+
raise CodeGenError(f"{target} 已存在且非空:加 --force 覆盖,或换一个目录")
|
|
105
|
+
shutil.copytree(src, target, dirs_exist_ok=True)
|
|
106
|
+
written.append(target)
|
|
107
|
+
return written
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# 内置示例
|
|
2
|
+
|
|
3
|
+
示例**随包发布**(wheel 与 sdist 里都有),所以装了 `excel-codegen` 就够了,
|
|
4
|
+
不需要克隆仓库:
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
excel-codegen examples # 列出示例、各自演示什么
|
|
8
|
+
excel-codegen examples --copy ./examples # 拷到 ./examples(--only abs_fpi 只拷一个)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
拷出来之后**打开里面的 `.xlsx` 就能改** —— 每一本都已经填好样例数值,而且默认是
|
|
12
|
+
**公式模式**:改一格参数,`Output` 表里的代码自己就重算,不需要跑任何命令。
|
|
13
|
+
要让「导出的代码文件」跟着更新,才需要回到命令行(或双击同目录的 `*_render.bat` / 跑 `*_render.sh`)。
|
|
14
|
+
|
|
15
|
+
| 示例 | 入口 YAML | 工作簿 | 演示什么 |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| [`basic/`](basic/) | `example_formula.yaml` | `template_formula.xlsx` | **默认的公式模式**:改参数在 Excel 里自动重算 |
|
|
18
|
+
| | `example.yaml` | `template.xlsx` | 显式 `engine: snapshot`:过滤器 / 循环 / 导出文件 |
|
|
19
|
+
| [`nastran/`](nastran/) | `nastran_case_control.yaml` | `nastran_case_control.xlsx` | **一行一个工况**(`local_direction: vertical`)生成工况控制语句(指南 §19 / §20) |
|
|
20
|
+
| [`abs_fpi/`](abs_fpi/) | `abs_fpi_internal.yaml` | `abs_fpi_internal.xlsx` | **现场用例**:ABS FPI 内外压 → GeniE;多规则集 / 成员表(指南 §18) |
|
|
21
|
+
|
|
22
|
+
> `generated*/` 里的文件是**提交进仓库的实测产物**(脚本导出一次的结果),
|
|
23
|
+
> 拿它跟你自己跑出来的对一下,就知道有没有改坏。
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## basic —— 入门:公式模式与快照模式
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cd basic
|
|
31
|
+
|
|
32
|
+
# 工作簿已经生成好了,直接打开 template_formula.xlsx 改参数即可。
|
|
33
|
+
# 要把代码导成文件:
|
|
34
|
+
excel-codegen render -c example_formula.yaml -x template_formula.xlsx --outdir generated_formula
|
|
35
|
+
|
|
36
|
+
# 核对「Excel 里算出来的」与「Python 渲染的」是否一致(CI 里很有用):
|
|
37
|
+
excel-codegen check -c example_formula.yaml -x template_formula.xlsx
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`example.yaml` 是**对照示例**:它显式写 `engine: snapshot`,用到了公式模式表达不了的
|
|
41
|
+
`{% for %}` 与过滤器,同时演示 `filename` 导出多个文件。两条路的取舍见指南 §14。
|
|
42
|
+
|
|
43
|
+
## nastran —— 一行一个工况
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cd nastran
|
|
47
|
+
excel-codegen render -c nastran_case_control.yaml -x nastran_case_control.xlsx --outdir generated
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Local 表是**竖着**的(`local_direction: vertical`):一行一个工况,下拉即增行,
|
|
51
|
+
整块从别处粘进来也方便。每条语句**留空就不输出**,而它在表里占的那一行仍然是空的
|
|
52
|
+
(方便整列复制)。输出是 `.inc` / `.deck` 工况控制语句,外加每块的摘要。
|
|
53
|
+
|
|
54
|
+
## abs_fpi —— 现场用例:ABS FPI 内外压 → GeniE
|
|
55
|
+
|
|
56
|
+
把 **ABS FPI**(5A-3-2/5.5 外压、5.7 内压)的面载荷计算,生成可直接粘进 GeniE 的
|
|
57
|
+
JavaScript 函数体。详见 [`abs_fpi/README.md`](abs_fpi/README.md)。
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cd abs_fpi
|
|
61
|
+
|
|
62
|
+
# 三本工作簿,挑一本打开就能改:
|
|
63
|
+
# abs_fpi_external.xlsx 外压单用(3 个工况)
|
|
64
|
+
# abs_fpi_internal.xlsx 内压单用(4 个工况)
|
|
65
|
+
# ABS_FPI_load_cases.xlsx 项目工作簿:两套规则共用一张 Global 表(7 个工况)
|
|
66
|
+
excel-codegen render -c abs_fpi_internal.yaml -x abs_fpi_internal.xlsx --outdir generated
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> 用例里的舱容与工况组合只是**示意数值**(随手取的几个数),不代表任何真实船舶。
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM ===========================================================================
|
|
3
|
+
REM 由 excel_codegen 生成 —— 改完参数双击本文件即可把结果写回 Output 表。
|
|
4
|
+
REM 重新生成工作簿(init)时会一并覆盖本文件。
|
|
5
|
+
REM ===========================================================================
|
|
6
|
+
cd /d "%~dp0"
|
|
7
|
+
|
|
8
|
+
where uv >nul 2>nul
|
|
9
|
+
if %errorlevel%==0 (
|
|
10
|
+
uv run excel-codegen render -c "abs_fpi.yaml" -x "ABS_FPI_load_cases.xlsx" --write-excel
|
|
11
|
+
) else (
|
|
12
|
+
excel-codegen render -c "abs_fpi.yaml" -x "ABS_FPI_load_cases.xlsx" --write-excel
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
echo.
|
|
16
|
+
if errorlevel 1 (
|
|
17
|
+
echo [失败] 上面有报错信息。常见原因:依赖没装(跑一次 setup.sh / uv sync)、
|
|
18
|
+
echo 或者 Excel 正开着这个文件(先关掉再试)。
|
|
19
|
+
) else (
|
|
20
|
+
echo [完成] 回到 Excel 打开「Output」表看结果。
|
|
21
|
+
)
|
|
22
|
+
pause
|