共有費用の割り勘計算機と精算プランナー
フリーのプロンプト百科事典 Wikiprompt より
共有費用の割り勘計算機と精算プランナー 共有経費をCSV台帳から分割するための完全なAIスキル/プロンプト。複数の分割方法、通貨、および最小数の清算送金を見つける機能をサポートします。詳細な指示、テンプレート、例、完全なPython実装が含まれています。
プロンプト内容保存
🌐
---
name: shared-expense-bill-splitter-and-settle-up-planner
description: 旅行、共同生活、イベントの共有コストのCSV台帳を分割し、均等、シェア、正確な金額、パーセント、税金とチップを按分する明細付き請求書、「全員以外」の除外、複数の支払い者、返金、返済、合意したレートでの複数通貨をセント単位で処理し、その後、証明可能な最小数の清算送金(正確なゼロ和サブグループ検索)を見つけます。残高、個人別監査、チャットメッセージを返します。stdlib Pythonプランナー(--self-test付き)を含みます。
---
# 共有費用の割り勘と清算プランナー
あなたは、乱雑な共有支出のリストを、公平で検証可能な割り勘と、可能な限り最短の支払いリストに変換します。台帳はグループが任意のスプレッドシートで編集できるCSVとして保持し、各行に本当に必要な分割ルール(誰がいたか、誰が何を注文したか、誰が何泊滞在したか)を適用し、グループが合意したレートで通貨を変換し、最小数の送金で全員を清算します。すべてのセントが説明され、誰でも自分の数字を一行ずつ追跡できます。
## このスキルのファイル
- `scripts/settle_up.py` - CSV台帳を読み取り、各行を整数セント(最大剰余丸め)で分割し、個人ごとの支払額/負担額/純額を計算し、すべてのゼロ和サブグループを検索して最小数の送金を見つけます(残高のある18人まで正確、それを超えると貪欲法)(Python 3.8+ 標準ライブラリのみ)
- `references/split-methods.md` - CSV形式、どのコストにどの分割を使用するか、税金とチップを含む明細付き請求書、除外、支払い者、返金、返済
- `references/settle-up-and-currency.md` - 最小値が「人数 - 1」より小さくなり得る理由、検索の仕組み、丸め、為替レートとカード手数料、支払いエチケット
- `templates/settle-up-summary.md` - 手渡してグループチャットに貼り付ける要約
- `examples/lisbon-trip.csv` - 5人の友人、11行: 宿泊数によるシェア、除外された人、明細付きディナー、USDでの予約、プレミアムシート、2人の支払い者、返金、GBPでの軽食、すでに行われた返済
- `examples/lisbon-settle-output.md` - その台帳に対するプランナーの実際の出力
- `examples/flat-october.csv` - 6人のルームメイトで、最小値(4回の送金)が最大債務から最大債権への支払い(5回)より優れている例
## ワークフロー
### 1. 台帳を収集する
すべての共有コストについて、何を、誰が支払ったか(複数人が支払った場合はそれぞれいくらか)、金額と通貨、誰のためだったかを尋ねます。分割を変える質問をします: 誰かが遅れて到着したり、早く出発したりしましたか?誰かが食事やアクティビティをスキップしましたか?ディナーは共有でしたか、それとも個別に注文しましたか?誰かがすでに誰かに返済しましたか?基本通貨とレート(グループがカード明細レートを望まない限り、旅行全体で通貨ごとに1つのレート)に同意します。`references/split-methods.md`のCSVとして書き出し、名前のタイプミスを検出するために`# people:`を含めます。
### 2. プランナーを実行する
```
python3 scripts/settle_up.py examples/lisbon-trip.csv
python3 scripts/settle_up.py flat.csv --base GBP --rates EUR=0.86 --detail Ben
python3 scripts/settle_up.py ledger.csv --json
python3 scripts/settle_up.py --self-test
```
終了コード0 = プランが出力されました(警告セクションを読んでください)、2 = 台帳にエラーがあります(合計が一致しない、レートがない、不明な分割または名前)- 指定された行を修正してください。コードを実行できない場合は、同じ手順を手動で実行してください: 各行を基本セントに変換し、分割し、支払額から負担額を引いたものを個人ごとに合計し、債務者と債権者を照合し、最初に残高が正確に相殺される個人または小グループを探します。
### 3. 結果をグループと確認する
- チェック行は`nets add up to 0.00`と読める必要があります。支払額と負担額は等しくなければなりません。
- 残高を平易な言葉で読み上げます(「Anaは990.00を支払い、彼女の負担額は377.31で、Eliはすでに彼女に100.00を渡しているので、彼女は512.69を受け取ります」)。
- 結果を左右し、異議を唱えやすい行(宿泊数によるシェア、明細付き食事、返金、返済)を指摘し、自分の内訳を希望する人には`--detail <name>`を提供します。
- 誰かが他の誰かに支払うことを好む場合(または直接支払えない場合)、合意した送金を`payment`行として追加して再実行します。プランは残りを再計算します。
### 4. 納品する
`templates/settle-up-summary.md`に記入します: 合計、残高テーブル、番号付き送金、前提条件(レート、誰がどこで除外されたか、税金/チップがどのように配分されたか)、および短いグループチャットメッセージ。各送金を完了としてマークするには、台帳に`payment`行として追加することを提案し、2回目の実行で全員が0.00になるようにします。
## 結果の例
`examples/lisbon-trip.csv`(5人、1,583.07 EUR支出)で、Anaはアパートとファドのチケットを支払い、512.69 EURを受け取ります。プランは4回の送金です: BenはAnaに243.17、DaraはAnaに140.94、EliはAnaに128.58、EliはCemに0.85を支払います(Eliの以前の100.00の返済はすでにカウントされています)。`examples/flat-october.csv`では、BoとFinnが互いに相殺されるため、6人のルームメイトは4回の送金で清算できますが、最大債務から最大債権への支払いでは5回かかります。
## ルール
- 金額、レート、参加者を決して invent しないでください。尋ねるか、その行を要約の前提条件としてマークしてください。
- 通貨ごとに1つの合意されたレートを使用し、それを表示してください。完成した分割にライブレートを混在させないでください。
- すべての計算をセントで行い、丸めセントがどこに行ったかを示してください(最大剰余、同点の場合は最初にリストされた人)。
- 支出を判断しないでください。重複、タイプミス、参加者の欠落のように見える行のみにフラグを立ててください。
- 返済は`payment`行であり、決してマイナスの支出ではありません。返金は、元のコストと同じ人々によって共有されるマイナスの支出です。
- プランを提案として提示してください。各人の純額が変わらない限り、グループは誰が誰に支払うかを交換できます。
FILE:references/split-methods.md
# 分割方法とCSV形式
## 列
`date,description,paid_by,amount,currency,split,participants,details` - ヘッダー行が必要です。任意のスプレッドシートでエクスポートできます。ヘッダー前のオプションのコメント行:
```
# people: Ana, Ben, Cem, Dara, Eli (厳密なリスト: 名前のタイプミスはエラー)
# base: EUR
# rates: USD=0.92, GBP=1.16 (1 USD = 0.92 EUR)
```
`# people:`がない場合、ファイル内のどこかに名前がある全員がメンバーであり、`all`は全員を意味します。名前は大文字と小文字を区別せずに照合されます。
## 分割の選択
| コスト | 分割 | `participants` / `details`の例 |
|---|---|---|
| 食料品、タクシー、燃料、共有チケット | `equal` | `all`、`Ana;Ben`、`all-Eli` |
| 滞在日数が異なる宿泊、異なるサイズの部屋 | `shares` | `Ana:3;Ben:3;Dara:2`(泊数または部屋の重み) |
| 価格が異なるチケット、既知の一人当たり金額 | `exact` | `Ana:30;Ben:45`(participantsの他の人は残りを均等に分割) |
| 合意されたパーセンテージ(収入による家賃、飲んだ人のワイン) | `percent` | `Ana:25;Ben:25;Dara:50`(合計100でなければなりません) |
| 人々が個別に注文したレストラン請求書 | `itemized` | `Ana:31;Ben:28.50;shared:42` |
| 誰かがすでに誰かに返済した | `payment` | participants = 受取人1人 |
## 明細付き請求書
- `details`は税金、サービス料、チップの前の各人の項目をリストします。`shared`は前菜、ボトル、または全員が共有したものの合計です(`participants`間で均等に分割)。
- `amount`は最終的な請求書です。差額(amount - items)は税金、サービス料、チップであり、各人が注文したものに比例して配分されるため、多く注文した人はより多くのチップを支払います。出力では`+17.3% tax/tip`として表示されます。
- 項目が請求書より多い場合(割引またはタイプミス)、プランナーは停止します。割引は、適用された項目価格を下げて入力してください。
## 除外と部分参加
- `all-Eli`または`all except Eli` = Eli以外の全員。複数連結: `all-Bo-Finn`。
- 一部の時間だけ参加した人には、除外する代わりに泊数または日数で`shares`を使用します。
- 1人のメンバーが支払った子供またはゲスト: メンバーの名前の下にリストするか(`shares`の重み1.5)、`# people:`に追加して親と清算します。
## 支払い者
- 1人の支払い者: `Ana`。1つの請求書に複数の支払い者: `Ben:40;Cem:20`(行の通貨での金額、`amount`に合計する必要があります)。
- カップルが共有するカード: カップルを1人として扱うか、支払い者金額を分割します。
## 返金、保証金、返済
- キャンセル後に受け取った返金: マイナスの`amount`、`paid_by` = お金を受け取った人、元のコストと同じ参加者。
- 大家から返還された保証金: マイナスの`amount`、保証金と同じ方法で分割。
- メンバー間で渡されたお金: `payment`(`paid_by`から`participants`の1人へ)。清算送金も同じように記録します。再実行すると、まだ未清算の人が表示されます。
## プランナーが検出する一般的な間違い
- 合計が一致しない正確な金額またはパーセンテージ、請求書と異なる支払い者金額、請求書を超える項目。
- レートのない通貨、不明な分割語、`# people:`が設定されている場合の名前のタイプミス。
- 2つの同一行(同じ日付、説明、金額、支払い者)- 警告として報告され、削除されません。
- ゼロ金額 - 警告付きでスキップされます。
FILE:references/settle-up-and-currency.md
# 清算と通貨
## 純残高
すべての人について: `net = paid + payments sent - share - payments received`。正 = お金を受け取る、負 = 支払う義務がある。すべての行が行の合計に等しい整数セントで分割されるため、純額は常に正確に0.00になります。
## 「最小送金数」が人数 - 1より小さくなり得る理由
- 残高のあるk人のグループは、最大k - 1回の送金で清算できます(各送金は少なくとも1人を清算します)。
- グループが、それぞれの残高がゼロになるサブグループに分割される場合、各サブグループは独自に清算します: そのようなサブグループがg個ある場合、最小値はk - gです。例: Bo +300とFinn -300が相殺されるため、6人のルームメイトは5回ではなく4回の送金が必要です。
- 最大債務から最大債権への支払い(通常のアプリのアプローチ)はこれらのサブグループを探さないため、より多くの送金が必要になる場合があります。
- ゼロ和サブグループへの最適な分割を見つけることは一般的に難しい問題です。プランナーは、残高がゼロでない最大18人まで正確に解決し(サブセット上の動的計画法、1秒未満)、それを超えると上記の貪欲法にフォールバックして、その旨を述べます。
- 各サブグループ内では、プランは最大債務から最大債権に支払います。これにより、正確に(サブグループサイズ - 1)回の送金が使用されます。
- 複数のプランが同じ最小数を持つ場合があります。各人の総入出金は純額によって固定されるため、どれも公平です。
## 丸め
- 各行は基本セントに一度だけ変換され(`amount x rate`、半数上げで丸め)、その後、最大剰余法で分割されます: 全員が切り捨てられた金額を受け取り、残りのセントは最大の端数に渡り、同点の場合は`# people:`で最初にリストされた人に渡ります。
- 旅行全体で、これは1人あたり最大数セントの変動です。10.00 / 3が1人にとって3.34になる理由を尋ねられたら、その旨を伝えてください。
## 為替レート
- 分割前に通貨ごとに1つのレートに同意します: ほとんどのコストが支払われた日のレート、旅行の平均、または支払い者のカード明細のレート(銀行のマークアップを含む)。
- 支払い者のカードが外国取引手数料を請求した場合、それを金額に含めるか(グループが共有)、除外するか(支払い者が吸収)を決定します。旅行全体で一度だけ決定します。
- 実際に支払われた通貨で金額を入力し、プランナーに変換させます。一部の行を手動で事前変換しないでください。
- 基本通貨で清算します。誰かが別の通貨で支払う場合、同じ合意されたレートで送金を変換し、その通貨で`payment`行として記録します。
## 支払いエチケット
- 送金と台帳へのリンクを含むグループチャットメッセージを送信します。支払いの前に、行に異議を唱えるために1日か2日与えます。
- 少額の送金(約2 EUR/USD未満)は、両者が同意すれば免除するか、次の共有コストに繰り越すことができます。台帳を正確に保つために、それを`payment`行として記録します。
- 全員が支払ったら、各送金を`payment`行として追加して再実行します: すべての純額が0.00になるはずです。
FILE:templates/settle-up-summary.md
# 清算: <旅行 / 共同生活 / イベント>、<日付>
**グループ:** <Ana, Ben, Cem, Dara, Eli> **基本通貨:** <EUR> **総支出:** <1,583.07 EUR>、<11>行
**合意されたレート:** <1 USD = 0.92 EUR、1 GBP = 1.16 EUR(旅行平均)>
## 残高
| 人 | 支払額 | 公平な負担額 | 送金済み / 受取済み | 純額 |
|---|---|---|---|---|
| <Ana> | <990.00> | <377.31> | <受取 100.00> | <+512.69 受け取る> |
| <Ben> | <64.00> | <307.17> | <-> | <-243.17 支払う> |
## 送金(<n>回、最小可能数)
1. <BenはAnaに243.17 EURを支払う>
2. <DaraはAnaに140.94 EURを支払う>
3. <...>
## 行の分割方法
- <アパート: 泊数による、DaraとEliは3泊中2泊>
- <空港タクシー: Eli以外の全員(電車で来た)>
- <ディナー: 明細付き、17.3%の税金とチップは各人が注文したものに比例して配分。前菜42.00は共有>
- <宮殿チケット返金: 支払ったBenとCemに戻る>
- <EliのAnaへの100.00はすでにカウント済み>
## 前提条件と未解決点
- <上記のレート。カード手数料は含まれていません>
- <行5(サーフレッスン)を確認してください: Benは参加していましたか?>
## グループチャットメッセージ
```
清算 (<EUR>、<11>行、レート: <USD 0.92、GBP 1.16>):
- <Ben -> Ana: 243.17 EUR>
- <Dara -> Ana: 140.94 EUR>
完全なテーブル: <シートへのリンク>。行が間違っている場合は2日以内に連絡してください。
```
**支払い完了時:** 各送金を`payment`行として追加して再実行します。すべての純額が0.00になるはずです。
FILE:examples/lisbon-trip.csv
# people: Ana, Ben, Cem, Dara, Eli
# base: EUR
# rates: USD=0.92, GBP=1.16
date,description,paid_by,amount,currency,split,participants,details
2026-10-02,Apartment 3 nights (Dara and Eli 2 nights),Ana,840.00,EUR,shares,all,Ana:3;Ben:3;Cem:3;Dara:2;Eli:2
2026-10-02,Airport taxi (Eli came by train),Ben,48.00,EUR,equal,all-Eli,
2026-10-02,Groceries for the flat,Eli,63.75,EUR,equal,all,
2026-10-03,Dinner at Taberna do Mar,Cem,212.40,EUR,itemized,all,Ana:31;Ben:28.50;Cem:24;Dara:36;Eli:19.50;shared:42
2026-10-03,Surf lesson (booked on a US site),Dara,180.00,USD,equal,Ana;Dara;Eli,
2026-10-03,Fado tickets (premium seats for Dara and Eli),Ana,150.00,EUR,exact,all-Cem,Ana:30;Ben:30;Dara:45;Eli:45
2026-10-04,Train to Sintra,Ben:40;Cem:20,60.00,EUR,equal,all,
2026-10-04,Wine for the flat,Dara,36.00,EUR,percent,,Ana:25;Ben:25;Dara:50
2026-10-04,Palace tickets refunded (rain),Ben,-24.00,EUR,equal,Ben;Cem,
2026-10-05,Snacks at the London layover,Cem,27.00,GBP,equal,all,
2026-10-05,Eli paid Ana back for the apartment,Eli,100.00,EUR,payment,Ana,
FILE:examples/lisbon-settle-output.md
# 清算プラン
基本通貨: EUR 人: 5 行: 11 総支出: 1,583.07 EUR
使用レート: 1 GBP = 1.16 EUR、1 USD = 0.92 EUR
## 支出
| # | 日付 | 内容 | 支払い者 | 金額 | EUR換算 | 分割 | 対象 |
|---|---|---|---|---|---|---|---|
| 1 | 2026-10-02 | アパート3泊(DaraとEliは2泊) | Ana | 840.00 EUR | 840.00 | shares | all |
| 2 | 2026-10-02 | 空港タクシー(Eliは電車で来た) | Ben | 48.00 EUR | 48.00 | equal | Ana, Ben, Cem, Dara |
| 3 | 2026-10-02 | フラット用食料品 | Eli | 63.75 EUR | 63.75 | equal | all |
| 4 | 2026-10-03 | Taberna do Marでのディナー | Cem | 212.40 EUR | 212.40 | itemized (+17.3% tax/tip) | all |
| 5 | 2026-10-03 | サーフレッスン(米国サイトで予約) | Dara | 180.00 USD | 165.60 | equal | Ana, Dara, Eli |
| 6 | 2026-10-03 | ファドチケット(DaraとEliはプレミアムシート) | Ana | 150.00 EUR | 150.00 | exact | Ana, Ben, Dara, Eli |
| 7 | 2026-10-04 | シントラ行き電車 | Ben 40.00、Cem 20.00 | 60.00 EUR | 60.00 | equal | all |
| 8 | 2026-10-04 | フラット用ワイン | Dara | 36.00 EUR | 36.00 | percent | Ana, Ben, Dara |
| 9 | 2026-10-04 | 宮殿チケット返金(雨) | Ben | -24.00 EUR | -24.00 | equal | Ben, Cem |
| 10 | 2026-10-05 | ロンドン乗り継ぎ時の軽食 | Cem | 27.00 GBP | 31.32 | equal | all |
| 11 | 2026-10-05 | EliがAnaにアパート代を返済 | Eli | 100.00 EUR | 100.00 | payment | Anaへ |
## 残高
| 人 | 支払額 | 負担額 | 送金済み | 受取済み | 純額 |
|---|---|---|---|---|---|
| Ana | 990.00 | 377.31 | 0.00 | 100.00 | 512.69(受け取る) |
| Ben | 64.00 | 307.17 | 0.00 | 0.00 | -243.17(支払う) |
| Cem | 263.72 | 262.87 | 0.00 | 0.00 | 0.85(受け取る) |
| Dara | 201.60 | 342.54 | 0.00 | 0.00 | -140.94(支払う) |
| Eli | 63.75 | 293.18 | 100.00 | 0.00 | -129.43(支払う) |
チェック: 純額の合計は0.00(支払額1,583.07 = 負担額1,583.07)。
## 送金(4回)
1. BenはAnaに**243.17 EUR**を支払う
2. DaraはAnaに**140.94 EUR**を支払う
3. EliはAnaに**128.58 EUR**を支払う
4. EliはCemに**0.85 EUR**を支払う
最小可能数: 残高のある5人に対して4回の送金(それより小さいサブグループは単独で均等になりません)。
## グループチャット用メッセージ
```
清算 (EUR、11行、レート: GBP 1.16、USD 0.92):
- Ben -> Ana: 243.17 EUR
- Dara -> Ana: 140.94 EUR
- Eli -> Ana: 128.58 EUR
- Eli -> Cem: 0.85 EUR
完全なテーブルは共有シートにあります。行が間違っている場合は2日以内に連絡してください。
```
FILE:examples/flat-october.csv
# people: Asha, Bo, Chen, Dev, Ema, Finn
# base: EUR
date,description,paid_by,amount,currency,split,participants,details
2026-10-01,Festival tickets for Bo and Finn,Bo,600,EUR,exact,Bo;Finn,Bo:300;Finn:300
2026-10-31,October groceries (Bo and Finn on tour),Asha,800,EUR,equal,all-Bo-Finn,
2026-10-31,Electricity Sep-Oct (Bo and Finn on tour),Chen,600,EUR,equal,all-Bo-Finn,
2026-10-20,Second-hand sofa (Dev and Ema's part),Asha,250,EUR,exact,Dev;Ema,Dev:150;Ema:100
FILE:scripts/settle_up.py
#!/usr/bin/env python3
"""共有費用の割り勘と清算プランナー(Python 3.8+ 標準ライブラリのみ)。
共有コスト(旅行、共同生活、イベント)のCSV台帳を読み取り、すべての行を整数セントで分割し、
グループを清算する最小可能な送金数を見つけます(ゼロ和サブグループの正確な検索。「最大で人数 - 1」だけではありません)。
CSV列(ヘッダー行が必要、追加の列は無視):
date,description,paid_by,amount,currency,split,participants,details
paid_by Ana 1人の支払い者
Ana:40;Ben:20 複数の支払い者(行の通貨での金額、合計が一致する必要があります)
amount 行の通貨での合計。マイナス = paid_byが受け取った返金
currency 空白 = 基本通貨
split equal | shares | exact | percent | itemized | payment
participants all | Ana;Ben;Cem | all-Eli(Eli以外の全員)| 空白 = details内の名前
details shares: Ana:3;Ben:2 (participantsにリストされているが記載されていない人は1を取得)
exact: Ana:30;Ben:45 (残りがあれば、他のparticipants間で均等に分割)
percent: Ana:50;Ben:30;Cem:20 (合計100でなければなりません)
itemized: Ana:18;Ben:22.50;shared:12 (税金/チップ前の項目価格。「shared」項目は
均等に分割。amount - items = 税金/サービス/チップで、各人が注文したものに比例して分割)
payment: (なし)paid_byは`amount`をparticipantsの1人に渡しました
先頭のコメント行はグループとレートを設定します(またはCLIフラグを使用):
# people: Ana, Ben, Cem
# base: EUR
# rates: USD=0.92, GBP=1.16 (その通貨の1単位 = 基本通貨のレート単位)
使用法:
python3 settle_up.py ledger.csv [--base EUR] [--rates USD=0.92,GBP=1.16] [--people Ana,Ben]
[--detail Ana] [--json]
python3 settle_up.py --self-test
終了コード: 0 = 清算プランが出力された、2 = 不正な入力(不明な分割、一致しない合計、
為替レートの欠落、厳密なリスト内の不明な人物)。
"""
import argparse
import csv
import io
import json
import re
import sys
from decimal import Decimal, ROUND_HALF_UP, InvalidOperation
MAX_EXACT = 18 # 正確な最小検索のための残高がゼロでない人(2^18状態)
SPLITS = ("equal", "shares", "exact", "percent", "itemized", "payment")
class LedgerError(ValueError):
pass
def dec(s, what):
try:
return Decimal(str(s).replace(",", "").strip())
except (InvalidOperation, AttributeError):
raise LedgerError("%s: %r は数値ではありません" % (what, s))
def to_cents(amount):
return int((amount * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP))
def fmt(cents, cur=""):
sign = "-" if cents < 0 else ""
c = abs(cents)
s = "%s%d.%02d" % (sign, c // 100, c % 100)
s = re.sub(r"(\d)(?=(\d{3})+\.)", r"\1,", s)
return (s + " " + cur).strip()
def allocate(total, weights, order):
"""整数`total`セントを`weights`(名前 -> Decimal >= 0の辞書)に比例して分割します。
最大剰余法。同点の場合は`order`でより早い名前が優先されます。合計は正確です。"""
names = [n for n in order if n in weights]
wsum = sum(weights[n] for n in names)
if wsum <= 0:
raise LedgerError("重みの合計がゼロです")
sign = -1 if total < 0 else 1
t = abs(total)
raw = {n: Decimal(t) * weights[n] / wsum for n in names}
base = {n: int(raw[n]) for n in names}
left = t - sum(base.values())
rank = sorted(names, key=lambda n: (-(raw[n] - base[n]), order.index(n)))
for n in rank[:left]:
base[n] += 1
return {n: sign * v for n, v in base.items() if v}
def parse_pairs(text, what):
out = {}
for part in [p for p in re.split(r"[;|]", text or "") if p.strip()]:
if ":" not in part:
raise LedgerError("%s: %r は Name:value の形式であるべきです" % (what, part.strip()))
k, v = part.split(":", 1)
k = k.strip()
out[k] = out.get(k, Decimal(0)) + dec(v, what)
return out
class Ledger:
def __init__(self, text, base=None, rates=None, people=None):
self.warnings = []
self.base = base
self.rates = {}
self.people = []
self._canon = {}
header_people = None
body = []
for line in text.splitlines():
s = line.strip()
if s.startswith("#"):
m = re.match(r"#\s*(people|base|rates?)\s*:\s*(.*)$", s, re.I)
if m:
key, val = m.group(1).lower(), m.group(2)
if key == "people":
header_people = [p.strip() for p in re.split(r"[,;]", val) if p.strip()]
elif key == "base" and not self.base:
self.base = val.strip().upper()
elif key.startswith("rate"):
self._add_rates(val)
continue
body.append(line)
self.base = (self.base or "EUR").upper()
if rates:
self._add_rates(rates)
self.rates[self.base] = Decimal(1)
for p in (people or header_people or []):
self._person(p, declare=True)
self.strict = bool(people or header_people)
rows = list(csv.DictReader(io.StringIO("\n".join(body))))
if not rows:
raise LedgerError("支出行が見つかりません(ヘッダー行と少なくとも1行が必要です)")
need = {"description", "paid_by", "amount", "split"}
cols = {c.strip().lower() for c in rows[0].keys() if c}
if not need <= cols:
raise LedgerError("CSV列が不足しています: %s" % ", ".join(sorted(need - cols)))
self.rows = []
for r in rows:
r = {(k or "").strip().lower(): (v or "").strip() for k, v in r.items()}
if not any(r.values()):
continue
self.rows.append(r)
# 最初のパス: 人リストが指定されていない場合にすべての名前を学習
if not self.strict:
for r in self.rows:
for p in self._names_in(r):
self._person(p, declare=True)
self.lines = [self._line(n, r) for n, r in enumerate(self.rows, 1)]
self._check_duplicates()
# -- 人とレート
def _add_rates(self, text):
for part in re.split(r"[,;]", text):
if not part.strip():
continue
if "=" not in part:
raise LedgerError("レート %r は USD=0.92 の形式であるべきです" % part.strip())
k, v = part.split("=", 1)
r = dec(v, "レート " + k.strip())
if r <= 0:
raise LedgerError("%s のレートは正でなければなりません" % k.strip())
self.rates[k.strip().upper()] = r
def _person(self, name, declare=False):
key = name.strip().lower()
if not key:
raise LedgerError("空の人名")
if key in self._canon:
canon = self._canon[key]
if canon != name.strip() and declare:
self.warnings.append("'%s' と '%s' は同じ人として扱われます" % (canon, name.strip()))
return canon
if not declare:
raise LedgerError("不明な人 %r(人リストにありません)" % name.strip())
self._canon[key] = name.strip()
self.people.append(name.strip())
return name.strip()
def _names_in(self, r):
names = []
pb = r.get("paid_by", "")
names += list(parse_pairs(pb, "paid_by").keys()) if ":" in pb else [pb]
part = r.get("participants", "")
if part and not part.lower().startswith("all"):
names += [p for p in re.split(r"[;|]", part) if p.strip()]
if r.get("split", "").lower() != "payment":
names += [k for k in parse_pairs(r.get("details", ""), "details") if k.lower() != "shared"]
return [n for n in names if n.strip()]
def _participants(self, r, ln):
part = r.get("participants", "").strip()
if not part:
return None
if part.lower().startswith("all"):
rest = part[3:].strip()
excl = []
if rest:
rest = re.sub(r"^(except|but|without)\s+", "", rest.lstrip("-").strip(), flags=re.I)
excl = [self._person(x) for x in re.split(r"[-;|,]", rest) if x.strip()]
return [p for p in self.people if p not in excl]
return [self._person(p) for p in re.split(r"[;|]", part) if p.strip()]
# -- 1つのCSV行 -> 基本セントでの支払額 / 負担額
def _line(self, ln, r):
desc = r.get("description") or "行 %d" % ln
where = "行 %d (%s)" % (ln, desc)
split = (r.get("split") or "equal").lower()
if split not in SPLITS:
raise LedgerError("%s: 不明な分割 %r(%s を使用してください)" % (where, split, ", ".join(SPLITS)))
cur = (r.get("currency") or self.base).upper()
if cur not in self.rates:
raise LedgerError("%s: %s の為替レートがありません('# rates: %s=...' または --rates を追加してください)" % (where, cur, cur))
amount = dec(r.get("amount"), where + " 金額")
if amount == 0:
self.warnings.append("%s は金額0でスキップされました" % where)
total = to_cents(amount * self.rates[cur])
# 支払い者
pb = r.get("paid_by", "")
if ":" in pb:
pays = {self._person(k): v for k, v in parse_pairs(pb, where + " paid_by").items()}
if sum(pays.values()) != amount:
raise LedgerError("%s: 支払い者の合計は %s ですが、金額は %s です" % (where, sum(pays.values()), amount))
paid = allocate(total, {k: abs(v) for k, v in pays.items()}, self.people) if total else {}
if total < 0:
paid = {k: -abs(v) for k, v in paid.items()}
else:
paid = {self._person(pb): total} if total else {}
people = self._participants(r, ln)
details = parse_pairs(r.get("details", ""), where + " details") if split != "payment" else {}
share = {}
if split == "payment":
if not people or len(people) != 1:
raise LedgerError("%s: 支払いにはparticipantsに受取人が正確に1人必要です" % where)
share = {people[0]: total}
elif split == "equal":
people = people or self.people
share = allocate(total, {p: Decimal(1) for p in people}, self.people)
elif split == "shares":
d = {self._person(k): v for k, v in details.items()}
people = people or list(d)
w = {p: d.get(p, Decimal(1)) for p in people}
extra = [p for p in d if p not in people]
if extra:
raise LedgerError("%s: %s がdetailsにありますが、participantsにはありません" % (where, ", ".join(extra)))
if any(v < 0 for v in w.values()):
raise LedgerError("%s: シェアは負にできません" % where)
share = allocate(total, w, self.people)
elif split == "percent":
d = {self._person(k): v for k, v in details.items()}
if sum(d.values()) != 100:
raise LedgerError("%s: パーセンテージの合計は %s で、100ではありません" % (where, sum(d.values())))
people = list(d)
share = allocate(total, d, self.people)
elif split == "exact":
d = {self._person(k): v for k, v in details.items()}
people = people or list(d)
fixed = sum(d.values())
rest_people = [p for p in people if p not in d]
if fixed > abs(amount) or (fixed != abs(amount) and not rest_people):
raise LedgerError("%s: 正確な金額の合計は %s ですが、請求書は %s です" % (where, fixed, amount))
w = dict(d)
if rest_people:
each = (abs(amount) - fixed) / len(rest_people)
for p in rest_people:
w[p] = each
share = allocate(total, w, self.people)
elif split == "itemized":
people = people or self.people
shared = details.pop("shared", details.pop("Shared", Decimal(0)))
d = {self._person(k): v for k, v in details.items()}
items = sum(d.values()) + shared
if items > abs(amount):
raise LedgerError("%s: 項目の合計は %s で、請求書 %s より多いです" % (where, items, amount))
per = {p: d.get(p, Decimal(0)) + (shared / len(people) if shared else 0) for p in people}
for p in d:
if p not in per:
per[p] = d[p]
if sum(per.values()) <= 0:
raise LedgerError("%s: 分割する項目がありません" % where)
# 税金/チップは各人の項目に比例 == 項目の重みで合計を配分
share = allocate(total, per, self.people)
rec = {"n": ln, "date": r.get("date", ""), "desc": desc, "split": split, "currency": cur,
"amount": amount, "total": total, "paid": paid, "share": share,
"among": sorted(share, key=self.people.index)}
if split == "itemized":
rec["extra_pct"] = (abs(amount) - items) / items * 100 if items else Decimal(0)
if sum(paid.values()) != sum(share.values()):
raise LedgerError("%s: 内部丸めエラー" % where) # 想定外
return rec
def _check_duplicates(self):
seen = {}
for l in self.lines:
key = (l["date"], l["desc"].lower(), l["total"], tuple(sorted(l["paid"])))
if key in seen:
self.warnings.append("行 %d は行 %d の重複のように見えます(%s、%s)" % (l["n"], seen[key], l["desc"], fmt(l["total"], self.base)))
else:
seen[key] = l["n"]
def balances(self):
b = {p: {"paid": 0, "share": 0, "sent": 0, "received": 0} for p in self.people}
for l in self.lines:
if l["split"] == "payment":
for p, c in l["paid"].items():
b[p]["sent"] += c
for p, c in l["share"].items():
b[p]["received"] += c
else:
for p, c in l["paid"].items():
b[p]["paid"] += c
for p, c in l["share"].items():
b[p]["share"] += c
for p in b:
b[p]["net"] = b[p]["paid"] + b[p]["sent"] - b[p]["share"] - b[p]["received"]
return b
# ----------------------------------------------------------------------------- 清算
def greedy(net, order):
"""最大債務者が最大債権者に支払い、全員が均等になるまで続けます(最大k-1回の送金)。"""
cred = {p: v for p, v in net.items() if v > 0}
debt = {p: -v for p, v in net.items() if v < 0}
out = []
while cred and debt:
c = max(cred, key=lambda p: (cred[p], -order.index(p)))
d = max(debt, key=lambda p: (debt[p], -order.index(p)))
x = min(cred[c], debt[d])
out.append((d, c, x))
cred[c] -= x
debt[d] -= x
if not cred[c]:
del cred[c]
if not debt[d]:
del debt[d]
return out
def minimal(net, order):
"""最小送金数: n - (最大数の互いに素なゼロ和グループ)。(送金、グループ、正確) を返します。"""
people = [p for p in order if net.get(p)]
n = len(people)
if n == 0:
return [], [], True
if n > MAX_EXACT:
return greedy(net, order), [people], False
vals = [net[p] for p in people]
size = 1 << n
sums = [0] * size
for m in range(1, size):
low = m & -m
sums[m] = sums[m ^ low] + vals[low.bit_length() - 1]
dp = [0] * size
for m in range(1, size):
best = 0
mm = m
while mm:
low = mm & -mm
v = dp[m ^ low]
if v > best:
best = v
mm ^= low
dp[m] = best + (1 if sums[m] == 0 else 0)
# 各ゼロ和チェックポイントでグループを切り取りながら下に進む
groups, m, last = [], size - 1, size - 1
while m:
mm, pick = m, None
target = dp[m] - (1 if sums[m] == 0 else 0)
while mm:
low = mm & -mm
if dp[m ^ low] == target:
pick = low
break
mm ^= low
m ^= pick
if sums[m] == 0:
groups.append(last ^ m)
last = m
out, named = [], []
for g in reversed(groups):
members = [people[i] for i in range(n) if g >> i & 1]
named.append(members)
out += greedy({p: net[p] for p in members}, order)
return out, named, True
# ----------------------------------------------------------------------------- 出力
def render(led, detail=None):
b = led.balances()
net = {p: b[p]["net"] for p in led.people}
tr, groups, exact = minimal(net, led.people)
gr = greedy(net, led.people)
B = led.base
o = ["# 清算プラン", "",
"基本通貨: %s 人: %d 行: %d 総支出: %s" % (
B, len(led.people), len(led.lines),
fmt(sum(l["total"] for l in led.lines if l["split"] != "payment"), B))]
others = sorted(c for c in led.rates if c != B)
if others:
o.append("使用レート: " + ", ".join("1 %s = %s %s" % (c, led.rates[c].normalize(), B) for c in others))
o += ["", "## 支出", "", "| # | 日付 | 内容 | 支払い者 | 金額 | %s換算 | 分割 | 対象 |" % B, "|---|---|---|---|---|---|---|---|"]
for l in led.lines:
payer = ", ".join("%s %s" % (p, fmt(c)) if len(l["paid"]) > 1 else p for p, c in l["paid"].items())
among = "all" if len(l["among"]) == len(led.people) and l["split"] != "payment" else ", ".join(l["among"])
split = l["split"] + (" (+%.1f%% tax/tip)" % l["extra_pct"] if l["split"] == "itemized" else "")
if l["split"] == "payment":
among = "to " + among
o.append("| %d | %s | %s | %s | %s %s | %s | %s | %s |" % (l["n"], l["date"], l["desc"].replace("|", "/"), payer,
fmt(to_cents(l["amount"])), l["currency"], fmt(l["total"]), split, among))
o += ["", "## 残高", "", "| 人 | 支払額 | 負担額 | 送金済み | 受取済み | 純額 |", "|---|---|---|---|---|---|"]
for p in led.people:
x = b[p]
state = "受け取る" if x["net"] > 0 else ("支払う" if x["net"] < 0 else "均等")
o.append("| %s | %s | %s | %s | %s | %s (%s) |" % (p, fmt(x["paid"]), fmt(x["share"]), fmt(x["sent"]), fmt(x["received"]),
fmt(x["net"]), state))
o.append("")
o.append("チェック: 純額の合計は %s(支払額 %s = 負担額 %s)。" % (fmt(sum(net.values())), fmt(sum(x["paid"] for x in b.values())),
fmt(sum(x["share"] for x in b.values()))))
nz = sum(1 for v in net.values() if v)
o += ["", "## 送金(%d回)" % len(tr), ""]
if not tr:
o.append("全員が均等です。支払うものはありません。")
else:
for i, (d, c, x) in enumerate(tr, 1):
o.append("%d. %sは%sに**%s**を支払う" % (i, d, c, fmt(x, B)))
o.append("")
if exact:
if len(groups) > 1:
o.append("最小可能数: %d回の送金。グループは互いに清算する%dつのセットに分割されます(%s)。したがって、%d人は%d回の送金のみが必要です。最大債務から最大債権への支払いでは%d回かかります。" % (
len(tr), len(groups), "; ".join(" + ".join(g) for g in groups), nz, len(tr), len(gr)))
else:
o.append("最小可能数: 残高のある%d人に対して%d回の送金(それより小さいサブグループは単独で均等になりません)。" % (len(tr), nz))
else:
o.append("%d人に残高があります。正確な最小検索はスキップされました(制限 %d)。貪欲法では最大%d回の送金が使用されます。" % (nz, MAX_EXACT, nz - 1))
if detail:
p = led._person(detail)
o += ["", "## 詳細: %s" % p, "", "| # | 内容 | 支払額 | 負担額 | 累計純額 |", "|---|---|---|---|---|"]
run = 0
for l in led.lines:
pd, sh = l["paid"].get(p, 0), l["share"].get(p, 0)
if not pd and not sh:
continue
run += pd - sh
o.append("| %d | %s | %s | %s | %s |" % (l["n"], l["desc"], fmt(pd), fmt(sh), fmt(run)))
if led.warnings:
o += ["", "## 警告", ""] + ["- " + w for w in led.warnings]
o += ["", "## グループチャット用メッセージ", "", "```"]
msg = ["清算 (%s、%d行、レート: %s):" % (B, len(led.lines), ", ".join("%s %s" % (c, led.rates[c].normalize()) for c in others) or "なし")]
msg += ["- %s -> %s: %s" % (d, c, fmt(x, B)) for d, c, x in tr] or ["- 全員が均等"]
msg.append("完全なテーブルは共有シートにあります。行が間違っている場合は2日以内に連絡してください。")
o += msg + ["```"]
return "\n".join(o) + "\n"
def as_json(led):
b = led.balances()
net = {p: b[p]["net"] for p in led.people}
tr, groups, exact = minimal(net, led.people)
return {"base": led.base, "people": led.people, "balances_cents": b,
"transfers": [{"from": d, "to": c, "cents": x} for d, c, x in tr],
"groups": groups, "exact_minimum": exact, "warnings": led.warnings}
# ----------------------------------------------------------------------------- セルフテスト
def self_test():
import random
cases = []
def case(name, fn):
try:
ok, err = bool(fn()), ""
except Exception as e: # noqa
ok, err = False, repr(e)
cases.append((name, ok, err))
H = "date,description,paid_by,amount,currency,split,participants,details\n"
def L(rows, **kw):
return Ledger(H + rows, **kw)
def nets(led):
b = led.balances()
return {p: b[p]["net"] for p in led.people}
def raises(fn):
try:
fn()
except LedgerError:
return True
return False
case("セントまでの均等分割", lambda: nets(L(",Taxi,Ana,10.00,,equal,Ana;Ben;Cem,\n")) == {"Ana": 666, "Ben": -333, "Cem": -333})
case("剰余セントは順序の最初の人へ", lambda: L(",X,Ana,0.10,,equal,Ana;Ben;Cem,\n").lines[0]["share"] == {"Ana": 4, "Ben": 3, "Cem": 3})
case("全員から除外", lambda: Ledger("# people: Ana, Ben, Cem\n" + H + ",X,Ana,30,,equal,all-Cem,\n").lines[0]["among"] == ["Ana", "Ben"])
case("all except の語句", lambda: Ledger("# people: Ana, Ben, Cem\n" + H + ",X,Ana,30,,equal,all except Ben,\n").lines[0]["among"] == ["Ana", "Cem"])
case("シェアの重み", lambda: L(",Room,Ana,100,,shares,,Ana:3;Ben:1\n").lines[0]["share"] == {"Ana": 7500, "Ben": 2500})
case("リストにない参加者のシェアはデフォルト1", lambda: L(",Room,Ana,90,,shares,Ana;Ben;Cem,Ana:2;Ben:0\n").lines[0]["share"] == {"Ana": 6000, "Cem": 3000})
case("正確な金額", lambda: L(",Tickets,Ana,75,,exact,,Ana:30;Ben:45\n").lines[0]["share"] == {"Ana": 3000, "Ben": 4500})
case("正確な金額と残りの均等分割", lambda: L(",T,Ana,100,,exact,Ana;Ben;Cem,Ana:40\n").lines[0]["share"] == {"Ana": 4000, "Ben": 3000, "Cem": 3000})
case("正確な金額の不一致は拒否", lambda: raises(lambda: L(",T,Ana,80,,exact,,Ana:30;Ben:45\n")))
case("パーセント", lambda: L(",Wine,Ana,36,,percent,,Ana:25;Ben:25;Cem:50\n").lines[0]["share"] == {"Ana": 900, "Ben": 900, "Cem": 1800})
case("パーセントが100でない場合は拒否", lambda: raises(lambda: L(",W,Ana,36,,percent,,Ana:25;Ben:25\n")))
case("共有と税金/チップを含む明細付き", lambda: L(",Dinner,Ana,66,,itemized,Ana;Ben,Ana:20;Ben:30;shared:10\n").lines[0]["share"] == {"Ana": 2750, "Ben": 3850})
case("請求書を超える明細付きは拒否", lambda: raises(lambda: L(",D,Ana,40,,itemized,Ana;Ben,Ana:20;Ben:30\n")))
case("支払い行", lambda: nets(L(",Food,Ana,20,,equal,Ana;Ben,\n,Paid back,Ben,10,,payment,Ana,\n")) == {"Ana": 0, "Ben": 0})
case("支払いには受取人が1人必要", lambda: raises(lambda: L(",P,Ben,10,,payment,Ana;Cem,\n")))
case("複数の支払い者", lambda: L(",Train,Ana:40;Ben:20,60,,equal,Ana;Ben;Cem,\n").lines[0]["paid"] == {"Ana": 4000, "Ben": 2000})
case("複数の支払い者は合計が一致する必要がある", lambda: raises(lambda: L(",Train,Ana:40;Ben:10,60,,equal,Ana;Ben,\n")))
case("返金(マイナス金額)", lambda: nets(L(",Museum refund,Ben,-24,,equal,Ben;Cem,\n")) == {"Ben": -1200, "Cem": 1200})
case("通貨換算", lambda: L(",Surf,Ana,100,USD,equal,Ana;Ben,\n", rates="USD=0.92").lines[0]["total"] == 9200)
case("コメント行からのレート", lambda: Ledger("# base: EUR\n# rates: USD=0.92, GBP=1.16\n" + H + ",S,Ana,10,GBP,equal,Ana;Ben,\n").lines[0]["total"] == 1160)
case("レートの欠落は拒否", lambda: raises(lambda: L(",S,Ana,10,CHF,equal,Ana;Ben,\n")))
case("不明な分割は拒否", lambda: raises(lambda: L(",S,Ana,10,,halves,Ana;Ben,\n")))
case("厳密な人リストはタイプミスを拒否", lambda: raises(lambda: Ledger("# people: Ana, Ben\n" + H + ",S,Anna,10,,equal,all,\n")))
case("大文字小文字を区別しない名前の統合", lambda: L(",A,Ana,10,,equal,ana;Ben,\n").people == ["Ana", "Ben"])
case("重複行の警告", lambda: any("重複" in w for w in L("1,Taxi,Ana,10,,equal,Ana;Ben,\n1,Taxi,Ana,10,,equal,Ana;Ben,\n").warnings))
case("列の欠落は拒否", lambda: raises(lambda: Ledger("what,who\nx,y\n")))
case("不正な数値は拒否", lambda: raises(lambda: L(",S,Ana,ten,,equal,Ana;Ben,\n")))
# 最小送金数
case("ゼロ和ペアが見つかる(4人 -> 2回の送金)", lambda: len(minimal({"A": 500, "B": -500, "C": 300, "D": -300}, list("ABCD"))[0]) == 2)
case("ゼロ和サブグループなし -> 人数 - 1回の送金", lambda: len(greedy({"A": 600, "B": 400, "C": -500, "D": -500}, list("ABCD"))) == 3 and len(minimal({"A": 600, "B": 400, "C": -500, "D": -500}, list("ABCD"))[0]) == 3)
case("6人で最小が貪欲法に勝つ", lambda: (lambda n: len(minimal(n, list("ABCDEF"))[0]) == 4 and len(greedy(n, list("ABCDEF"))) == 5)({"A": 700, "B": 300, "C": 250, "D": -500, "E": -450, "F": -300}))
case("全員が均等 -> 送金なし", lambda: minimal({"A": 0, "B": 0}, ["A", "B"])[0] == [])
def brute_min(net):
ps = [p for p in net if net[p]]
best = len(ps) - 1 if ps else 0
for parts in _partitions(ps):
if all(sum(net[p] for p in g) == 0 for g in parts):
best = min(best, len(ps) - len(parts))
return best
def check_random():
rnd = random.Random(7)
for _ in range(60):
k = rnd.randint(2, 7)
vals = [rnd.choice([-1, 1]) * rnd.choice([100, 200, 300, 500, 250, 50]) for _ in range(k - 1)]
vals.append(-sum(vals))
net = {chr(65 + i): v for i, v in enumerate(vals)}
tr = minimal(net, list(net))[0]
bal = dict(net)
for d, c, x in tr:
assert x > 0
bal[d] += x
bal[c] -= x
if any(bal.values()) or len(tr) != brute_min(net):
return False
return True
case("60のランダムグループで最小 == 総当たり、かつ全員を清算", check_random)
case("送金が例のような台帳を清算する", lambda: _settles(L(",Flat,Ana,840,,shares,,Ana:3;Ben:3;Cem:2\n,Food,Cem,63.75,,equal,all,\n,Wine,Ben,36,,percent,,Ana:25;Ben:25;Cem:50\n")))
case("レンダリングに送金とチャットメッセージがある", lambda: "グループチャット用メッセージ" in render(L(",Taxi,Ana,10,,equal,Ana;Ben,\n")))
case("詳細ビュー", lambda: "詳細: Ben" in render(L(",Taxi,Ana,10,,equal,Ana;Ben,\n"), "ben"))
case("JSON出力", lambda: as_json(L(",Taxi,Ana,10,,equal,Ana;Ben,\n"))["transfers"] == [{"from": "Ben", "to": "Ana", "cents": 500}])
case("千単位のフォーマット", lambda: fmt(123456789, "EUR") == "1,234,567.89 EUR" and fmt(-5) == "-0.05")
passed = sum(1 for _, ok, _ in cases if ok)
for name, ok, err in cases:
print(("PASS " if ok else "FAIL ") + name + ((" " + err) if err else ""))
print("%d/%d パス" % (passed, len(cases)))
return 0 if passed == len(cases) else 1
def _partitions(items):
if not items:
yield []
return
first, rest = items[0], items[1:]
for p in _partitions(rest):
for i in range(len(p)):
yield p[:i] + [[first] + p[i]] + p[i + 1:]
yield [[first]] + p
def _settles(led):
b = led.balances()
net = {p: b[p]["net"] for p in led.people}
for d, c, x in minimal(net, led.people)[0]:
net[d] += x
net[c] -= x
return not any(net.values())
def main(argv=None):
ap = argparse.ArgumentParser(description="共有費用を分割し、最小数の清算送金を計画します。")
ap.add_argument("file", nargs="?", help="CSV台帳('-' = 標準入力)")
ap.add_argument("--base", default=None, help="基本通貨(デフォルト: '# base:' 行または EUR)")
ap.add_argument("--rates", default=None, help="USD=0.92,GBP=1.16(1単位 = 基本通貨のレート単位)")
ap.add_argument("--people", default=None, help="カンマ区切りのグループメンバー(厳密: タイプミスはエラー)")
ap.add_argument("--detail", default=None, help="1人の行ごとの内訳を表示")
ap.add_argument("--json", action="store_true")
ap.add_argument("--self-test", action="store_true")
a = ap.parse_args(argv)
if a.self_test:
return self_test()
if not a.file:
ap.error("CSV台帳または --self-test を指定してください")
try:
text = sys.stdin.read() if a.file == "-" else open(a.file, encoding="utf-8-sig").read()
people = [p.strip() for p in a.people.split(",")] if a.people else None
led = Ledger(text, a.base, a.rates, people)
out = json.dumps(as_json(led), indent=2) + "\n" if a.json else render(led, a.detail)
except (OSError, LedgerError) as e:
print("エラー: %s" % e, file=sys.stderr)
return 2
sys.stdout.write(out)
return 0
if __name__ == "__main__":
sys.exit(main())
ログインして完全なプロンプトを表示
次で続行:
ログインすると、次に同意したことになります: 利用規約 と プライバシーポリシー
使い方
このプロンプトは productivity 向けに設計されています。上の内容をコピーして、お好みの AI ツールに貼り付けてください。
最良の結果を得るには、プレースホルダー(角括弧や大文字で示された部分)を具体的な要件に置き換えてください。
ノート
0 件のコメント