"""R03构造教学例题;仅用标准库,金额内部以整分运算。""" import json from pathlib import Path def run(opening, floor, incomes, expenses, labels=None, extra=None): """收入、支出均为月度实收实付;追加资金单列,不推断借款资格。""" assert len(incomes) == len(expenses) labels = labels or [f"第{i+1}月" for i in range(len(incomes))] extra = extra or [0] * len(incomes) assert len(extra) == len(incomes) balance = int(round(opening * 100)) rows = [] for month, income, expense, injection in zip(labels, incomes, expenses, extra): start = balance change = int(round((income + injection - expense) * 100)) balance += change rows.append(dict(month=month, opening=start/100, income=income, expense=expense, injection=injection, net=change/100, closing=balance/100, below_floor=max(0, floor-balance/100))) assert balance == round((opening+sum(incomes)+sum(extra)-sum(expenses))*100) minimum = min(r['closing'] for r in rows) return dict(input=dict(start_month='2026-11', unit='CNY', opening=opening, floor=floor, incomes=incomes, expenses=expenses, injections=extra, months=labels), rows=rows, summary=dict(total_income=sum(incomes), total_expense=sum(expenses), total_injection=sum(extra), final=balance/100, minimum_month_end=minimum, minimum_including_opening=min(opening, minimum), first_below_floor=next((r['month'] for r in rows if r['closing']=0),None), first_at_floor_month=next((r['month'] for r in rows if r['closing']>=floor),None), required_opening_for_floor=floor-min(0,minimum-opening))) def event_run(opening, floor, events): balance=opening rows=[] for day,income,expense in events: balance+=income-expense rows.append(dict(day=day,income=income,expense=expense,closing=balance)) minimum=min([opening]+[r['closing'] for r in rows]) assert opening+sum(e[1]-e[2] for e in events)==balance return dict(opening=opening,floor=floor,events=rows,minimum=minimum, extra_for_floor=max(0,floor-minimum),final=balance) def build(): d={} d['DJ2-019']={'partial_recovery':run(24000,6000,[4000]*4+[6500]*4,[8000]*8), 'no_recovery':run(24000,6000,[4000]*8,[8000]*8), 'zero_income':run(24000,6000,[0]*8,[8000]*8), 'normal':run(24000,6000,[8000]*8,[8000]*8)} a=[0,0,0,7000,7000,7000,7000,7000] b=[5000,0,0,0,0,5000,5000,5000] d['DJ2-020']={'person_a':a,'person_b':b, 'staggered':run(30000,8000,[x+y for x,y in zip(a,b)],[9000]*8), 'b_late':run(30000,8000,[5000,0,0,7000,7000,7000,7000,12000],[9000]*8), 'normal':run(30000,8000,[12000]*8,[9000]*8)} d['DJ2-021']={'locked':run(8000,3000,[1000,1000,1000,19180,1000,1000],[5000]*6), 'earlier_release':run(8000,3000,[1000,19180,1000,1000,1000,1000],[5000]*6), 'incorrect_upfront':run(26180,3000,[1000]*6,[5000]*6), 'deposit':{'principal':18000,'contract_input_interest':180,'release_month':4}, 'locked_claim_end':[18180,18180,18180,0,0,0]} inc=[0,0,0,8000,8000,8000] exp=[4000,5200,9000,4800,4000,4000] d['DJ2-022']={'with_costs':run(18000,4000,inc,exp), 'without_costs':run(18000,4000,inc,[4000]*6), 'pay_late':run(18000,4000,[0,0,0,0,8000,8000],exp), 'recovery_costs':[{'month':2,'amount':1200,'purpose':'求职差旅'}, {'month':3,'amount':5000,'purpose':'搬家及新住所前置款'}, {'month':4,'amount':800,'purpose':'必要通勤设备'}]} inc=[2000]*4+[6000]*2 d['DJ2-023']={'delayed':run(15000,3000,inc,[6000,6000,4500,4500,4500,4500]), 'immediate':run(15000,3000,inc,[4500]*6), 'one_more_month':run(15000,3000,inc,[6000,6000,6000,4500,4500,4500])} exp=[14500]+[6500]*5 d['DJ2-024']={'on_time':run(12000,5000,[6000,14000,6000,6000,6000,6000],exp), 'delay_two_months':run(12000,5000,[6000,6000,6000,14000,6000,6000],exp), 'no_reimbursement':run(12000,5000,[6000]*6,exp), 'advance':8000,'planned_receipt_month':2,'delayed_receipt_month':4} d['DJ2-025']={'confirmed_only':run(12000,4000,[3000]*6,[5000]*6), 'conditional':run(12000,4000,[3000,3000,9000,3000,6000,3000],[5000]*6), 'bonus_late_no_subsidy':run(12000,4000,[3000,3000,3000,3000,3000,9000],[5000]*6), 'conditional_inputs':[{'kind':'构造奖金','amount':6000,'month':3}, {'kind':'构造补贴','amount':3000,'month':5}]} d['DJ2-026']={'late_topup':run(-2500,2000,[4500]*4,[5000,4000,4000,4000],extra=[0,4000,0,0]), 'no_topup':run(-2500,2000,[4500]*4,[5000,4000,4000,4000]), 'upfront_topup':run(2500,2000,[4500]*4,[5000,4000,4000,4000]), 'upfront_injection':5000} discrete=[4000,16000]+[4000]*10 d['DJ2-027']={'concentrated':run(18000,6000,[5000]*12,discrete), 'averaged':run(18000,6000,[5000]*12,[5000]*12), 'first_month_due':run(18000,6000,[5000]*12,[16000]+[4000]*11), 'lower_opening':run(14000,6000,[5000]*12,discrete), 'month_2_intramonth':event_run(19000,6000,[(3,0,12000),(5,0,4000),(25,5000,0)])} # 独立的封闭式累计值及边界断言,避免只验证循环自身。 assert d['DJ2-019']['partial_recovery']['summary']['final']==2000 assert d['DJ2-020']['staggered']['summary']['minimum_month_end']==4000 assert d['DJ2-021']['locked']['summary']['first_negative_month']=='第3月' assert d['DJ2-022']['with_costs']['summary']['upfront_topup_for_floor']==4200 assert d['DJ2-023']['delayed']['summary']['minimum_month_end']==2000 assert d['DJ2-023']['one_more_month']['summary']['minimum_month_end']==500 assert d['DJ2-024']['delay_two_months']['summary']['minimum_month_end']==2500 assert d['DJ2-025']['confirmed_only']['summary']['final']==0 assert d['DJ2-026']['late_topup']['rows'][2]['closing']==2000 assert sum(discrete)==12*5000 assert d['DJ2-027']['concentrated']['summary']['final']==18000 assert d['DJ2-027']['concentrated']['summary']['required_opening_for_floor']==16000 assert d['DJ2-027']['first_month_due']['summary']['required_opening_for_floor']==17000 assert d['DJ2-027']['month_2_intramonth']['extra_for_floor']==3000 for item in d.values(): for scenario in item.values(): if isinstance(scenario,dict) and 'rows' in scenario: for row in scenario['rows']: assert row['closing']==row['opening']+row['income']+row['injection']-row['expense'] return d if __name__=='__main__': output=Path(__file__).with_suffix('.json') output.write_text(json.dumps(build(),ensure_ascii=False,indent=2)+'\n',encoding='utf-8') print(f'已复算9篇构造例题:{output}')