계산기마다 숫자를 받더라도 서로 같은 종류의 숫자를 받는 것은 아니다. 나는 실제 도구에서 확인한 시간·거리·주기·합의 방식의 차이를 독립 합성 함수로 정리했다. 비교 결과에 어떤 전제가 필요한지 전체 구현과 테스트를 함께 공개한다.

이 모듈은 Python 표준 라이브러리로 계산하며 실제 도로·유가·차량·장부·지급 요청에 연결되지 않는다. 페이지의 선택기는 로컬 실행에서 저장한 네 결과를 읽는다. 브라우저에서 Python이나 서버가 실행되는 예제는 아니다.

네 함수가 서로 다른 기준을 받는다

compare는 두 계획의 거리, 연료 비용과 명시된 다른 비용, 운전·대기·작업·휴게 시간을 함께 읽는다. reserve는 교체비를 주기로 나눈 분수와 월 예산의 거리 몫을 합한다. measured_fuel은 같은 구간이라는 호출자의 확인을 요구하고 전체 연료와 비주행 몫을 구분한다. waiting_quote는 관측 시간과 선언한 요금 방식으로 요청 후보를 만든다.

함수의 숫자 한도는 작은 실험의 범위다. 현행 산업 표준과 실제 업체의 운영 한도를 가져온 것이 아니다. 추가 입력이나 다른 단위가 필요하면 이름만 바꾸기 전에 어떤 기준이 유지되는지 다시 정해야 한다. 무엇을 비교하고 어떤 근거를 호출자가 제공하는지 읽을 수 있게 만든 계약이며 전체 운송 관리 시스템은 아니다.

정확한 계산과 입력 사실을 구별한다

내부 비율에는 Fraction을 사용한다. 정비 항목의 작은 분수를 먼저 정수로 바꾸지 않고 합산 후 목표 거리를 곱해 반올림한다. 시간당 값도 표시와 비교를 나누어 정확한 비율로 순위를 정한다. 정수와 제한된 소수 문자열만 받고 참·거짓이나 부동소수점 입력을 거절해 입력 해석의 혼동을 줄였다.

그렇다고 실제 측정이 정확하다는 보장은 아니다. 주유 기준과 비용, 계약 단가가 맞는지는 호출자가 확인해야 한다. 같은 기준 확인이 참이라는 값도 인증된 외부 관측 결과가 아니라 함수 입력이다. 이 차이를 숨기면 소수점이 정교할수록 입력 사실에 대한 과신이 커질 수 있다. 계산의 일관성과 자료 출처의 검토를 다른 책임으로 설명한다.

모르는 일정과 다른 합의를 덮지 않는다

일정의 도착과 마감은 함께 있거나 함께 없어야 한다. 한쪽 계획의 일정이 없으면 두 계획의 여유시간 차이도 알 수 없음으로 남긴다. 날짜를 추측하지 않고 자정 경계와 존재하는 날짜를 확인한다. 단일 민간 달력 기준의 작은 계산이며 세계 시간이나 교통 예측이 아니다. 알려진 0분과 모르는 값은 다른 결과다.

대기료의 비례 방식과 시작 단위 방식도 별도로 선언한다. 선택하지 않은 방식의 요금 필드가 남으면 거절하며 세금은 계산하지 않는다. 실제 계약 권리와 지급 여부를 결정하지 않고 proposal_only를 참으로 둔다. 부품 교체주기의 추천과 실제 차량의 안전 판단도 없다. 같은 함수 이름에서 운영 권한과 결과 확정까지 읽히지 않도록 제한을 코드에 남겼다.

코드와 구현 살펴보기

독립 합성 전체 구현

"""Synthetic unit/basis calculations, not market rates or legal/vehicle advice."""
from fractions import Fraction
from decimal import Decimal,InvalidOperation
from datetime import datetime
import re

def number(value,minimum=0,maximum=10**9,places=2):
    if type(value) not in (str,int):raise ValueError('explicit numeric text or integer required')
    text=str(value)
    if len(text)>32 or not re.fullmatch(r'(?:0|[1-9][0-9]*)(?:\.[0-9]{1,'+str(places)+r'})?' if places else r'(?:0|[1-9][0-9]*)',text):raise ValueError('number format')
    try:result=Fraction(Decimal(text))
    except InvalidOperation:raise ValueError('number') from None
    if not minimum<=result<=maximum:raise ValueError('number range')
    return result

