Advertisement
Kaelygon

Better divisibility rule finder

Nov 23rd, 2024 (edited)
272
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 11.59 KB | None | 0 0
  1. import math
  2. import sys
  3. from tabulate import tabulate
  4. from dataclasses import dataclass
  5.  
  6. # This terrible python code is provided by Kaelygon
  7. #
  8. # Find divisibility rule by prime P: P * N = a * 10^b + c
  9. # such that an integer can be split in two parts A and B
  10. # and reduced to a smaller number by simulating 10^b division and modulus
  11. # using string manipulation and addition: B*a-A*c
  12. #
  13. # This doesn't always hold for composites if a and c share factors
  14. #
  15. # Matt Parker explanation https://youtu.be/6pLz8wEQYkA
  16.  
  17. # Define the custom countDigits function
  18. def countDigits(n): return int(math.log10(abs(n))) + 1
  19.  
  20. def intDiv(a,b): return (a+b//2)//b #Rounded integer division
  21.  
  22. #Find divisibility rules using the fact P*N = a*10^b+c
  23. def findDivRulesByInverse(P, aRange, bRange, cRange, recurseDepth=0, maxRecursion=2):
  24.     rules = []
  25.     recurseOffset=0
  26.     if recurseDepth: # nudge N for reattempts, +1, -1, +2, -2, +3...
  27.         recurseOffset = recurseDepth+1
  28.         recurseOffset = -recurseOffset//2 if recurseOffset%2==0 else recurseOffset//2
  29.    
  30.  
  31.     for b in range(bRange[0], bRange[1]):
  32.         bPowTen=10**b # 10^b
  33.         for a in range(1,aRange):
  34.             # Find nearest multiples by reciprocal*10^b
  35.             abPowTen = a*bPowTen # a*10^b
  36.             N = intDiv(abPowTen,P)
  37.             if recurseDepth: #recursion offset
  38.                 N = max(N+recurseOffset,1)
  39.            
  40.             PN = P*N
  41.             c  = PN-abPowTen # c = P*N - a*10^b
  42.            
  43.             if ( # Verify conditions
  44.                 ( a==0 or c==0 ) or # a or c is zero
  45.                 ( (c<cRange[0] or c>cRange[1]) and (not recurseDepth) ) or  # c outside range
  46.                 ( math.gcd(a, c)>1 ) # a and c have common factor
  47.             ): continue
  48.            
  49.             rules.append((P, N, a, b, c))
  50.    
  51.     # If no rules, reattempt. This rarely happens with default values
  52.     # recurseDepth>0 activates recurseOffset and ignores cRange
  53.     if not rules and recurseDepth<maxRecursion:
  54.         recurseDepth+=1
  55.         rules = findDivRulesByInverse(P, aRange, bRange, cRange, recurseDepth)
  56.     return rules
  57.  
  58. #Output formatting
  59. def printRuleTables(rules):
  60.     headers = ["P", "N", "a", "b", "c", "P*N"]
  61.     table = [[P, N, a, b, c, P * N] for P, N, a, b, c in rules]
  62.     print(tabulate(table, headers=headers, tablefmt="plain"))
  63.  
  64. def printDivIters(iterTable):
  65.     def getEquation(A, B, a, c):
  66.         BSign = ""  if B*a  >=0 else "-"
  67.         ASign = "+" if A*-c >=0 else "-"
  68.         BPart=f"{abs(B)}*{abs(a)}"
  69.         APart=f"{abs(A)}*{abs(c)}"
  70.         return f"{BSign}{BPart}{ASign}{APart}"
  71.     headers = ["A|B", "B*a-A*c", "Intermed"]
  72.     table = [[f"{A}|{B}", getEquation(A, B, a, c), iterNum] for A, B, iterNum, a, c in iterTable]
  73.     print(tabulate(table, headers=headers, tablefmt="plain"))
  74.  
  75. # Test function that reduces a number by using the given rules
  76. def divByRule(rules,num):
  77.     P,N,a,b,c=rules
  78.     print("Example:")
  79.     print(f"Using rules P={P} a={a} b={b} c={c}")
  80.     print(f"Testing {num} divisibility by {P}\n")
  81.    
  82.     iterNum=num
  83.     iterHistory=[num]
  84.     iterTable=[]
  85.     maxIters=100 #Very unlikely to reach +100 loop
  86.     iterCount=0
  87.     iterSmallest=num #best valid iteration
  88.    
  89.     while(iterCount<maxIters):
  90.         iterCount+=1
  91.         #Simulate splitting number at b:th digit
  92.         powTen=10**b
  93.         A=iterNum//powTen
  94.         B=iterNum%powTen
  95.         iterNum=B*a-A*c
  96.         if abs(iterNum)<abs(iterSmallest): iterSmallest=iterNum
  97.         #Keep history to check whether the iteration is in a loop
  98.         if iterNum in iterHistory: break
  99.         iterHistory.append(iterNum)
  100.         iterTable.append([A,B,iterNum,a,c])
  101.         if A==0 or B==0: break #Break if invalid A or B
  102.    
  103.     printDivIters(iterTable)
  104.     print(f"\nSmallest iteration {iterSmallest} = {P}*{iterSmallest//P}")
  105.    
  106.     falsePositive=0
  107.     floorIter=(iterSmallest//P)*P
  108.     if floorIter != iterSmallest:
  109.         print(f"Not divisible by {P}")
  110.         print(f"Remainder of {num}/{P} is {num%P}")
  111.         if num%P==0: falsePositive=1
  112.     else:
  113.         print(f"{num} is divisible by {P}")
  114.         if num%P!=0: falsePositive=1
  115.    
  116.     if falsePositive:
  117.         print("Something's not right")
  118.         print("Bad rule perhaps, P*N=a*10^b+c ")
  119.         print("doesn't always hold for highly ")
  120.         print("composite numbers ")
  121.  
  122.     return iterNum
  123.    
  124. #Simple rotate right LCG for deterministic results
  125. def rorLCG(seed):
  126.     modulus=0xFFFFFFFFFFFFFFFF #2^64-1
  127.     bits=modulus.bit_length()
  128.     shift=int((bits+1)/3)
  129.     randNum=(seed>>shift)|((seed<<(bits-shift))&modulus) #RORR
  130.     randNum=(randNum*3343+11770513)&modulus #LCG
  131.     return randNum
  132.  
  133. def randDivRuleTest(rules,isDivisible):
  134.     minSize=10**(rules[3]+1)+17 # at least 10^b
  135.     maxSize=10**(2*rules[3])+minSize
  136.     P=rules[0]
  137.     randPN=rorLCG(P)
  138.     while(randPN<maxSize):
  139.         randPN=randPN<<64|rorLCG(randPN) #cocat random values till maxSize
  140.     randPN=randPN%maxSize+minSize
  141.     if isDivisible:
  142.         mult=(randPN%maxSize+minSize )//P
  143.         randPN=P*mult # random product of P
  144.     else:
  145.         while abs(P)>1: # 1 would always be divisible by itself
  146.             if randPN%P!=0: break # ensure the test number is not product of P
  147.             randPN+=rorLCG(randPN)%(maxSize)+minSize
  148.    
  149.     num=divByRule(rules,randPN)
  150.  
  151. def parkerPrinting(rules):
  152.     def ordinal(num):
  153.         if 10 <= num % 100 <= 20:  # special cases 11th, 12th...
  154.             return f"{num}th"
  155.         return f"{num}{['th', 'st', 'nd', 'rd', 'th'][min(num % 10, 4)]}"
  156.     P,N,a,b,c=rules
  157.     bStr = ordinal(b)
  158.    
  159.     APart="A"
  160.     BPart="B"
  161.     print(f"{P} has following divisibility rule using B*a-A*c")
  162.     print(f"Split the tested number into {APart} and {BPart} after {bStr} digit.")
  163.    
  164.     aStr = f"{abs(a)}" if abs(a) != 1 else ""
  165.     cStr = f"{abs(c)}" if abs(c) != 1 else ""
  166.    
  167.     if cStr:
  168.         cStr=f"{cStr}" # add only non 1 mult strings
  169.         print(f"Multiply {APart} by {cStr} ",end="") # has c mult
  170.    
  171.     if aStr and cStr:
  172.         print("and multiply ",end="") # has c and a mult
  173.     elif aStr:
  174.         print("Multiply ",end="") # has only a mult
  175.    
  176.     if aStr:
  177.         print(f"{BPart} by {aStr}",end="")
  178.    
  179.     if aStr or cStr: #Don't print new line if both multipliers are 1
  180.         print("",end="\n")
  181.  
  182.     #B*a-A*c sign
  183.     BNum,ANum=a,-c
  184.    
  185.     if (ANum<0 and BNum<0): # if both are negative, make them positive
  186.         ANum,BNum = abs(ANum),abs(BNum)
  187.    
  188.     esign=""
  189.     if ANum>=0 and BNum>=0: #if added
  190.         esign="+"
  191.         print(f"Add {APart} and {BPart} together",end="")
  192.     else: #if subtracted
  193.         esign="-"
  194.         if ANum>0:
  195.             addend,subbed= (APart,BPart) # A minus B
  196.         else:
  197.             addend,subbed= (BPart,APart) # B minus A
  198.         print(f"Subtract {subbed} from {addend}",end="")
  199.    
  200.     # add non 1 mult strings
  201.     APart+= f"*{cStr}" if cStr else ""
  202.     BPart+= f"*{aStr}" if aStr else ""
  203.    
  204.     if ANum>0: # first part is positive, it goes first
  205.         equation=f"{APart}{esign}{BPart}"
  206.     else:
  207.         equation=f"{BPart}{esign}{APart}"
  208.        
  209.     print(f" = {equation}")
  210.  
  211.  
  212. def main():
  213.     @dataclass
  214.     class Config:
  215.         P: (int,int)
  216.         PDefault: (int,int)
  217.         useExample: bool
  218.         isDivisible: bool
  219.         parkerStyle: bool
  220.         ruleCount: int
  221.         ruleIndex: int
  222.    
  223.     #Configuration
  224.     cfg = Config(
  225.         P           = [None, None],
  226.         PDefault    = [313 , None],   # [start,end) Any non-zero number which divisibility rule is
  227.         useExample  = True,  # Verify rule by iterating B*a-A*c
  228.         isDivisible = True,  # If tested example divided num is products of P
  229.         parkerStyle = True,  # Parker style printing
  230.         ruleCount   = 10,    # Limit shown rule count
  231.         ruleIndex   = 0      # Index of used rule
  232.     )
  233.     if (not cfg.PDefault[1]): cfg.PDefault[1]=cfg.PDefault[0]+1
  234.    
  235.     # print help
  236.     print("")
  237.     if len(sys.argv) > 1:
  238.         arg = str(sys.argv[1])
  239.         if arg == "-h" or arg == "-help":
  240.             spacerCount=len(str(f"python {sys.argv[0]} "))-1
  241.             spacer=" "*spacerCount
  242.             print(f"python {sys.argv[0]} [Integer or \"(Start,End)\"] [Show example? (0,1)] ")
  243.             print(spacer,f"[Example is divisible? (0,1)] [Parker style? (0,1)] [Rule count] [Rule index]")
  244.             print(f"Default command : python {sys.argv[0]} {cfg.PDefault[0]} {cfg.useExample} {cfg.isDivisible} {cfg.parkerStyle} {cfg.ruleCount} {cfg.ruleIndex}")
  245.             print(f"Range example   : python {sys.argv[0]} \"[1,11]\" 0 0 1 2 0\n")
  246.             return 0
  247.    
  248.     # Argument parsing
  249.     # Detect if input is a single number or range tuple
  250.     if len(sys.argv) > 1:
  251.         arg1 = eval(sys.argv[1])
  252.         try:
  253.             if len(arg1)>1:
  254.                 cfg.P=arg1
  255.         except:
  256.             cfg.P=[arg1,arg1+1]
  257.            
  258.     # set unset values
  259.     if (not cfg.P[0]): cfg.P[0]=cfg.PDefault[0]
  260.     if (not cfg.P[1]): cfg.P[1]=cfg.P[0]+1
  261.        
  262.     if (cfg.P[1]-cfg.P[0]<=0): #invalid range
  263.         cfg.P=cfg.PDefault
  264.  
  265.     cfg.useExample  = eval(sys.argv[2]) if len(sys.argv) > 2 else cfg.useExample      
  266.     cfg.isDivisible = eval(sys.argv[3]) if len(sys.argv) > 3 else cfg.isDivisible  
  267.     cfg.parkerStyle = eval(sys.argv[4]) if len(sys.argv) > 4 else cfg.parkerStyle  
  268.     cfg.ruleCount   = eval(sys.argv[5]) if len(sys.argv) > 5 else cfg.ruleCount    
  269.     cfg.ruleIndex   = eval(sys.argv[6]) if len(sys.argv) > 6 else cfg.ruleIndex    
  270.    
  271.     for P in range(cfg.P[0],cfg.P[1]):
  272.         if P==0: continue
  273.         # 'a' and 'c' are multipliers in B*a-A*c, the smaller they are
  274.         # the easier they will be to calculate by hand
  275.         # 'b' determines where the tested number split right to left,
  276.         #
  277.         # Chosen a,b,c reasoning:
  278.         # checking a>9 is redundant, these values will be checked by next b (a*10^b).
  279.         # b 1-3x the magnitude of (small) P are doable by hand. 'b' sets the split 'B' magnitude.
  280.         # increase c when numbers grows as it becomes harder to find divisibility rules.
  281.         # if P has factors 2 or 5
  282.         #
  283.         # Compute time is limited by the number of digits in P due to division,
  284.         # In terms of P, O( log(P)^1.466 * log(log(P)) * bRange^aRange )
  285.         #
  286.         digitsP=countDigits(P)
  287.         bBase = digitsP # search b around P digit count
  288.         bOffset = 10
  289.         cBase = abs(P)//20+10
  290.         aRange = 10
  291.         bRange = [ bBase-bOffset, bBase+bOffset ]
  292.         cRange = [ -cBase, cBase ]
  293.         #limits
  294.         bRange[0] = max(bRange[0],digitsP)
  295.         bRange[1] = min(bRange[1],2*bBase)
  296.        
  297.         #Computation starts
  298.         rules = findDivRulesByInverse(P, aRange, bRange, cRange)
  299.         rules.sort(key=lambda x: (x[3], x[2]+abs(x[4]))) # sort by b, then a+c
  300.  
  301.         #Print
  302.         if rules:
  303.             rulesLen=len(rules)
  304.             cfg.ruleIndex=max(0,min(cfg.ruleIndex,rulesLen-1))
  305.            
  306.             ruleWord = "rules" if rulesLen!=1 else "rule"
  307.             print(f"Found {rulesLen} {ruleWord} for {P}, showing first {cfg.ruleCount}:")
  308.             printRuleTables(rules[:cfg.ruleCount])  # Display first 10 rules
  309.             print("")
  310.             if cfg.parkerStyle:
  311.                 parkerPrinting(rules[cfg.ruleIndex])
  312.                 print("")
  313.             if cfg.useExample: #random test
  314.                 randDivRuleTest(rules[cfg.ruleIndex],cfg.isDivisible)
  315.         else:
  316.             print(f"No rules found for {P}",end="")
  317.        
  318.         print("")
  319.  
  320. if __name__=="__main__":
  321.     main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement