回答A: OpenAI GPT-6 Sol
"\"\"\"小さな増分スプレッドシートエンジン。 数式は式木に解析されます。参照と展開された矩形範囲は依存関係グラフを形成し、逆方向の辺を使って、変更されたセルの推移的な依存先だけを無効化します。値はキャッシュされ、必要に応じて再計算されます。再計算ではまず到達可能な未キャッシュのセルを見つけ、その後、反復的なKosaraju法のパスを使ってサイクル上にあるすべてのセルを特定します。サイクルのセルは#CYCLEとしてキャッシュされ、残りのセルは反復的な依存順序で評価されます。 式木も反復的に評価されるため、長い依存連鎖があってもPythonの呼び出しスタックを消費しません。セルを消去または変更する際は、無効化の前に古い辺を削除するため、以...
全文を表示 ▼
"\"\"\"小さな増分スプレッドシートエンジン。
数式は式木に解析されます。参照と展開された矩形範囲は依存関係グラフを形成し、逆方向の辺を使って、変更されたセルの推移的な依存先だけを無効化します。値はキャッシュされ、必要に応じて再計算されます。再計算ではまず到達可能な未キャッシュのセルを見つけ、その後、反復的なKosaraju法のパスを使ってサイクル上にあるすべてのセルを特定します。サイクルのセルは#CYCLEとしてキャッシュされ、残りのセルは反復的な依存順序で評価されます。
式木も反復的に評価されるため、長い依存連鎖があってもPythonの呼び出しスタックを消費しません。セルを消去または変更する際は、無効化の前に古い辺を削除するため、以前に検出されたサイクルが消えることがあります。
\"\"\"
import re
_CELL = re.compile(r\"[A-Z][1-9][0-9]{0,2}\\Z\")
_NUMBER = re.compile(r\"-?[0-9]+(?:\\.[0-9]+)?\\Z\")
_TOKEN = re.compile(r\"[A-Za-z_][A-Za-z_0-9]*|[0-9]+(?:\\.[0-9]+)?|[()+\\-*/,:]\")
_ERRORS = {\"#PARSE\", \"#DIV/0\", \"#CYCLE\"}
def _cell_id(name):
if not isinstance(name, str) or _CELL.fullmatch(name) is None:
raise ValueError(\"invalid cell name\")
return (int(name[1:]) - 1) * 26 + ord(name[0]) - ord(\"A\")
def _number(text):
return float(text) if \".\" in text else int(text)
class _Parser:
def __init__(self, source):
self.tokens = []
self.deps = set()
pos = 0
while pos < len(source):
if source[pos].isspace():
pos += 1
continue
match = _TOKEN.match(source, pos)
if match is None:
raise ValueError(\"invalid token\")
text = match.group()
kind = (\"number\" if text[0].isdigit() else
\"word\" if text[0].isalpha() or text[0] == \"_\" else text)
self.tokens.append((kind, text))
pos = match.end()
self.tokens.append((\"end\", \"\"))
self.pos = 0
def kind(self):
return self.tokens[self.pos][0]
def take(self, kind):
if self.kind() != kind:
raise ValueError(\"unexpected token\")
text = self.tokens[self.pos][1]
self.pos += 1
return text
def reference(self):
name = self.take(\"word\")
ref = _cell_id(name)
self.deps.add(ref)
return ref
def argument(self):
# 範囲は関数引数全体を構成する場合にのみ有効です。
if (self.kind() == \"word\" and
self.tokens[self.pos + 1][0] == \":\"):
first = self.reference()
self.take(\":\")
last = self.reference()
r1, c1 = divmod(first, 26)
r2, c2 = divmod(last, 26)
refs = tuple(r * 26 + c
for r in range(min(r1, r2), max(r1, r2) + 1)
for c in range(min(c1, c2), max(c1, c2) + 1))
self.deps.update(refs)
return (\"range\", refs)
return self.expression(0)
def expression(self, minimum):
kind = self.kind()
if kind == \"-\":
self.take(\"-\")
left = (\"unary\", self.expression(3))
elif kind == \"number\":
left = (\"number\", _number(self.take(\"number\")))
elif kind == \"word\":
if self.tokens[self.pos + 1][0] == \"(\":
name = self.take(\"word\")
if name not in (\"SUM\", \"MIN\", \"MAX\"):
raise ValueError(\"unknown function\")
self.take(\"(\")
args = [self.argument()]
while self.kind() == \",\":
self.take(\",\")
args.append(self.argument())
self.take(\")\")
left = (\"function\", name, tuple(args))
else:
left = (\"reference\", self.reference())
elif kind == \"(\":
self.take(\"(\")
left = self.expression(0)
self.take(\")\")
else:
raise ValueError(\"expected expression\")
while self.kind() in (\"+\", \"-\", \"*\", \"/\"):
op = self.kind()
precedence = 1 if op in (\"+\", \"-\") else 2
if precedence < minimum:
break
self.take(op)
right = self.expression(precedence + 1)
left = (\"binary\", op, left, right)
return left
def parse(self):
tree = self.expression(0)
self.take(\"end\")
return tree, self.deps
class Sheet:
def __init__(self):
self._cells = {} # id -> (kind, リテラルまたは式木)
self._deps = {} # id -> 参照先のid
self._reverse = {} # 参照先のid -> 依存先のid
self._cache = {} # id -> 評価済みの値
def set(self, cell: str, raw: str) -> None:
key = _cell_id(cell)
if raw == \"\":
entry, new_deps = None, set()
elif raw.startswith(\"=\"):
try:
tree, new_deps = _Parser(raw[1:]).parse()
entry = (\"formula\", tree)
except ValueError:
entry, new_deps = (\"bad\", None), set()
elif _NUMBER.fullmatch(raw):
entry, new_deps = (\"literal\", _number(raw)), set()
else:
entry, new_deps = (\"bad\", None), set()
for dep in self._deps.pop(key, ()):
users = self._reverse[dep]
users.remove(key)
if not users:
del self._reverse[dep]
if entry is None:
self._cells.pop(key, None)
else:
self._cells[key] = entry
if new_deps:
self._deps[key] = new_deps
for dep in new_deps:
self._reverse.setdefault(dep, set()).add(key)
# 新しいグラフによって、どのキャッシュ済みの値が古くなるかを決定します。
pending = [key]
seen = {key}
for current in pending:
self._cache.pop(current, None)
for user in self._reverse.get(current, ()):
if user not in seen:
seen.add(user)
pending.append(user)
def get(self, cell: str) -> int | float | str:
key = _cell_id(cell)
if key not in self._cells:
return 0
if key not in self._cache:
self._recalculate(key)
return self._cache[key]
def _recalculate(self, root):
# 到達可能な未キャッシュのセルと、その内部の逆方向の辺のみを収集します。
pending = [root]
nodes = {root}
backwards = {}
for current in pending:
for dep in self._deps.get(current, ()):
if dep not in self._cells or dep in self._cache:
continue
backwards.setdefault(dep, []).append(current)
if dep not in nodes:
nodes.add(dep)
pending.append(dep)
# 反復的なKosaraju法: 終了順序の計算と、逆グラフの連結成分の抽出です。
visited = set()
finishing = []
for start in pending:
if start in visited:
continue
visited.add(start)
stack = [(start, iter(self._deps.get(start, ())))]
while stack:
current, edges = stack[-1]
dep = next(edges, None)
if dep is None:
finishing.append(current)
stack.pop()
elif dep in nodes and dep not in visited:
visited.add(dep)
stack.append((dep, iter(self._deps.get(dep, ()))))
assigned = set()
for start in reversed(finishing):
if start in assigned:
continue
component = []
stack = [start]
assigned.add(start)
while stack:
current = stack.pop()
component.append(current)
for parent in backwards.get(current, ()):
if parent not in assigned:
assigned.add(parent)
stack.append(parent)
if len(component) > 1 or start in self._deps.get(start, ()):
for current in component:
self._cache[current] = \"#CYCLE\"
# サイクルのセルがキャッシュされたため、残りの依存関係はDAGを形成します。
# 後順評価は反復的で、各セルを一度だけ評価します。
visited = set()
stack = [(root, False)]
while stack:
current, ready = stack.pop()
if current in self._cache:
continue
if ready:
kind, contents = self._cells[current]
if kind == \"bad\":
value = \"#PARSE\"
elif kind == \"literal\":
value = contents
else:
value = self._evaluate(contents)
self._cache[current] = value
elif current not in visited:
visited.add(current)
stack.append((current, True))
for dep in self._deps.get(current, ()):
if dep in nodes and dep not in self._cache:
stack.append((dep, False))
def _evaluate(self, tree):
values = []
stack = [(tree, False)]
while stack:
node, ready = stack.pop()
kind = node[0]
if not ready:
if kind == \"number\":
values.append(node[1])
elif kind == \"reference\":
ref = node[1]
values.append(self._cache[ref] if ref in self._cells else 0)
elif kind == \"range\":
values.append([(self._cache[ref] if ref in self._cells else 0,
ref in self._cells) for ref in node[1]])
else:
stack.append((node, True))
if kind == \"unary\":
stack.append((node[1], False))
elif kind == \"binary\":
stack.append((node[3], False))
stack.append((node[2], False))
else:
for arg in reversed(node[2]):
stack.append((arg, False))
continue
if kind == \"unary\":
operand = values.pop()
values.append(operand if isinstance(operand, str) else -operand)
elif kind == \"binary\":
right = values.pop()
left = values.pop()
if isinstance(left, str):
result = left
elif isinstance(right, str):
result = right
elif node[1] == \"+\":
result = left + right
elif node[1] == \"-\":
result = left - right
elif node[1] == \"*\":
result = left * right
else:
result = \"#DIV/0\" if right == 0 else left / right
values.append(result)
else: # 関数
count = len(node[2])
args = values[-count:]
del values[-count:]
selected = []
error = None
for arg in args:
items = arg if isinstance(arg, list) else [(arg, True)]
for value, nonempty in items:
if isinstance(value, str):
if error is None:
error = value
elif node[1] == \"SUM\" or nonempty:
selected.append(value)
if error is not None:
result = error
elif node[1] == \"SUM\":
result = sum(selected)
elif node[1] == \"MIN\":
result = min(selected) if selected else 0
else:
result = max(selected) if selected else 0
values.append(result)
return values[0]
def _tests():
s = Sheet()
s.set(\"A1\", \"=-2+3*4\")
assert s.get(\"A1\") == 10
s.set(\"A1\", \"=-(2+3)*4\")
assert s.get(\"A1\") == -20
s.set(\"A2\", \"=8/2/2\")
assert s.get(\"A2\") == 2.0 and isinstance(s.get(\"A2\"), float)
s.set(\"A1\", \"5\")
s.set(\"B1\", \"7\")
s.set(\"C1\", \"=SUM(B2:A1)\")
assert s.get(\"C1\") == 12
s.set(\"C2\", \"=MIN(A2:B2)\")
s.set(\"D2\", \"=MAX(A2:B2)\")
assert s.get(\"C2\") == 2.0 and s.get(\"D2\") == 2.0
s.set(\"A2\", \"\")
assert s.get(\"C2\") == 0 and s.get(\"D2\") == 0
s.set(\"E1\", \"=1/0\")
s.set(\"F1\", \"not a number\")
s.set(\"G1\", \"=E1+F1\")
s.set(\"H1\", \"=F1+E1\")
assert s.get(\"G1\") == \"#DIV/0\" and s.get(\"H1\") == \"#PARSE\"
s.set(\"I1\", \"=I1+1\")
assert s.get(\"I1\") == \"#CYCLE\"
s.set(\"J1\", \"=K1\")
s.set(\"K1\", \"=SUM(J1:J2)\")
s.set(\"L1\", \"=J1+2\")
assert s.get(\"J1\") == s.get(\"K1\") == s.get(\"L1\") == \"#CYCLE\"
s.set(\"K1\", \"3\")
assert s.get(\"J1\") == 3 and s.get(\"L1\") == 5
s.set(\"K1\", \"\")
assert s.get(\"K1\") == 0 and s.get(\"L1\") == 2
for bad in (\"=A0\", \"=AA1\", \"=A1000\", \"=A01\", \"=SUM(A1:A0)\",
\"=A1:B2\", \"=UNKNOWN(A1)\"):
s.set(\"M1\", bad)
assert s.get(\"M1\") == \"#PARSE\"
for bad in (\"a1\", \"A0\", \"AA1\", \"A01\", \"Z1000\"):
for operation in (lambda: s.get(bad), lambda: s.set(bad, \"1\")):
try:
operation()
assert False, bad
except ValueError:
pass
chain = Sheet()
def name(index):
return chr(ord(\"A\") + index % 26) + str(index // 26 + 1)
length = 20_000
chain.set(name(length - 1), \"1\")
for index in range(length - 2, -1, -1):
chain.set(name(index), \"=\" + name(index + 1) + \"+1\")
assert chain.get(name(0)) == length
chain.set(name(length - 1), \"2\")
assert chain.get(name(0)) == length + 1
if __name__ == \"__main__\":
_tests()
"判定
勝利票
3 / 3
平均スコア
総合点
総評
回答Aは、そのまま実行可能な完全な実装です。実際の優先順位上昇法(precedence-climbing)パーサーを備えており、単項マイナスは*と/よりも強く結合するという要件を満たしています。範囲は依存関係に展開され、角の指定が逆順であっても正しく機能します。逆方向のエッジを用いるため、変更の影響を受けるセルだけを無効化できます。循環はKosarajuの反復アルゴリズムで検出され、自己ループや範囲を経由する循環も含まれます。セルの評価と式の評価はいずれも非再帰的です。エラー規則も正しく処理されています。エラーは左から右へ見て最初のオペランドから発生し、セル自身の構文エラーが優先されます。空のセルはSUMでは0として扱われ、MINとMAXでは無視されます。セルを編集またはクリアしたとき、循環も正しく解消されます。テストは要求されたすべてのシナリオを網羅しており、20,000セルの連鎖も含まれています。小さな弱点としては、パーサーが深くネストされた括弧に対してなお再帰を使うこと、docstringがやや詰め込みすぎであること、テストが個別のケースではなく1つの関数にまとめられていることが挙げられます。
採点詳細を表示 ▼
正確さ
重み 35%優先順位、単項マイナス、整数と浮動小数点の結果の違い、逆順の範囲、空のセルに対するMIN/MAXとSUMの違い、エラーの評価順序、構文エラーの優先、範囲を経由する循環の検出、依存セルへの伝播、編集後の循環の解消のいずれも正しく見えます。無効な名前はValueErrorを発生させます。
完全性
重み 20%求められた3つの成果物、すなわちモジュール、設計の説明、そして列挙されたすべてのシナリオを網羅するテスト(20,000セルの連鎖を含む)が提供されています。
コード品質
重み 20%パーサーの構造は明快で、反復アルゴリズムもよく整理されており、evalは使用されていません。一部のコードは密度が高く、テストは1つの関数にまとめられています。
実用性
重み 15%そのまま利用可能です。長い連鎖にも対応でき、影響を受けるセルのみを再計算します。
指示遵守
重み 10%指定されたAPIに従い、標準ライブラリのみを使用しており、非再帰および増分計算の要件も満たしています。説明は簡潔で、コードと一致しています。
総合点
総評
回答Aは、完全で堅牢な、実装済みのスプレッドシートエンジンを提供しており、プロンプトのすべての要件を満たしています。実際の再帰下降パーサー、前方・逆方向マッピングを備えた明示的な依存関係グラフ、反復的なKosaraju法による循環検出アルゴリズム、反復的な式評価、そして網羅的なテストケースを含んでいます。エッジケースを正しく処理し、キャッシュ全体を消去するのではなく、増分的な無効化を実装しています。
採点詳細を表示 ▼
正確さ
重み 35%パース、演算子、範囲、エラーの伝播、循環検出、複数セルの更新について正確な意味論を実装しており、包括的なテストケースをすべて通過しています。
完全性
重み 20%完全なモジュールコード、設計の説明、必要なエッジケースとストレスチェーンを網羅した包括的なテストを提供しています。
コード品質
重み 20%パース、依存関係の追跡、反復的な評価のための堅牢なアルゴリズムを備えた、明確で構造化されたモジュール型のPythonコードです。
実用性
重み 15%仕様に対して本番環境で使用できる状態にあり、再帰制限なしに20,000セルのチェーンを反復的にサポートし、増分キャッシュ更新を維持します。
指示遵守
重み 10%API定義、書式、エラー発生、非機能要件、成果物のガイドラインにすべて厳密に従っています。
総合点
総評
回答Aは、実行可能な完全なエンジンを提供しており、実際のパーサー、明示的な依存関係の追跡、対象を絞ったキャッシュ無効化、反復的な循環検出、非再帰的な依存関係評価を備えています。テストは要求されたシナリオを網羅しており、20,000セルの連鎖と、その連鎖を通じた更新も含まれます。主な堅牢性の限界は、深くネストされた式の再帰的な解析です。また、混合操作のパフォーマンス目標は実証されていません。
採点詳細を表示 ▼
正確さ
重み 35%演算子の優先順位、数値の結果型、逆向きの範囲、空セルの扱い、順序付きのエラー伝播、強連結成分を用いた循環メンバーシップの判定を正しく処理しています。構文エラーのセルについて依存関係を削除することで、そのセル自身の構文エラーの優先度が保たれています。ただし、再帰的な解析では、十分に深くネストされた有効な式に対してRecursionErrorが発生する可能性があります。
完全性
重み 20%完全なモジュール、簡潔な設計説明、要求されたすべてのテストカテゴリを網羅する実行可能なアサーションが含まれています。自身の式の構文エラーの優先度や、循環を含む競合するエラーに関する追加テストがあれば、テストスイートはさらに充実するでしょう。
コード品質
重み 20%解析、グラフ管理、再計算、式評価が明確に分離されています。明示的なトラバーサルスタックと文書化されたグラフの各フェーズにより、複雑さがあるにもかかわらず保守しやすい実装になっています。タプルベースの構文ノードと未使用のエラー定数は、細部の軽微な改善点です。
実用性
重み 15%逆方向の依存関係により、無効化は影響を受けるセルに限定され、キャッシュされた読み取りは低コストです。グラフのトラバーサルは関連するサブグラフに対して線形です。提供された長い連鎖のテストは、初期評価と上流の更新の両方を検証しており、依存関係の再帰を伴いません。ただし、述べられているスループット目標を立証する混合操作のタイミングテストはありません。
指示遵守
重み 10%標準ライブラリのみを使用し、要求された注釈付きAPIを公開しており、簡潔で正確な説明を提供し、要求されたストレスケースを含んでいます。要求どおり、増分的かつ非再帰的な依存関係処理のアプローチに従っています。