def whole(value,minimum=0,maximum=10**9):return int(number(value,minimum,maximum,0))
def rounded(value):
    sign=-1 if value<0 else 1;value=abs(value)
    return sign*((2*value.numerator+value.denominator)//(2*value.denominator))
def ratio(value):return {'numerator':value.numerator,'denominator':value.denominator}
def exact_keys(data,keys):
    if not isinstance(data,dict) or set(data)!=set(keys):raise ValueError('known fields required')
def label(value):
    if not isinstance(value,str) or not re.fullmatch(r'[A-Za-z0-9_-]{1,32}',value):raise ValueError('synthetic label')
    return value
def civil(value):
    if not isinstance(value,str) or not re.fullmatch(r'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}',value):raise ValueError('civil date time')
    parsed=datetime.strptime(value,'%Y-%m-%dT%H:%M')
    if not 2000<=parsed.year<=2100:raise ValueError('year')
    return parsed

def plan(data):
    exact_keys(data,('revenue','loaded_km','empty_km','fuel_economy','fuel_price','toll','fee','other_cost','fixed_allocation','drive_minutes','wait_minutes','handling_minutes','rest_minutes','arrival','deadline'))
    distance=number(data['loaded_km'],0,100000)+number(data['empty_km'],0,100000)
    if distance<=0:raise ValueError('positive total distance')
    economy=number(data['fuel_economy'],Fraction(1,100),100)
    fuel=rounded(distance*whole(data['fuel_price'],1)/economy)
    costs=fuel+sum(whole(data[k]) for k in ('toll','fee','other_cost','fixed_allocation'))
    revenue=whole(data['revenue']);minutes=sum(whole(data[k],0,10080) for k in ('drive_minutes','wait_minutes','handling_minutes','rest_minutes'))
    if not 1<=minutes<=10080:raise ValueError('positive bounded total minutes')
    arrival,deadline=data['arrival'],data['deadline'];buffer=None
    if arrival is not None or deadline is not None:
        if arrival is None or deadline is None:raise ValueError('paired civil schedule')
        buffer=int((civil(deadline)-civil(arrival)).total_seconds()/60)
    residual=revenue-costs
    return {'revenue':revenue,'fuel_cost':fuel,'total_cost':costs,'residual':residual,'minutes':minutes,'hourly_residual':rounded(Fraction(residual*60,minutes)),'hourly_exact':ratio(Fraction(residual*60,minutes)),'schedule_buffer_minutes':buffer,'basis':'declared_inputs_only'}

def compare(a,b):
    a,b=plan(a),plan(b);ar,br=(Fraction(x['hourly_exact']['numerator'],x['hourly_exact']['denominator']) for x in (a,b))
    return {'a':a,'b':b,'residual_difference':a['residual']-b['residual'],'hourly_winner':'tie' if ar==br else ('a' if ar>br else 'b'),'schedule_buffer_difference':None if a['schedule_buffer_minutes'] is None or b['schedule_buffer_minutes'] is None else a['schedule_buffer_minutes']-b['schedule_buffer_minutes']}

def reserve(items,monthly_budget=None,monthly_distance=None,target_distance=1,basis='paid'):
    if not isinstance(items,list) or len(items)>12 or basis not in ('paid','supply'):raise ValueError('reserve contract')
    total=Fraction(0);seen=set()
    for item in items:
        exact_keys(item,('id','replacement_cost','interval_km'));key=label(item['id'])
        if key in seen:raise ValueError('duplicate reserve item')
        seen.add(key);total+=Fraction(whole(item['replacement_cost'],1),whole(item['interval_km'],1,10**7))
    if (monthly_budget is None)!=(monthly_distance is None):raise ValueError('paired monthly basis')
    periodic=total
    if monthly_budget is not None:total+=Fraction(whole(monthly_budget),whole(monthly_distance,1,10**7))
    if total<=0:raise ValueError('nonzero reserve basis')
    target=whole(target_distance,1,10**7)
    return {'periodic_exact':ratio(periodic),'total_per_km_exact':ratio(total),'target_reserve':rounded(total*target),'target_distance':target,'cost_basis':basis,'recommendation':False}

def measured_fuel(data):
    exact_keys(data,('start_km','end_km','liters','gross_cost','confirmed_deductions','non_driving_liters','same_fill_basis'))
    if data['same_fill_basis'] is not True:raise ValueError('measurement basis unconfirmed')
    distance=number(data['end_km'])-number(data['start_km']);liters=number(data['liters'],Fraction(1,100),100000);idle=number(data['non_driving_liters'],0,100000)
    gross=whole(data['gross_cost'],1);deductions=whole(data['confirmed_deductions'])
    if distance<=0 or idle>=liters or deductions>gross:raise ValueError('inconsistent measurement')
    return {'distance_exact':ratio(distance),'total_economy_exact':ratio(distance/liters),'adjusted_driving_economy_exact':ratio(distance/(liters-idle)) if idle else None,'net_cost':gross-deductions,'net_per_km':rounded(Fraction(gross-deductions)/distance),'basis_confirmed_by_caller':True,'vehicle_diagnosis':False}

def waiting_quote(data):
    keys=('start','end','excluded_minutes','free_minutes','mode','hourly_rate','unit_minutes','unit_rate','works','expenses');exact_keys(data,keys)
    elapsed=int((civil(data['end'])-civil(data['start'])).total_seconds()/60)
    excluded=whole(data['excluded_minutes'],0,10080);free=whole(data['free_minutes'],0,10080)
    if not 1<=elapsed<=10080 or excluded>elapsed:raise ValueError('time interval')
    billable=max(0,elapsed-excluded-free)
    if data['mode']=='proportional':
        if data['unit_minutes'] is not None or data['unit_rate'] is not None:raise ValueError('inactive rate fields')
        waiting=rounded(Fraction(billable*whole(data['hourly_rate']),60));units=None
    elif data['mode']=='started_unit':
        if data['hourly_rate'] is not None:raise ValueError('inactive rate fields')
        unit=whole(data['unit_minutes'],1,1440);units=(billable+unit-1)//unit;waiting=units*whole(data['unit_rate'])
    else:raise ValueError('declared agreement mode')
    extra=0
    for key in ('works','expenses'):
        rows=data[key]
        if not isinstance(rows,list) or len(rows)>5:raise ValueError('row bound')
        seen=set()
        for row in rows:
            exact_keys(row,('id','quantity','unit_price'));identity=label(row['id'])
            if identity in seen:raise ValueError('duplicate quote row')
            seen.add(identity);extra+=whole(row['quantity'],1,10000)*whole(row['unit_price'])
    return {'elapsed_minutes':elapsed,'observed_minutes':elapsed-excluded,'billable_minutes':billable,'billed_units':units,'waiting_amount':waiting,'extras':extra,'proposed_amount':waiting+extra,'proposal_only':True,'includes_tax':False,'entitlement_decided':False}

def synthetic_plan(**changes):
    return {**dict(revenue=1000,loaded_km=100,empty_km=0,fuel_economy=10,fuel_price=10,toll=0,fee=0,other_cost=0,fixed_allocation=0,drive_minutes=120,wait_minutes=0,handling_minutes=0,rest_minutes=0,arrival=None,deadline=None),**changes}
def synthetic_waiting(**changes):
    return {**dict(start='2026-10-21T09:00',end='2026-10-21T11:30',excluded_minutes=20,free_minutes=60,mode='proportional',hourly_rate=36,unit_minutes=None,unit_rate=None,works=[],expenses=[]),**changes}

실제 로컬에서 확인한 경계

소스와 새로 푼 ZIP에서 24개 테스트를 실행했다. 금액 순위와 시간 순위의 역전, 공차 비용, 대기·휴게 시간, 일정 누락과 자정, 정확한 분수 합, 예산 쌍, 측정 구간과 차감액, 요금 방식과 추가 행을 확인했다. 입력 객체를 바꾸지 않는 검사도 있다. 아래 결과는 그때 실제 실행한 고정 예제이며 사용자의 운행 기록을 받지 않는다.

별도 운영 프로그램이나 고객의 브라우저를 실행한 것은 아니다. 현재 시장 단가와 실제 차량 기록을 대조한 검증도 아니다. 실제 화면으로 연결하면 입력 문구, 단위 안내, 오류 위치와 결과 표현을 추가로 확인해야 한다. 기능 코드가 계산을 거절한다는 사실과 사람이 오류를 이해하는 화면을 검수했다는 사실은 구분해 둔다.

저장된 실행 기록을 읽는 뷰어입니다. Python이나 서버를 브라우저에서 실행하지 않습니다.

다운로드 이후 연결할 책임

ZIP에는 전체 구현, 테스트, 실행기와 저장된 기록, 사용 안내가 들어 있다. 수정과 재사용이 가능한 독립 합성 코드다. 실제 배차표와 원래 영수증, 업체 연락처는 없다. 결과는 메모리에서 계산하며 계정, 지속 저장과 전송도 구현하지 않았다. 새로운 현장 데이터 연결은 이 예제를 내려받는 것과 별개의 작업이다.

적용할 때는 입력의 측정 구간과 비용 범위를 먼저 합의하고 어떤 결과 이름이 과장되지 않는지 살펴야 한다. 원래 도구와 새 예제의 범위도 같지 않다. 특히 새 측정 예제는 확인된 비주행 리터만 받고 요금 예제에는 세금이 없다. 나는 모든 기능을 옮기기보다 판단에 필요한 작은 계약과 실제 확인한 근거를 재사용할 수 있게 남겼다.

구현·테스트·실행 예제 ZIP 받기
독립 합성 코드, 수정·재사용 가능