"""R07原创构造底稿:只用标准库,以文章编号为顶层键;与网页引擎相互独立。""" from decimal import Decimal as D, ROUND_HALF_UP, ROUND_CEILING, getcontext from pathlib import Path import argparse import json import re getcontext().prec = 40 HERE = Path(__file__).resolve().parent LIMIT = D('1000000000000') def money(x): return D(str(x)).quantize(D('.01'), rounding=ROUND_HALF_UP) def fmt(x): if isinstance(x, str): return x v = money(x) return format(v, '.2f') if v != v.to_integral_value() else str(int(v)) def table(headers, rows): return {'headers': headers, 'rows': [[fmt(c) for c in r] for r in rows]} def loan(principal, annual, terms, months): """等额本金,月息按期初余额;逐笔现金四舍五入到分,末期封顶。""" p = money(principal) original = p q = money(p / terms) out = [] for m in range(1, months + 1): interest = money(p * D(str(annual)) / 12) if p else D(0) paid = min(p, q) if m < terms else p out.append([m, p, interest, paid, p - paid]) p -= paid assert original - sum(r[3] for r in out) == p assert all(r[1] - r[3] == r[4] for r in out) return out def ledger(rents, costs, opening=0, events=None): events = events or {} cash = money(opening) rows = [] for m, (r, c) in enumerate(zip(rents, costs), 1): event = money(events.get(m, 0)) net = money(r) - money(c) - event cash += net rows.append([m, money(r), money(c), event, net, cash]) assert money(opening) + sum(r[4] for r in rows) == cash return rows def build(): out = {} amort = loan(120000, '.06', 12, 6) cash = D(20000) rows = [] for m, begin, interest, principal, end in amort: net = 4000 - interest - principal cash += net rows.append([m, 4500, 500, 4000, interest, principal, net, cash, end]) out['DJ2-055'] = {'inputs': {'months': 6, 'opening_cash': 20000, 'loan': 120000, 'annual_rate': '.06', 'remaining_terms': 12, 'rent': 4500, 'operating': 500}, 'tables': { 'layers': table(['月序', '租金', '运营', '经营净额', '利息', '还本', '融资后现金', '月末现金', '贷款余额'], rows), 'compare': table(['六个月口径', '有贷款', '无贷款'], [['经营净额', 24000, 24000], ['利息', 2850, 0], ['还本', 60000, 0], ['净现金变化', -38850, 24000], ['期末现金', -18850, 44000]])}, 'checks': {'principal_repaid': 60000, 'interest': 2850, 'end_loan': 60000, 'min_month_end': cash, 'minimum_additional_funding': 18850, 'operating_less_interest': 21150}} assert sum(r[4] for r in rows) == 2850 and cash == -18850 vac = {} for name, empty in [('early', {1, 2}), ('late', {11, 12})]: vac[name] = ledger([0 if m in empty else 5000 for m in range(1, 13)], [1000] * 12, 2000, {2: 6000}) out['DJ2-056'] = {'inputs': {'months': 12, 'opening_cash': 2000, 'rent': 5000, 'monthly_operating': 1000, 'oneoff_month2': 6000, 'empty_early': [1, 2], 'empty_late': [11, 12]}, 'tables': {'timing': table(['月序', '前段空置租金', '前段净额', '前段月末', '后段空置租金', '后段净额', '后段月末'], [[a[0], a[1], a[4], a[5], b[1], b[4], b[5]] for a, b in zip(vac['early'], vac['late'])])}, 'checks': {'rent_total_each': 50000, 'operating_total_each': 12000, 'net_each': 32000, 'end_each': 34000, 'early_min': min(r[5] for r in vac['early']), 'late_min': min(r[5] for r in vac['late']), 'early_funding': 6000, 'late_funding': 0}} assert vac['early'][-1][5] == vac['late'][-1][5] == 34000 assert min(r[5] for r in vac['early']) == -6000 assert min(r[5] for r in vac['late']) == 4000 reserve = D(0) cash = D(0) rows = [] for m in range(1, 25): repair = D(9600 if m == 18 else 0) reserve += 600 - repair cash += 3500 - repair rows.append([m, 600, repair, reserve, cash, cash - reserve]) out['DJ2-057'] = {'inputs': {'months': 24, 'rent': 4000, 'monthly_operating': 500, 'monthly_internal_reserve': 600, 'repair_month': 18, 'repair': 9600}, 'tables': {'reserve': table(['月序', '内部提留', '实付维修', '准备余额', '合并现金', '非准备现金'], rows), 'reconcile': table(['项目', '金额'], [['累计租金', 96000], ['日常实付', 12000], ['维修实付', 9600], ['合并现金净增', 74400], ['累计内部提留', 14400], ['准备未用余额', 4800], ['错误再扣提留的现金', 60000]])}, 'checks': {'end_cash': cash, 'reserve': reserve, 'unallocated': cash - reserve, 'wrong_understatement': 14400, 'larger_repair': 14400, 'larger_repair_reserve_shortfall': 3600, 'larger_repair_end_cash': 69600}} assert cash == 74400 and reserve == 4800 and cash - reserve == 69600 cash = D(2000) liability = D(0) rows = [] for m in range(1, 7): deposit = D(6000 if m == 1 else -6000 if m == 6 else 0) liability += deposit cash += 2600 + deposit rows.append([m, 3000, 400, deposit, cash, liability, cash - liability]) out['DJ2-058'] = {'inputs': {'months': 6, 'opening_cash': 2000, 'rent': 3000, 'operating': 400, 'deposit': 6000, 'receive_month': 1, 'refund_month': 6}, 'tables': {'deposit': table(['月序', '租金', '运营', '押金收退', '合并现金', '待退负债', '现金减负债'], rows), 'compare': table(['期末口径', '按期退款', '退款移至次月'], [['合并现金', 17600, 23600], ['待退负债', 0, 6000], ['现金减负债', 17600, 17600], ['经营净额累计', 15600, 15600]])}, 'checks': {'rents': 18000, 'operating': 2400, 'net': 15600, 'end_cash': cash, 'end_liability': liability, 'wrong_income': 21600}} assert cash == 17600 and liability == 0 fit = {} for free in (0, 2, 3): records = ledger([0 if m <= free else 5000 for m in range(1, 13)], [600] * 12, 15000, {1: 8000}) fit[free] = records rows = [[r[0], '免租' if r[0] <= 2 else '收租', r[1], r[2], r[3], r[4], r[5] + 5000, 5000, r[5]] for r in fit[2]] out['DJ2-059'] = {'inputs': {'months': 12, 'opening_owner_cash': 15000, 'free_months': [1, 2], 'rent': 5000, 'operating': 600, 'setup_month1': 8000, 'deposit_month1': 5000, 'deposit_not_refunded_in_horizon': True}, 'tables': {'fitout': table(['月序', '状态', '实收租金', '运营', '一次付款', '业主净额', '合并月末现金', '待退押金', '剔押金月末'], rows), 'compare': table(['免租月数', '实收租金', '业主净额累计', '剔押金最低月末', '剔押金期末'], [[free, sum(r[1] for r in rs), sum(r[4] for r in rs), min(r[5] for r in rs), rs[-1][5]] for free, rs in fit.items()])}, 'checks': {'base_rent': 50000, 'operating': 7200, 'setup': 8000, 'owner_net': 34800, 'owner_end_cash': 49800, 'all_end_cash': 54800, 'min_owner_cash': 5800, 'first_paid_month': 3}} assert fit[2][-1][5] == 49800 and min(r[5] for r in fit[2]) == 5800 rows = [] for m in range(1, 13): rows.append([m, 36000 if m == 1 else 0, 3000, 36000 - 1800 * m, 1200 * m, 3000 * (12 - m)]) pvs = [] for rate in ('0', '.005', '.01'): pv = sum(D(3000) / (1 + D(rate)) ** m for m in range(12)) pvs.append([str(D(rate) * 100) + '%', 36000, money(pv), money(36000 - pv)]) out['DJ2-060'] = {'inputs': {'months': 12, 'rent_total': 36000, 'monthly_rent': 3000, 'monthly_operating': 1800, 'annual_payment_month': 1, 'discount_timing': '月初收款,第1月月初为t=0', 'discount_monthly': '.005'}, 'tables': {'bulk': table(['月序', '年付实收', '月付实收', '年付月末现金', '月付月末现金', '年付未覆盖月份租金'], rows), 'pv': table(['假设月折现率', '年付现值', '月付现值', '年付减月付'], pvs)}, 'checks': {'net_each': 14400, 'annual_mean_month_end': 24300, 'monthly_mean_month_end': 7800, 'mean_difference': 16500, 'pv_monthly_at_half_percent': pvs[1][2], 'pv_difference': pvs[1][3]}} assert sum(r[3] for r in rows) / 12 == 24300 assert sum(r[4] for r in rows) / 12 == 7800 rows = [[n, 3000 * n, 300 * n, 14400, 2700 * n - 14400, 2700 * n - 36000] for n in range(13)] cases = [] for rent, variable, fixed, extra in [(3000, 300, 14400, 0), (3000, 300, 14400, 21600), (1400, 300, 14400, 0), (300, 300, 14400, 0)]: margin = rent - variable need = int((D(fixed + extra) / margin).to_integral_value(rounding=ROUND_CEILING)) if margin > 0 else None cases.append([rent, variable, fixed + extra, str(need) if need is not None else '不存在', '可达' if need is not None and need <= 12 else '本年不可达', 12 * margin - fixed - extra]) out['DJ2-061'] = {'inputs': {'rent': 3000, 'variable_per_occupied_month': 300, 'annual_fixed_operating': 14400, 'months_available': 12, 'optional_zero_interest_principal': 21600, 'optional_terms': 12}, 'tables': {'occupancy': table(['出租月数', '租金', '变动实付', '固定实付', '经营净额', '另扣零息还本后'], rows), 'threshold': table(['月租', '出租月变动费', '待覆盖固定现金', '所需整月数', '可达性', '全年满租净额'], cases)}, 'checks': {'base_required': 6, 'five_months_net': -900, 'six_months_net': 1800, 'financed_required': 14, 'financed_full_net': -3600, 'low_rent_required': 14, 'zero_margin_unreachable': True}} assert rows[5][4] == -900 and rows[6][4] == 1800 and rows[12][5] == -3600 amort = loan(600000, '.06', 60, 6) cash = D(60000) rows = [] for m, begin, interest, principal, end in amort: net = 5000 - interest - principal cash += net rows.append([m, begin, interest, principal, end, 5000, net, cash]) exit_net = D(1200000 - 36000 - 540000 - 10000) bridge = [['假设售价', 1200000], ['已确认交易支出假设', -36000], ['当期正常还本后剩余贷款', -540000], ['尚未退还押金', -10000], ['退出净回款', exit_net], ['退出前账户现金', cash], ['全部结清后账户现金', cash + exit_net]] out['DJ2-062'] = {'inputs': {'months': 6, 'opening_all_cash': 60000, 'deposit_in_opening_cash': 10000, 'loan': 600000, 'annual_rate': '.06', 'remaining_terms': 60, 'rent': 6000, 'operating': 1000, 'sale_after_month6_payment': 1200000, 'transaction_cost_user_amount': 36000}, 'tables': {'loan': table(['月序', '期初贷款', '利息', '还本', '期末贷款', '经营净额', '融资后净额', '月末现金'], rows), 'bridge': table(['桥接项目', '金额'], bridge), 'prices': table(['假设售价', '退出净回款', '结清后账户现金'], [[p, p - 586000, cash + p - 586000] for p in (1100000, 1200000, 1300000, 500000)])}, 'checks': {'interest': 17250, 'repaid': 60000, 'remaining': 540000, 'operating_net': 30000, 'financing_net': -47250, 'cash_before_sale': cash, 'free_before_sale': cash - 10000, 'exit_net': exit_net, 'end_cash': cash + exit_net, 'negative_case_funding': 73250}} assert cash == 12750 and exit_net == 614000 and cash + exit_net == 626750 rows = [] base = D(50000) upgrade = D(50000) for m in range(1, 25): rent = 0 if m <= 2 else 5000 repair = 12000 if m == 1 else 8000 if m == 2 else 0 net = rent - 700 - repair base += 3400 upgrade += net rows.append([m, 4000, rent, repair, 3400, net, base, upgrade, upgrade - base]) sensitivity = [] for rent in (5000, 4600, 4300, 4000): gain = rent - 700 - 3400 catch = 2 + int((D(28200) / gain).to_integral_value(rounding=ROUND_CEILING)) if gain > 0 else None sensitivity.append([rent, gain, -28200 + 22 * gain, str(catch) if catch is not None and catch <= 120 else '120月内不追平', -28200 + 118 * gain]) out['DJ2-063'] = {'inputs': {'months': 24, 'opening_cash': 50000, 'base_rent': 4000, 'base_operating': 600, 'upgrade_rent_from_month3': 5000, 'upgrade_operating': 700, 'vacant_months': [1, 2], 'payment_month1': 12000, 'payment_month2': 8000}, 'tables': {'renovation': table(['月序', '原路径租金', '升级路径租金', '装修付款', '原路径净额', '升级净额', '原路径月末', '升级月末', '累计增量'], rows), 'sensitivity': table(['后续假设月租', '正常月净增量', '24月累计增量', '首次追平月序', '120月累计增量'], sensitivity)}, 'checks': {'base_net': 81600, 'upgrade_net': 73200, 'difference': -8400, 'min_upgrade_month_end': 28600, 'initial_incremental_cost': 28200, 'normal_gain': 900, 'catchup_month_extended': 34, 'month33_difference': -300, 'month34_difference': 600, 'one_more_vacancy_month24_difference': -13400}} assert base == 131600 and upgrade == 123200 and upgrade - base == -8400 assert min(r[7] for r in rows) == 28600 # 使用闭式/前缀和与递推表再次核对,避免只验证一个终点。 for row in out['DJ2-055']['tables']['layers']['rows']: m, rent, op, operating, interest, principal, net, cash, remaining = map(D, row) assert remaining == 120000 - 10000 * m assert interest == 600 - 50 * (m - 1) assert net == rent - op - interest - principal assert cash == 20000 - 6600 * m + 25 * m * (m - 1) for name, records in vac.items(): for i, row in enumerate(records): assert row[5] == 2000 + sum(x[1] - x[2] - x[3] for x in records[:i + 1]) for row in out['DJ2-057']['tables']['reserve']['rows']: m, allocation, paid, reserve, cash, free = map(D, row) repair_cumulative = 9600 if m >= 18 else 0 assert reserve == 600 * m - repair_cumulative assert cash == 3500 * m - repair_cumulative and free + reserve == cash for row in out['DJ2-058']['tables']['deposit']['rows']: m, rent, op, change, cash, debt, free = map(D, row) assert debt == (6000 if m < 6 else 0) assert cash == 2000 + 2600 * m + debt and free == cash - debt for row in out['DJ2-059']['tables']['fitout']['rows']: m = int(row[0]) assert D(row[-1]) == 15000 - 8000 - 600 * m + max(0, m - 2) * 5000 assert D(row[-3]) - D(row[-2]) == D(row[-1]) for row in out['DJ2-062']['tables']['loan']['rows']: m = D(row[0]) accrued = 3000 * m - 25 * m * (m - 1) assert D(row[4]) == 600000 - 10000 * m assert D(row[-1]) == 60000 - 5000 * m - accrued for row in out['DJ2-063']['tables']['renovation']['rows']: m = int(row[0]) expected = -16100 if m == 1 else -28200 + max(0, m - 2) * 900 assert D(row[-1]) == expected assert int((D(14400) / 2700).to_integral_value(rounding=ROUND_CEILING)) == 6 assert (6 - 1) * 2700 < 14400 <= 6 * 2700 assert -28200 + (34 - 3) * 900 < 0 <= -28200 + (34 - 2) * 900 # 中文金额与月序的关键叙述保留为可重跑的语义断言;全文仍需作者人工读审。 claims = { 'DJ2-055': ['六期累计还本60000元', '利息合计2850元', '现金净变化负38850元', '期末为负18850元', '额外需要补入18850元'], 'DJ2-056': ['净增32000元', '期末都是34000元', '甲在第二月已出现负6000元', '乙的最低观察月末为4000元', '只需补入这一笔6000元'], 'DJ2-057': ['第十七月准备余额10200元', '合并现金59500元', '准备余额变1200元', '合并现金变53400元', '合并现金净增74400元', '准备缺口3600元', '期末合并现金为69600元'], 'DJ2-058': ['第一月月末合并现金10600元', '剔除这笔负债后是4600元', '第五个月合并现金21000元', '退完后现金17600元', '经营净额都是15600元', '错报为21600元'], 'DJ2-059': ['第一月业主净流出8600元', '剔押金月末6400元', '降至5800元', '运营付款7200元', '业主现金净增34800元', '合并现金54800元'], 'DJ2-060': ['第一月末年付余额34200元', '月付余额1200元', '年付平均余额24300元', '月付7800元', '差额16500元', '月付租金现值35031.08元', '高968.92元'], 'DJ2-061': ['出租五月时,租金15000元', '仍负900元', '经营净额1800元', '得到6', '需14个完整月', '满租融资后现金仍负3600元'], 'DJ2-062': ['六个月归还本金60000元', '利息17250元', '剩余贷款540000元', '融资后净额负47250元', '出售前现金只剩12750元', '净回款614000元', '结清后账户现金626750元', '还需额外补入73250元'], 'DJ2-063': ['第一月增量负16100元', '第二月再负12100元', '累计落后28200元', '最低月末现金28600元', '第二十四月共经历22个', '累计增量仍负8400元', '总月序34', '第三十三月仍落后300元', '第三十四月领先600元', '期末还落后4600元', '变成负13400元']} for key, value in claims.items(): out[key]['narrative_claims'] = value out['DJ2-055']['checks'].update(total_rent=4500 * 6, total_operating=500 * 6) out['DJ2-056']['checks']['month2_all_payment'] = 1000 + 6000 out['DJ2-057']['checks']['month18_pre_payment_reserve'] = 600 * 18 out['DJ2-058']['checks']['monthly_operating_net'] = 3000 - 400 out['DJ2-060']['checks']['operating_total'] = 1800 * 12 out['DJ2-061']['checks'].update(base_margin=3000 - 300, low_rent_margin=1400 - 300) out['DJ2-062']['checks']['opening_owner_cash'] = 60000 - 10000 out['DJ2-063']['checks'].update(normal_months_to_catchup=34 - 2, extended_horizon=120, gross_rent_increment=5000 - 4000, renovation_total=12000 + 8000, first_month_increment=-12700 - 3400, second_month_increment=-8700 - 3400) # 对所有数值层次作范围核验,不把字符串里的金额或结果漏掉。 def bounds(v): if isinstance(v, dict): for x in v.values(): bounds(x) elif isinstance(v, list): for x in v: bounds(x) elif isinstance(v, (int, D)) and not isinstance(v, bool): assert abs(v) <= LIMIT elif isinstance(v, str) and re.fullmatch(r'-?\d+(\.\d+)?', v): assert abs(D(v)) <= LIMIT bounds(out) return out def article_check(data): ROOT = HERE.parents[4] # 仅仓库内正文核对需要,不让默认下载运行依赖目录深度。 """逐表全单元格对照,不以数字在任意位置出现替代行列匹配。""" plan = json.loads((HERE.parent / 'content-plan.json').read_text('utf-8')) result = {} for a in [r for r in plan['articles'] if r['cluster'] == 'R07']: p = ROOT / 'sites/daojijun.com/src/content/article' / (a['planned_path'].strip('/').split('/')[-1] + '.md') body = p.read_text('utf-8').split('---', 2)[2].split('')[0] table_results = [] for name, expected in data[a['id']]['tables'].items(): block = body.split('', 1)[1].split('', 1)[0] actual = [[c.strip() for c in line.strip().strip('|').split('|')] for line in block.splitlines() if line.startswith('|')] assert actual[0] == expected['headers'], (a['id'], name, '表头不符') assert actual[2:] == expected['rows'], (a['id'], name, '单元格不符') table_results.append({'name': name, 'rows': len(actual) - 2, 'cells': sum(map(len, actual[2:])), 'passed': True}) for claim in data[a['id']]['narrative_claims']: assert claim in body, (a['id'], '关键叙述需重核', claim) allowed = {D(0), D(1), D(12)} # 零边界、月序起点、年率除12的周期约定。 def amounts(v): if isinstance(v, dict): for k, value in v.items(): if k not in ('narrative_claims', 'article_verification'): amounts(value) elif isinstance(v, list): for value in v: amounts(value) elif isinstance(v, (D, int)) and not isinstance(v, bool): allowed.add(abs(D(v))) elif isinstance(v, str) and re.fullmatch(r'-?\d+(\.\d+)?%?', v): allowed.add(abs(D(v.rstrip('%')))) amounts(data[a['id']]) prose = re.sub(r'', '', body, flags=re.S) literals = sorted(set(re.findall(r'-?\d+(?:\.\d+)?%?', prose))) for literal in literals: assert abs(D(literal.rstrip('%'))) in allowed, (a['id'], '正文数字未找到独立模型值', literal) chinese = sorted(set(re.findall(r'(?:第)?[一二三四五六七八九十百千万零两]+(?:个月|月|年|元|期|次)', prose))) result[a['id']] = {'tables': table_results, 'narrative_claims_checked': len(data[a['id']]['narrative_claims']), 'prose_arabic_literals': literals, 'chinese_mentions_for_author_semantic_review': chinese, 'core_cjk': len(re.findall('[\u4e00-\u9fff]', body)), 'passed': True} return result def serial(v): if isinstance(v, D): return fmt(v) raise TypeError(type(v).__name__) if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--check-articles', action='store_true') args = parser.parse_args() data = build() if args.check_articles: checked = article_check(data) (HERE / 'r07-article-verification.json').write_text(json.dumps(checked, ensure_ascii=False, indent=2) + '\n', 'utf-8') (HERE / 'calc_r07.json').write_text(json.dumps(data, ensure_ascii=False, indent=2, default=serial) + '\n', 'utf-8') print('R07:九篇独立模型守恒与范围检查通过' + (';全部正文表格逐格一致' if args.check_articles else ''))