"""Authored BM25-style one-term score; exact token lengths, no Lucene runtime."""
import math,json
from collections import Counter
DOCS={'A':'تماس پیگیری مشتری','B':'تماس تماس تماس پرداخت سفارش زمان امروز','C':'سفارش زمان','D':'تماس پرداخت پرونده'}
def score(tf,dl,avgdl,n,df,k1=1.2,b=.75):
    if any(not math.isfinite(x) for x in [tf,dl,avgdl,n,df,k1,b]):raise ValueError('finite inputs')
    if not(0<=tf<=dl and dl>0 and avgdl>0 and n>=1 and 0<=df<=n and k1>0 and 0<=b<=1):raise ValueError('invalid BM25 exercise domain')
    idf=math.log(1+(n-df+.5)/(df+.5))
    return idf*tf/(tf+k1*(1-b+b*dl/avgdl))
def demo():
    counts={k:Counter(v.split()) for k,v in DOCS.items()};n=len(counts);avg=sum(sum(c.values()) for c in counts.values())/n;df=sum('تماس' in c for c in counts.values())
    rows=[{'id':k,'length':sum(c.values()),'tf':c['تماس'],'score':score(c['تماس'],sum(c.values()),avg,n,df)} for k,c in counts.items()]
    assert avg==3.75 and df==3 and rows[2]['score']==0
    normalized=[score(1,dl,avg,n,df) for dl in [3,12]]
    without=[score(1,dl,avg,n,df,b=0) for dl in [3,12]]
    assert normalized[0]>normalized[1] and without[0]==without[1]
    saturation=[score(tf,7,avg,n,df) for tf in [1,2,4]]
    assert saturation[2]>saturation[1]>saturation[0] and saturation[2]<4*saturation[0]
    # Same fixed field statistics, all lengths and frequencies in this bounded test.
    monotone=0
    for dl in range(1,21):
        previous=-1
        for tf in range(0,dl+1):
            s=score(tf,dl,avg,n,df);assert s>=previous;previous=s;monotone+=1
    return {'documents':DOCS,'term':'تماس','N':n,'df':df,'average_token_length':avg,'idf':math.log(10/7),'k1':1.2,'b':.75,'rows':rows,'fixed_tf1_length3_12':normalized,'b0_length3_12':without,'tf1_2_4_fixed_length7':saturation,'frequency_monotonicity_checks':monotone,'Lucene_or_Elasticsearch_executed':False,'length_norm_quantization_modeled':False}
if __name__=='__main__':print(json.dumps(demo(),ensure_ascii=False,indent=2))
