"""Single-reference BLEU-4 and ROUGE-1 recall on three authored fixtures."""
from collections import Counter
from fractions import Fraction as F
import math,json
def grams(tokens,n):return Counter(tuple(tokens[i:i+n]) for i in range(len(tokens)-n+1))
def bp(c,r):
    if c<=0 or r<=0:raise ValueError("positive lengths required")
    return 1. if c>=r else math.exp(1-r/c)
def score(reference,candidate):
    r=reference.split();c=candidate.split()
    if len(c)<4 or not r:raise ValueError("this fixture requires candidate length >=4 and nonempty reference")
    ps=[]
    for n in range(1,5):
        cc,rr=grams(c,n),grams(r,n);ps.append(F(sum(min(v,rr[k]) for k,v in cc.items()),sum(cc.values())))
    penalty=bp(len(c),len(r));bleu=penalty*math.exp(sum(math.log(float(p)) for p in ps)/4) if all(ps) else 0.
    recall=F(sum((Counter(c)&Counter(r)).values()),len(r))
    return {"modified_precisions":[str(p) for p in ps],"BP":penalty,"BLEU4_unsmoothed":bleu,"ROUGE1_recall":str(recall)}
if __name__=="__main__":
    reference="امروز سفارش را لغو نکن"
    candidates=["امروز سفارش را لغو کن","امروز سفارش را کنسل نکن","امروز امروز امروز امروز امروز"]
    result=[score(reference,c) for c in candidates]
    assert result[0]['modified_precisions']==['4/5','3/4','2/3','1/2']
    assert math.isclose(result[0]['BLEU4_unsmoothed'],.2**.25,abs_tol=1e-14)
    assert result[1]['modified_precisions']==['4/5','1/2','1/3','0'] and result[1]['BLEU4_unsmoothed']==0
    assert result[0]['ROUGE1_recall']==result[1]['ROUGE1_recall']=='4/5'
    assert result[2]['modified_precisions'][0]=='1/5' and result[2]['ROUGE1_recall']=='1/5'
    assert math.isclose(bp(2,5),math.exp(-1.5),abs_tol=1e-14)
    print(json.dumps({"results":result,"separate_BP_example_c2_r5":bp(2,5),"actual_translation_or_summary_model":False},ensure_ascii=False,indent=2))
