100 doors

There are 100 doors in a row that are all initially closed.

You make 100 passes by the doors.

The first time through, visit every door and  ''toggle''  the door  (if the door is closed,  open it;   if it is open,  close it).

The second time, only visit every 2nd door   (door #2, #4, #6, ...),   and toggle it.

The third time, visit every 3rd door   (door #3, #6, #9, ...), etc,   until you only visit the 100th door.

;Task:
Answer the question:   what state are the doors in after the last pass?   Which are open, which are closed?

'''Alternate:'''
As noted in this page's   discussion page,   the only doors that remain open are those whose numbers are perfect squares.

Opening only those doors is an   optimization   that may also be expressed;
however, as should be obvious, this defeats the intent of comparing implementations across programming languages.

Solution:
doors = [False] * 100
for i in range(100):
   for j in range(i, 100, i+1):
       doors[j] = not doors[j]
   print("Door %d:" % (i+1), 'open' if doors[i] else 'close')


100 doors

Solution:
for i in range(1, 101):
    if i**0.5 % 1:
        state='closed'
    else:
        state='open'
    print("Door {}:{}".format(i, state))


100 prisoners

The Problem:
* 100 prisoners are individually numbered 1 to 100
* A room having a cupboard of 100 opaque drawers numbered 1 to 100, that cannot be seen from outside.
* Cards numbered 1 to 100 are placed randomly, one to a drawer, and the drawers all closed; at the start.
* Prisoners start outside the room
:* They can decide some strategy before any enter the room.
:* Prisoners enter the room one by one, can open a drawer, inspect the card number in the drawer, then close the drawer.
:* A prisoner can open no more than 50 drawers.
:* A prisoner tries to find his own number.
:* A prisoner finding his own number is then held apart from the others.
* If '''all''' 100 prisoners find their own numbers then they will all be pardoned. If ''any'' don't then ''all'' sentences stand.

;The task:
# Simulate several thousand instances of the game where the prisoners randomly open drawers
# Simulate several thousand instances of the game where the prisoners use the optimal strategy mentioned in the Wikipedia article, of:
:* First opening the drawer whose outside number is his prisoner number.
:* If the card within has his number then he succeeds otherwise he opens the drawer with the same number as that of the revealed card. (until he opens his maximum).

Show and compare the computed probabilities of success for the two strategies, here, on this page.

;References:
# The unbelievable solution to the 100 prisoner puzzle standupmaths (Video).
# [[wp:100 prisoners problem]]
# 100 Prisoners Escape Puzzle DataGenetics.
# Random permutation statistics#One hundred prisoners on Wikipedia.

Solution:
import random

def play_random(n):
    # using 0-99 instead of ranges 1-100
    pardoned = 0
    in_drawer = list(range(100))
    sampler = list(range(100))
    for _round in range(n):
        random.shuffle(in_drawer)
        found = False
        for prisoner in range(100):
            found = False
            for reveal in random.sample(sampler, 50):
                card = in_drawer[reveal]
                if card == prisoner:
                    found = True
                    break
            if not found:
                break
        if found:
            pardoned += 1
    return pardoned / n * 100   # %

def play_optimal(n):
    # using 0-99 instead of ranges 1-100
    pardoned = 0
    in_drawer = list(range(100))
    for _round in range(n):
        random.shuffle(in_drawer)
        for prisoner in range(100):
            reveal = prisoner
            found = False
            for go in range(50):
                card = in_drawer[reveal]
                if card == prisoner:
                    found = True
                    break
                reveal = card
            if not found:
                break
        if found:
            pardoned += 1
    return pardoned / n * 100   # %

if __name__ == '__main__':
    n = 100_000
    print(" Simulation count:", n)
    print(f" Random play wins: {play_random(n):4.1f}% of simulations")
    print(f"Optimal play wins: {play_optimal(n):4.1f}% of simulations")


100 prisoners

Solution:
'''100 Prisoners'''

from random import randint, sample

# allChainedPathsAreShort :: Int -> IO (0|1)
def allChainedPathsAreShort(n):
    '''1 if none of the index-chasing cycles in a shuffled
       sample of [1..n] cards are longer than half the
       sample size. Otherwise, 0.
    '''
    limit = n // 2
    xs = range(1, 1 + n)
    shuffled = sample(xs, k=n)

    # A cycle of boxes, drawn from a shuffled
    # sample, which includes the given target.
    def cycleIncluding(target):
        boxChain = [target]
        v = shuffled[target - 1]
        while v != target:
            boxChain.append(v)
            v = shuffled[v - 1]
        return boxChain

    # Nothing if the target list is empty, or if the cycle which contains the
    # first target is larger than half the sample size.
    # Otherwise, just a cycle of enchained boxes containing the first target
    # in the list, tupled with the residue of any remaining targets which
    # fall outside that cycle.
    def boxCycle(targets):
        if targets:
            boxChain = cycleIncluding(targets[0])
            return Just((
                difference(targets[1:])(boxChain),
                boxChain
            )) if limit >= len(boxChain) else Nothing()
        else:
            return Nothing()

    # No cycles longer than half of total box count ?
    return int(n == sum(map(len, unfoldr(boxCycle)(xs))))

# randomTrialResult :: RandomIO (0|1) -> Int -> (0|1)
def randomTrialResult(coin):
    '''1 if every one of the prisoners finds their ticket
       in an arbitrary half of the sample. Otherwise 0.
    '''
    return lambda n: int(all(
        coin(x) for x in range(1, 1 + n)
    ))

# TEST ----------------------------------------------------
# main :: IO ()
def main():
    '''Two sampling techniques constrasted with 100 drawers
       and 100 prisoners, over 100,000 trial runs.
    '''
    halfOfDrawers = randomRInt(0)(1)

    def optimalDrawerSampling(x):
        return allChainedPathsAreShort(x)

    def randomDrawerSampling(x):
        return randomTrialResult(halfOfDrawers)(x)

    # kSamplesWithNBoxes :: Int -> Int -> String
    def kSamplesWithNBoxes(k):
        tests = range(1, 1 + k)
        return lambda n: '\n\n' + fTable(
            str(k) + ' tests of optimal vs random drawer-sampling ' +
            'with ' + str(n) + ' boxes: \n'
        )(fName)(lambda r: '{:.2%}'.format(r))(
            lambda f: sum(f(n) for x in tests) / k
        )([
            optimalDrawerSampling,
            randomDrawerSampling,
        ])

    print(kSamplesWithNBoxes(10000)(10))

    print(kSamplesWithNBoxes(10000)(100))

    print(kSamplesWithNBoxes(100000)(100))

# ------------------------DISPLAY--------------------------

# fTable :: String -> (a -> String) ->
# (b -> String) -> (a -> b) -> [a] -> String
def fTable(s):
    '''Heading -> x display function -> fx display function ->
       f -> xs -> tabular string.
    '''
    def go(xShow, fxShow, f, xs):
        ys = [xShow(x) for x in xs]
        w = max(map(len, ys))
        return s + '\n' + '\n'.join(map(
            lambda x, y: y.rjust(w, ' ') + ' -> ' + fxShow(f(x)),
            xs, ys
        ))
    return lambda xShow: lambda fxShow: lambda f: lambda xs: go(
        xShow, fxShow, f, xs
    )

# fname :: (a -> b) -> String
def fName(f):
    '''Name bound to the given function.'''
    return f.__name__

# ------------------------GENERIC -------------------------

# Just :: a -> Maybe a
def Just(x):
    '''Constructor for an inhabited Maybe (option type) value.
       Wrapper containing the result of a computation.
    '''
    return {'type': 'Maybe', 'Nothing': False, 'Just': x}

# Nothing :: Maybe a
def Nothing():
    '''Constructor for an empty Maybe (option type) value.
       Empty wrapper returned where a computation is not possible.
    '''
    return {'type': 'Maybe', 'Nothing': True}

# difference :: Eq a => [a] -> [a] -> [a]
def difference(xs):
    '''All elements of xs, except any also found in ys.'''
    return lambda ys: list(set(xs) - set(ys))

# randomRInt :: Int -> Int -> IO () -> Int
def randomRInt(m):
    '''The return value of randomRInt is itself
       a function. The returned function, whenever
       called, yields a a new pseudo-random integer
       in the range [m..n].
    '''
    return lambda n: lambda _: randint(m, n)

# unfoldr(lambda x: Just((x, x - 1)) if 0 != x else Nothing())(10)
# -> [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
# unfoldr :: (b -> Maybe (a, b)) -> b -> [a]
def unfoldr(f):
    '''Dual to reduce or foldr.
       Where catamorphism reduces a list to a summary value,
       the anamorphic unfoldr builds a list from a seed value.
       As long as f returns Just(a, b), a is prepended to the list,
       and the residual b is used as the argument for the next
       application of f.
       When f returns Nothing, the completed list is returned.
    '''
    def go(v):
        xr = v, v
        xs = []
        while True:
            mb = f(xr[0])
            if mb.get('Nothing'):
                return xs
            else:
                xr = mb.get('Just')
                xs.append(xr[1])
        return xs
    return lambda x: go(x)

# MAIN ---
if __name__ == '__main__':
    main()


15 puzzle game

Implement the Fifteen Puzzle Game.

The   '''15-puzzle'''   is also known as:
:::*   '''Fifteen Puzzle'''
:::*   '''Gem Puzzle'''
:::*   '''Boss Puzzle'''
:::*   '''Game of Fifteen'''
:::*   '''Mystic Square'''
:::*   '''14-15 Puzzle'''
:::*   and some others.

;Related Tasks:
:*   15 Puzzle Solver
:*   [[16 Puzzle Game]]

Solution:
''' Python 3.6.5 code using Tkinter graphical user interface.'''

from tkinter import *
from tkinter import messagebox
import random

# ************************************************

class Board:
    def __init__(self, playable=True):
        while True:
            # list of text for game squares:
            self.lot = [str(i) for i in range(1,16)] + ['']
            if not playable:
                break
            # list of text for game squares randomized:
            random.shuffle(self.lot)
            if self.is_solvable():
                break

        # game board is 2D array of game squares:
        self.bd = []
        i = 0
        for r in range(4):
            row = []
            for c in range(4):
                row.append(Square(r,c,self.lot[i]))
                i += 1
            self.bd.append(row)

    # How to check if an instance of a 15 puzzle
    # is solvable is explained here:
    # https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/
    # I only coded for the case where N is even.
    def is_solvable(self):
        inv = self.get_inversions()
        odd = self.is_odd_row()
        if inv % 2 == 0 and odd:
            return True
        if inv % 2 == 1 and not odd:
            return True
        return False

    def get_inversions(self):
        cnt = 0
        for i, x in enumerate(self.lot[:-1]):
            if x != '':
                for y in self.lot[i+1:]:
                    if y != '' and int(x) > int(y):
                        cnt += 1
        return cnt

    # returns True if open square is in odd row from bottom:
    def is_odd_row(self):
        idx = self.lot.index('')
        return idx in [4,5,6,7,12,13,14,15]

    # returns name, text, and button object at row & col:
    def get_item(self, r, c):
        return self.bd[r][c].get()

    def get_square(self, r, c):
        return self.bd[r][c]

    def game_won(self):
        goal = [str(i) for i in range(1,16)] + ['']
        i = 0
        for r in range(4):
            for c in range(4):
                nm, txt, btn = self.get_item(r,c)
                if txt != goal[i]:
                    return False
                i += 1
        return True

# ************************************************

class Square:       # ['btn00', '0', None]
    def __init__(self, row, col, txt):
        self.row = row
        self.col = col
        self.name = 'btn' + str(row) + str(col)
        self.txt = txt
        self.btn = None

    def get(self):
            return [self.name, self.txt, self.btn]

    def set_btn(self, btn):
        self.btn = btn

    def set_txt(self, txt):
        self.txt = txt

# ************************************************

class Game:
    def __init__(self, gw):
        self.window = gw

        # game data:
        self.bd = None
        self.playable = False

        # top frame:
        self.top_fr = Frame(gw,
                            width=600,
                            height=100,
                            bg='light green')
        self.top_fr.pack(fill=X)

        self.hdg = Label(self.top_fr,
                         text='  15 PUZZLE GAME  ',
                         font='arial 22 bold',
                         fg='Navy Blue',
                         bg='white')
        self.hdg.place(relx=0.5, rely=0.4,
                       anchor=CENTER)

        self.dir = Label(self.top_fr,
                 text="(Click 'New Game' to begin)",
                 font='arial 12 ',
                 fg='Navy Blue',
                 bg='light green')
        self.dir.place(relx=0.5, rely=0.8,
                       anchor=CENTER)

        self.play_btn = Button(self.top_fr,
                               text='New \nGame',
                               bd=5,
                               bg='PaleGreen4',
                               fg='White',
                               font='times 12 bold',
                               command=self.new_game)
        self.play_btn.place(relx=0.92, rely=0.5,
                       anchor=E)

        # bottom frame:
        self.btm_fr = Frame(gw,
                            width=600,
                            height=500,
                            bg='light steel blue')
        self.btm_fr.pack(fill=X)

        # board frame:
        self.bd_fr = Frame(self.btm_fr,
                           width=400+2,
                           height=400+2,
                           relief='solid',
                           bd=1,
                           bg='lemon chiffon')
        self.bd_fr.place(relx=0.5, rely=0.5,
                         anchor=CENTER)

        self.play_game()

# ************************************************

    def new_game(self):
        self.playable = True
        self.dir.config(text='(Click on a square to move it)')
        self.play_game()

    def play_game(self):
        # place squares on board:
        if self.playable:
            btn_state = 'normal'
        else:
            btn_state = 'disable'
        self.bd = Board(self.playable)
        objh = 100  # widget height
        objw = 100  # widget width
        objx = 0    # x-position of widget in frame
        objy = 0    # y-position of widget in frame

        for r in range(4):
            for c in range(4):
                nm, txt, btn = self.bd.get_item(r,c)
                bg_color = 'RosyBrown1'
                if txt == '':
                    bg_color = 'White'
                game_btn = Button(self.bd_fr,
                                  text=txt,
                                  relief='solid',
                                  bd=1,
                                  bg=bg_color,
                                  font='times 12 bold',
                                  state=btn_state,
                                  command=lambda x=nm: self.clicked(x))
                game_btn.place(x=objx, y=objy,
                               height=objh, width=objw)

                sq = self.bd.get_square(r,c)
                sq.set_btn(game_btn)

                objx = objx + objw
            objx = 0
            objy = objy + objh

    # processing when a square is clicked:
    def clicked(self, nm):
        r, c = int(nm[3]), int(nm[4])
        nm_fr, txt_fr, btn_fr = self.bd.get_item(r,c)

        # cannot 'move' open square to itself:
        if not txt_fr:
            messagebox.showerror(
                'Error Message',
                'Please select "square" to be moved')
            return

        # 'move' square to open square if 'adjacent' to it:
        adjs = [(r-1,c), (r, c-1), (r, c+1), (r+1, c)]
        for x, y in adjs:
            if 0 <= x <= 3 and 0 <= y <= 3:
                nm_to, txt_to, btn_to = self.bd.get_item(x,y)
                if not txt_to:
                    sq = self.bd.get_square(x,y)
                    sq.set_txt(txt_fr)
                    sq = self.bd.get_square(r,c)
                    sq.set_txt(txt_to)
                    btn_to.config(text=txt_fr,
                                  bg='RosyBrown1')
                    btn_fr.config(text=txt_to,
                                  bg='White')
                    # check if game is won:
                    if self.bd.game_won():
                        ans = messagebox.askquestion(
                            'You won!!!   Play again?')
                        if ans == 'no':
                            self.window.destroy()
                        else:
                            self.new_game()
                    return

        # cannot move 'non-adjacent' square to open square:
        messagebox.showerror(
            'Error Message',
            'Illigal move, Try again')
        return

# ************************************************

root = Tk()
root.title('15 Puzzle Game')
root.geometry('600x600+100+50')
root.resizable(False, False)
g = Game(root)
root.mainloop()


21 game

'''21''' is a two player game, the game is played by choosing
a number ('''1''', '''2''', or '''3''')  to be added to the ''running total''.

The game is won by the player whose chosen number causes the ''running total''
to reach ''exactly'' '''21'''.

The ''running total'' starts at zero.
One player will be the computer.

Players alternate supplying a number to be added to the ''running total''.

;Task:
Write a computer program that will:
::* do the prompting (or provide a button menu),
::* check for errors and display appropriate error messages,
::* do the additions (add a chosen number to the ''running total''),
::* display the ''running total'',
::* provide a mechanism for the player to quit/exit/halt/stop/close the program,
::* issue a notification when there is a winner, and
::* determine who goes first (maybe a random or user choice, or can be specified when the game begins).

Solution:
from random import randint
def start():
	game_count=0
	print("Enter q to quit at any time.\nThe computer will choose first.\nRunning total is now {}".format(game_count))
	roundno=1
	while game_count<21:
		print("\nROUND {}: \n".format(roundno))
		t = select_count(game_count)
		game_count = game_count+t
		print("Running total is now {}\n".format(game_count))
		if game_count>=21:
			print("So, commiserations, the computer has won!")
			return 0
		t = request_count()
		if not t:
			print('OK,quitting the game')
			return -1
		game_count = game_count+t
		print("Running total is now {}\n".format(game_count))
		if game_count>=21:
			print("So, congratulations, you've won!")
			return 1
		roundno+=1

def select_count(game_count):
	'''selects a random number if the game_count is less than 18. otherwise chooses the winning number'''
	if game_count<18:
		t= randint(1,3)
	else:
		t = 21-game_count
	print("The computer chooses {}".format(t))
	return t

def request_count():
	'''request user input between 1,2 and 3. It will continue till either quit(q) or one of those numbers is requested.'''
	t=""
	while True:
		try:
			t = raw_input('Your choice 1 to 3 :')
			if int(t) in [1,2,3]:
				return int(t)
			else:
				print("Out of range, try again")
		except:
			if t=="q":
				return None
			else:
				print("Invalid Entry, try again")

c=0
m=0
r=True
while r:
	o = start()
	if o==-1:
		break
	else:
		c+=1 if o==0 else 0
		m+=1 if o==1 else 0
	print("Computer wins {0} game, human wins {1} games".format(c,m))
	t = raw_input("Another game?(press y to continue):")
	r = (t=="y")


24 game

The 24 Game tests one's mental arithmetic.

;Task
Write a program that displays four digits, each from 1 --> 9 (inclusive) with repetitions allowed.

The program should prompt for the player to enter an arithmetic expression using ''just'' those, and ''all'' of those four digits, used exactly ''once'' each. The program should ''check'' then evaluate the expression.

The goal is for the player to enter an expression that (numerically) evaluates to '''24'''.
*  Only the following operators/functions are allowed: multiplication, division, addition, subtraction
*  Division should use floating point or rational arithmetic, etc, to preserve remainders.
*  Brackets are allowed, if using an infix expression evaluator.
*  Forming multiple digit numbers from the supplied digits is ''disallowed''. (So an answer of 12+12 when given 1, 2, 2, and 1 is wrong).
*  The order of the digits when given does not have to be preserved.

;Notes
* The type of expression evaluator used is not mandated. An RPN evaluator is equally acceptable for example.
* The task is not for the program to generate the expression, or test whether an expression is even possible.

;Related tasks
* [[24 game/Solve]]

;Reference
* The 24 Game on h2g2.

Solution:
'''
 The 24 Game

 Given any four digits in the range 1 to 9, which may have repetitions,
 Using just the +, -, *, and / operators; and the possible use of
 brackets, (), show how to make an answer of 24.

 An answer of "q" will quit the game.
 An answer of "!" will generate a new set of four digits.
 Otherwise you are repeatedly asked for an expression until it evaluates to 24

 Note: you cannot form multiple digit numbers from the supplied digits,
 so an answer of 12+12 when given 1, 2, 2, and 1 would not be allowed.

'''

from __future__ import division, print_function
import random, ast, re
import sys

if sys.version_info[0] < 3: input = raw_input

def choose4():
    'four random digits >0 as characters'
    return [str(random.randint(1,9)) for i in range(4)]

def welcome(digits):
    print (__doc__)
    print ("Your four digits: " + ' '.join(digits))

def check(answer, digits):
    allowed = set('() +-*/\t'+''.join(digits))
    ok = all(ch in allowed for ch in answer) and \
         all(digits.count(dig) == answer.count(dig) for dig in set(digits)) \
         and not re.search('\d\d', answer)
    if ok:
        try:
            ast.parse(answer)
        except:
            ok = False
    return ok

def main():
    digits = choose4()
    welcome(digits)
    trial = 0
    answer = ''
    chk = ans = False
    while not (chk and ans == 24):
        trial +=1
        answer = input("Expression %i: " % trial)
        chk = check(answer, digits)
        if answer.lower() == 'q':
            break
        if answer == '!':
            digits = choose4()
            print ("New digits:", ' '.join(digits))
            continue
        if not chk:
            print ("The input '%s' was wonky!" % answer)
        else:
            ans = eval(answer)
            print (" = ", ans)
            if ans == 24:
                print ("Thats right!")
    print ("Thank you and goodbye")

if __name__ == '__main__': main()


4-rings or 4-squares puzzle

Replace       '''a, b, c, d, e, f,'''   and
  '''g '''       with the decimal
digits   LOW   --->   HIGH
such that the sum of the letters inside of each of the four large squares add up to
the same sum.

            +--------------+      +--------------+
            |              |      |              |
            |      a       |      |      e       |
            |              |      |              |
            |          +---+------+---+      +---+---------+
            |          |   |      |   |      |   |         |
            |          | b |      | d |      | f |         |
            |          |   |      |   |      |   |         |
            |          |   |      |   |      |   |         |
            +----------+---+      +---+------+---+         |
                       |       c      |      |      g      |
                       |              |      |             |
                       |              |      |             |
                       +--------------+      +-------------+

Show all output here.

:*   Show all solutions for each letter being unique with
         LOW=1     HIGH=7
:*   Show all solutions for each letter being unique with
         LOW=3     HIGH=9
:*   Show only the   ''number''   of solutions when each letter can be non-unique
         LOW=0     HIGH=9

;Related task:
* [[Solve the no connection puzzle]]

Solution:
def foursquares(lo,hi,unique,show):

    def acd_iter():
        """
        Iterates through all the possible valid values of
        a, c, and d.

        a = c + d
        """
        for c in range(lo,hi+1):
            for d in range(lo,hi+1):
                if (not unique) or (c <> d):
                    a = c + d
                    if a >= lo and a <= hi:
                        if (not unique) or (c <> 0 and d <> 0):
                            yield (a,c,d)

    def ge_iter():
        """
        Iterates through all the possible valid values of
        g and e.

        g = d + e
        """
        for e in range(lo,hi+1):
            if (not unique) or (e not in (a,c,d)):
                g = d + e
                if g >= lo and g <= hi:
                    if (not unique) or (g not in (a,c,d,e)):
                        yield (g,e)

    def bf_iter():
        """
        Iterates through all the possible valid values of
        b and f.

        b = e + f - c
        """
        for f in range(lo,hi+1):
            if (not unique) or (f not in (a,c,d,g,e)):
                b = e + f - c
                if b >= lo and b <= hi:
                    if (not unique) or (b not in (a,c,d,g,e,f)):
                        yield (b,f)

    solutions = 0
    acd_itr = acd_iter()
    for acd in acd_itr:
        a,c,d = acd
        ge_itr = ge_iter()
        for ge in ge_itr:
            g,e = ge
            bf_itr = bf_iter()
            for bf in bf_itr:
                b,f = bf
                solutions += 1
                if show:
                    print str((a,b,c,d,e,f,g))[1:-1]
    if unique:
        uorn = "unique"
    else:
        uorn = "non-unique"

    print str(solutions)+" "+uorn+" solutions in "+str(lo)+" to "+str(hi)
    print


4-rings or 4-squares puzzle

Solution:
'''4-rings or 4-squares puzzle'''

from itertools import chain

# rings :: noRepeatedDigits -> DigitList -> Lists of solutions
# rings :: Bool -> [Int] -> [[Int]]
def rings(uniq):
    '''Sets of unique or non-unique integer values
       (drawn from the `digits` argument)
       for each of the seven names [a..g] such that:
       (a + b) == (b + c + d) == (d + e + f) == (f + g)
    '''
    def go(digits):
        ns = sorted(digits, reverse=True)
        h = ns[0]

        # CENTRAL DIGIT :: d
        def central(d):
            xs = list(filter(lambda x: h >= (d + x), ns))

            # LEFT NEIGHBOUR AND LEFTMOST :: c and a
            def left(c):
                a = c + d
                if a > h:
                    return []
                else:
                    # RIGHT NEIGHBOUR AND RIGHTMOST :: e and g
                    def right(e):
                        g = d + e
                        if ((g > h) or (uniq and (g == c))):
                            return []
                        else:
                            agDelta = a - g
                            bfs = difference(ns)(
                                [d, c, e, g, a]
                            ) if uniq else ns

                            # MID LEFT AND RIGHT :: b and f
                            def midLeftRight(b):
                                f = b + agDelta
                                return [[a, b, c, d, e, f, g]] if (
                                    (f in bfs) and (
                                        (not uniq) or (
                                            f not in [a, b, c, d, e, g]
                                        )
                                    )
                                ) else []

    # CANDIDATE DIGITS BOUND TO POSITIONS [a .. g] --------

                            return concatMap(midLeftRight)(bfs)

                    return concatMap(right)(
                        difference(xs)([d, c, a]) if uniq else ns
                    )

            return concatMap(left)(
                delete(d)(xs) if uniq else ns
            )

        return concatMap(central)(ns)

    return lambda digits: go(digits) if digits else []

# TEST ----------------------------------------------------
# main :: IO ()
def main():
    '''Testing unique digits [1..7], [3..9] and unrestricted digits'''

    print(main.__doc__ + ':\n')
    print(unlines(map(
        lambda tpl: '\nrings' + repr(tpl) + ':\n\n' + unlines(
            map(repr, uncurry(rings)(*tpl))
        ), [
            (True, enumFromTo(1)(7)),
            (True, enumFromTo(3)(9))
        ]
    )))
    tpl = (False, enumFromTo(0)(9))
    print(
        '\n\nlen(rings' + repr(tpl) + '):\n\n' +
        str(len(uncurry(rings)(*tpl)))
    )

# GENERIC -------------------------------------------------

# concatMap :: (a -> [b]) -> [a] -> [b]
def concatMap(f):
    '''A concatenated list over which a function has been mapped.
       The list monad can be derived by using a function f which
       wraps its output in a list,
       (using an empty list to represent computational failure).
    '''
    return lambda xs: list(
        chain.from_iterable(map(f, xs))
    )

# delete :: Eq a => a -> [a] -> [a]
def delete(x):
    '''xs with the first of any instances of x removed.'''
    def go(xs):
        xs.remove(x)
        return xs
    return lambda xs: go(list(xs)) if (
        x in xs
    ) else list(xs)

#  difference :: Eq a => [a] -> [a] -> [a]
def difference(xs):
    '''All elements of ys except any also found in xs'''
    def go(ys):
        s = set(ys)
        return [x for x in xs if x not in s]
    return lambda ys: go(ys)

# enumFromTo :: (Int, Int) -> [Int]
def enumFromTo(m):
    '''Integer enumeration from m to n.'''
    return lambda n: list(range(m, 1 + n))

# uncurry :: (a -> b -> c) -> ((a, b) -> c)
def uncurry(f):
    '''A function over a pair of arguments,
       derived from a vanilla or curried function.
    '''
    return lambda x, y: f(x)(y)

# unlines :: [String] -> String
def unlines(xs):
    '''A single string formed by the intercalation
       of a list of strings with the newline character.
    '''
    return '\n'.join(xs)

# MAIN ---
if __name__ == '__main__':
    main()


99 bottles of beer

Display the complete lyrics for the song:     '''99 Bottles of Beer on the Wall'''.

;The beer song:
The lyrics follow this form:

:::              99 bottles of beer on the wall
:::              99 bottles of beer
:::              Take one down, pass it around
:::              98 bottles of beer on the wall

:::              98 bottles of beer on the wall
:::              98 bottles of beer
:::              Take one down, pass it around
:::              97 bottles of beer on the wall

... and so on, until reaching   '''0'''     (zero).

Grammatical support for   ''1 bottle of beer''   is optional.

As with any puzzle, try to do it in as creative/concise/comical a way
as possible (simple, obvious solutions allowed, too).

;See also:
*   http://99-bottles-of-beer.net/
*   [[:Category:99_Bottles_of_Beer]]
*   [[:Category:Programming language families]]
*   Wikipedia 99 bottles of beer

Solution:
"""
Excercise of style. An overkill for the task :-D

1. OOP, with abstract class and implementation with much common magic methods
2. you can customize:
    a. the initial number
    b. the name of the item and its plural
    c. the string to display when there's no more items
    d. the normal action
    e. the final action
    f. the template used, for foreign languages
3. strofas of the song are created with multiprocessing
4. when you launch it as a script, you can specify an optional parameter for
   the number of initial items
"""

from string import Template
from abc import ABC, abstractmethod
from multiprocessing.pool import Pool as ProcPool
from functools import partial
import sys

class Song(ABC):
    @abstractmethod
    def sing(self):
        """
        it must return the song as a text-like object
        """

        pass

class MuchItemsSomewhere(Song):
    eq_attrs = (
        "initial_number",
        "zero_items",
        "action1",
        "action2",
        "item",
        "items",
        "strofa_tpl"
    )

    hash_attrs = eq_attrs
    repr_attrs = eq_attrs

    __slots__ = eq_attrs + ("_repr", "_hash")

    def __init__(
        self,
        items = "bottles of beer",
        item = "bottle of beer",
        where = "on the wall",
        initial_number = None,
        zero_items = "No more",
        action1 = "Take one down, pass it around",
        action2 = "Go to the store, buy some more",
        template = None,
    ):
        initial_number_true = 99 if initial_number is None else initial_number

        try:
            is_initial_number_int = (initial_number_true % 1) == 0
        except Exception:
            is_initial_number_int = False

        if not is_initial_number_int:
            raise ValueError("`initial_number` parameter must be None or a int-like object")

        if initial_number_true < 0:
            raise ValueError("`initial_number` parameter must be >=0")

        true_tpl = template or """\
$i $items1 $where
$i $items1
$action
$j $items2 $where"""

        strofa_tpl_tmp = Template(true_tpl)
        strofa_tpl = Template(strofa_tpl_tmp.safe_substitute(where=where))

        self.zero_items = zero_items
        self.action1 = action1
        self.action2 = action2
        self.initial_number = initial_number_true
        self.item = item
        self.items = items
        self.strofa_tpl = strofa_tpl
        self._hash = None
        self._repr = None

    def strofa(self, number):
        zero_items = self.zero_items
        item = self.item
        items = self.items

        if number == 0:
            i = zero_items
            action = self.action2
            j = self.initial_number
        else:
            i = number
            action = self.action1
            j = i - 1

        if i == 1:
            items1 = item
            j = zero_items
        else:
            items1 = items

        if j == 1:
            items2 = item
        else:
            items2 = items

        return self.strofa_tpl.substitute(
            i = i,
            j = j,
            action = action,
            items1 = items1,
            items2 = items2
        )

    def sing(self):
        with ProcPool() as proc_pool:
            strofa = self.strofa
            initial_number = self.initial_number
            args = range(initial_number, -1, -1)
            return "\n\n".join(proc_pool.map(strofa, args))

    def __copy__(self, *args, **kwargs):
        return self

    def __deepcopy__(self, *args, **kwargs):
        return self

    def __eq__(self, other, *args, **kwargs):
        if self is other:
            return True

        getmyattr = partial(getattr, self)
        getotherattr = partial(getattr, other)
        eq_attrs = self.eq_attrs

        for attr in eq_attrs:
            val = getmyattr(attr)

            try:
                val2 = getotherattr(attr)
            except Exception:
                return False

            if attr == "strofa_tpl":
                val_true = val.safe_substitute()
                val2_true = val.safe_substitute()
            else:
                val_true = val
                val2_true = val

            if val_true != val2_true:
                return False

        return True

    def __hash__(self, *args, **kwargs):
        _hash = self._hash

        if _hash is None:
            getmyattr = partial(getattr, self)
            attrs = self.hash_attrs
            hash_true = self._hash = hash(tuple(map(getmyattr, attrs)))
        else:
            hash_true = _hash

        return hash_true

    def __repr__(self, *args, **kwargs):
        _repr = self._repr

        if _repr is None:
            repr_attrs = self.repr_attrs
            getmyattr = partial(getattr, self)

            attrs = []

            for attr in repr_attrs:
                val = getmyattr(attr)

                if attr == "strofa_tpl":
                    val_true = val.safe_substitute()
                else:
                    val_true = val

                attrs.append(f"{attr}={repr(val_true)}")

            repr_true = self._repr = f"{self.__class__.__name__}({', '.join(attrs)})"
        else:
            repr_true = _repr

        return repr_true

def muchBeersOnTheWall(num):
    song = MuchItemsSomewhere(initial_number=num)

    return song.sing()

def balladOfProgrammer(num):
    """
    Prints
    "99 Subtle Bugs in Production"
    or
    "The Ballad of Programmer"
    """

    song = MuchItemsSomewhere(
        initial_number = num,
        items = "subtle bugs",
        item = "subtle bug",
        where = "in Production",
        action1 = "Debug and catch, commit a patch",
        action2 = "Release the fixes, wait for some tickets",
        zero_items = "Zarro",
    )

    return song.sing()

def main(num):
    print(f"### {num} Bottles of Beers on the Wall ###")
    print()
    print(muchBeersOnTheWall(num))
    print()
    print()
    print('### "The Ballad of Programmer", by Marco Sulla')
    print()
    print(balladOfProgrammer(num))

if __name__ == "__main__":
    # Ok, argparse is **really** too much
    argv = sys.argv

    if len(argv) == 1:
        num = None
    elif len(argv) == 2:
        try:
            num = int(argv[1])
        except Exception:
            raise ValueError(
                f"{__file__} parameter must be an integer, or can be omitted"
            )
    else:
        raise RuntimeError(f"{__file__} takes one parameter at max")

    main(num)

__all__ = (Song.__name__, MuchItemsSomewhere.__name__, muchBeersOnTheWall.__name__, balladOfProgrammer.__name__)


99 bottles of beer

Solution:
"""
    99 Bottles of Beer on the Wall made functional

    Main function accepts a number of parameters, so you can specify a name of
    the drink, its container and other things. English only.
"""

from functools import partial
from typing import Callable

def regular_plural(noun: str) -> str:
    """English rule to get the plural form of a word"""
    if noun[-1] == "s":
        return noun + "es"

    return noun + "s"

def beer_song(
    *,
    location: str = 'on the wall',
    distribution: str = 'Take one down, pass it around',
    solution: str = 'Better go to the store to buy some more!',
    container: str = 'bottle',
    plurifier: Callable[[str], str] = regular_plural,
    liquid: str = "beer",
    initial_count: int = 99,
) -> str:
    """
    Return the lyrics of the beer song
    :param location: initial location of the drink
    :param distribution: specifies the process of its distribution
    :param solution: what happens when we run out of drinks
    :param container: bottle/barrel/flask or other containers
    :param plurifier: function converting a word to its plural form
    :param liquid: the name of the drink in the given container
    :param initial_count: how many containers available initially
    """

    verse = partial(
        get_verse,
        initial_count = initial_count,
        location = location,
        distribution = distribution,
        solution = solution,
        container = container,
        plurifier = plurifier,
        liquid = liquid,
    )

    verses = map(verse, range(initial_count, -1, -1))
    return '\n\n'.join(verses)

def get_verse(
    count: int,
    *,
    initial_count: str,
    location: str,
    distribution: str,
    solution: str,
    container: str,
    plurifier: Callable[[str], str],
    liquid: str,
) -> str:
    """Returns the verse for the given amount of drinks"""

    asset = partial(
        get_asset,
        container = container,
        plurifier = plurifier,
        liquid = liquid,
    )

    current_asset = asset(count)
    next_number = count - 1 if count else initial_count
    next_asset = asset(next_number)
    action = distribution if count else solution

    inventory = partial(
        get_inventory,
        location = location,
    )

    return '\n'.join((
        inventory(current_asset),
        current_asset,
        action,
        inventory(next_asset),
    ))

def get_inventory(
    asset: str,
    *,
    location: str,
) -> str:
    """
    Used to return the first or the fourth line of the verse

    >>> get_inventory("10 bottles of beer", location="on the wall")
    "10 bottles of beer on the wall"
    """
    return ' '.join((asset, location))

def get_asset(
    count: int,
    *,
    container: str,
    plurifier: Callable[[str], str],
    liquid: str,
) -> str:
    """
    Quantified asset

    >>> get_asset(0, container="jar", plurifier=regular_plural, liquid='milk')
    "No more jars of milk"
    """

    containers = plurifier(container) if count != 1 else container
    spelled_out_quantity = str(count) if count else "No more"
    return ' '.join((spelled_out_quantity, containers, "of", liquid))

if __name__ == '__main__':
    print(beer_song())


99 bottles of beer

Solution:
'''99 Units of Disposable Asset'''

from itertools import chain

# main :: IO ()
def main():
    '''Modalised asset dispersal procedure.'''

    # localisation :: (String, String, String)
    localisation = (
        'on the wall',
        'Take one down, pass it around',
        'Better go to the store to buy some more'
    )

    print(unlines(map(
        incantation(localisation),
        enumFromThenTo(99)(98)(0)
    )))

# incantation :: (String, String, String) -> Int -> String
def incantation(localisation):
    '''Versification of asset disposal
       and inventory update.'''

    location, distribution, solution = localisation

    def inventory(n):
        return unwords([asset(n), location])
    return lambda n: solution if 0 == n else (
        unlines([
            inventory(n),
            asset(n),
            distribution,
            inventory(pred(n))
        ])
    )

# asset :: Int -> String
def asset(n):
    '''Quantified asset.'''
    def suffix(n):
        return [] if 1 == n else 's'
    return unwords([
        str(n),
        concat(reversed(concat(cons(suffix(n))(["elttob"]))))
    ])

# GENERIC -------------------------------------------------

# concat :: [[a]] -> [a]
# concat :: [String] -> String
def concat(xxs):
    '''The concatenation of all the elements in a list.'''
    xs = list(chain.from_iterable(xxs))
    unit = '' if isinstance(xs, str) else []
    return unit if not xs else (
        ''.join(xs) if isinstance(xs[0], str) else xs
    )

# cons :: a -> [a] -> [a]
def cons(x):
    '''Construction of a list from x as head,
       and xs as tail.'''
    return lambda xs: [x] + xs if (
        isinstance(xs, list)
    ) else chain([x], xs)

# enumFromThenTo :: Int -> Int -> Int -> [Int]
def enumFromThenTo(m):
    '''Integer values enumerated from m to n
       with a step defined by nxt-m.'''
    def go(nxt, n):
        d = nxt - m
        return list(range(m, d + n, d))
    return lambda nxt: lambda n: (
        go(nxt, n)
    )

# pred ::  Enum a => a -> a
def pred(x):
    '''The predecessor of a value. For numeric types, (- 1).'''
    return x - 1 if isinstance(x, int) else (
        chr(ord(x) - 1)
    )

# unlines :: [String] -> String
def unlines(xs):
    '''A single string derived by the intercalation
       of a list of strings with the newline character.'''
    return '\n'.join(xs)

# unwords :: [String] -> String
def unwords(xs):
    '''A space-separated string derived from
       a list of words.'''
    return ' '.join(xs)

if __name__ == '__main__':
    main()


9 billion names of God the integer

This task is a variation of the short story by Arthur C. Clarke.

(Solvers should be aware of the consequences of completing this task.)

In detail, to specify what is meant by a   "name":
:The integer 1 has 1 name       "1".
:The integer 2 has 2 names   "1+1",   and   "2".
:The integer 3 has 3 names   "1+1+1",   "2+1",   and   "3".
:The integer 4 has 5 names   "1+1+1+1",   "2+1+1",   "2+2",   "3+1",   "4".
:The integer 5 has 7 names   "1+1+1+1+1",   "2+1+1+1",   "2+2+1",   "3+1+1",   "3+2",   "4+1",   "5".

;Task
Display the first 25 rows of a number triangle which begins:

                                      1
                                    1   1
                                  1   1   1
                                1   2   1   1
                              1   2   2   1   1
                            1   3   3   2   1   1

Where row   n   corresponds to integer   n,   and each column   C   in row   m   from left to right corresponds to the number of names beginning with   C.

A function   G(n)   should return the sum of the   n-th   row.

Demonstrate this function by displaying:   G(23),   G(123),   G(1234),   and   G(12345).

Optionally note that the sum of the   n-th   row   P(n)   is the      integer partition function.

Demonstrate this is equivalent to   G(n)   by displaying:   P(23),   P(123),   P(1234),   and   P(12345).

;Extra credit

If your environment is able, plot   P(n)   against   n   for   n=1\ldots 999.

;Related tasks
* [[Partition function P]]

Solution:
cache = [[1]]
def cumu(n):
    for l in range(len(cache), n+1):
        r = [0]
        for x in range(1, l+1):
            r.append(r[-1] + cache[l-x][min(x, l-x)])
        cache.append(r)
    return cache[n]

def row(n):
    r = cumu(n)
    return [r[i+1] - r[i] for i in range(n)]

print "rows:"
for x in range(1, 11): print "%2d:"%x, row(x)

print "\nsums:"
for x in [23, 123, 1234, 12345]: print x, cumu(x)[-1]


9 billion names of God the integer

Solution:
def partitions(n):
    partitions.p.append(0)

    for k in xrange(1, n + 1):
        d = n - k * (3 * k - 1) // 2
        if d < 0:
            break

        if k & 1:
            partitions.p[n] += partitions.p[d]
        else:
            partitions.p[n] -= partitions.p[d]

        d -= k
        if d < 0:
            break

        if k & 1:
            partitions.p[n] += partitions.p[d]
        else:
            partitions.p[n] -= partitions.p[d]

    return partitions.p[-1]

partitions.p = [1]

def main():
    ns = set([23, 123, 1234, 12345])
    max_ns = max(ns)

    for i in xrange(1, max_ns + 1):
        if i > max_ns:
            break
        p = partitions(i)
        if i in ns:
            print "%6d: %s" % (i, p)

main()


A+B

'''A+B'''   --- a classic problem in programming contests,   it's given so contestants can gain familiarity with the online judging system being used.

;Task:
Given two integers,   '''A''' and '''B'''.

Their sum needs to be calculated.

;Input data:
Two integers are written in the input stream, separated by space(s):
: (-1000 \le A,B \le +1000)

;Output data:
The required output is one integer:   the sum of '''A''' and '''B'''.

;Example:
::{|class="standard"
 ! input
 ! output
 |-
 | 2 2
 | 4
 |-
 | 3 2
 | 5
 |}

Solution:
a = int(input("First number: "))
b = int(input("Second number: "))
print("Result:", a+b)


ABC problem

You are given a collection of ABC blocks   (maybe like the ones you had when you were a kid).

There are twenty blocks with two letters on each block.

A complete alphabet is guaranteed amongst all sides of the blocks.

The sample collection of blocks:
  (B O)
  (X K)
  (D Q)
  (C P)
  (N A)
  (G T)
  (R E)
  (T G)
  (Q D)
  (F S)
  (J W)
  (H U)
  (V I)
  (A N)
  (O B)
  (E R)
  (F S)
  (L Y)
  (P C)
  (Z M)

;Task:
Write a function that takes a string (word) and determines whether the word can be spelled with the given collection of blocks.

The rules are simple:
::#   Once a letter on a block is used that block cannot be used again
::#   The function should be case-insensitive
::#   Show the output on this page for the following 7 words in the following example

;Example:
    >>> can_make_word("A")
    True
    >>> can_make_word("BARK")
    True
    >>> can_make_word("BOOK")
    False
    >>> can_make_word("TREAT")
    True
    >>> can_make_word("COMMON")
    False
    >>> can_make_word("SQUAD")
    True
    >>> can_make_word("CONFUSE")
    True

Solution:
'''
Note that this code is broken, e.g., it won't work when
blocks = [("A", "B"), ("A","C")] and the word is "AB", where the answer
should be True, but the code returns False.
'''
blocks = [("B", "O"),
          ("X", "K"),
          ("D", "Q"),
          ("C", "P"),
          ("N", "A"),
          ("G", "T"),
          ("R", "E"),
          ("T", "G"),
          ("Q", "D"),
          ("F", "S"),
          ("J", "W"),
          ("H", "U"),
          ("V", "I"),
          ("A", "N"),
          ("O", "B"),
          ("E", "R"),
          ("F", "S"),
          ("L", "Y"),
          ("P", "C"),
          ("Z", "M")]

def can_make_word(word, block_collection=blocks):
    """
    Return True if `word` can be made from the blocks in `block_collection`.

    >>> can_make_word("")
    False
    >>> can_make_word("a")
    True
    >>> can_make_word("bark")
    True
    >>> can_make_word("book")
    False
    >>> can_make_word("treat")
    True
    >>> can_make_word("common")
    False
    >>> can_make_word("squad")
    True
    >>> can_make_word("coNFused")
    True
    """
    if not word:
        return False

    blocks_remaining = block_collection[:]
    for char in word.upper():
        for block in blocks_remaining:
            if char in block:
                blocks_remaining.remove(block)
                break
        else:
            return False
    return True

if __name__ == "__main__":
    import doctest
    doctest.testmod()
    print(", ".join("'%s': %s" % (w, can_make_word(w)) for w in
                    ["", "a", "baRk", "booK", "treat",
                     "COMMON", "squad", "Confused"]))


ASCII art diagram converter

Given the RFC 1035 message diagram from Section 4.1.1 (Header section format) as a string:
http://www.ietf.org/rfc/rfc1035.txt

 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 |                      ID                       |
 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 |QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 |                    QDCOUNT                    |
 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 |                    ANCOUNT                    |
 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 |                    NSCOUNT                    |
 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 |                    ARCOUNT                    |
 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

Where (every column of the table is 1 bit):

 ID is 16 bits
 QR = Query (0) or Response (1)
 Opcode = Four bits defining kind of query:
   0:    a standard query (QUERY)
   1:    an inverse query (IQUERY)
   2:    a server status request (STATUS)
   3-15: reserved for future use
 AA = Authoritative Answer bit
 TC = Truncation bit
 RD = Recursion Desired bit
 RA = Recursion Available bit
 Z = Reserved
 RCODE = Response code
 QC = Question Count
 ANC = Answer Count
 AUC = Authority Count
 ADC = Additional Count

Write a function, member function, class or template that accepts a similar multi-line string as input to define a data structure or something else able to decode or store a header with that specified bit structure.

If your language has macros, introspection, code generation, or powerful enough templates, then accept such string at compile-time to define the header data structure statically.

Such "Header" function or template should accept a table with 8, 16, 32 or 64 columns, and any number of rows. For simplicity the only allowed symbols to define the table are + - | (plus, minus, pipe), and whitespace. Lines of the input string composed just of whitespace should be ignored. Leading and trailing whitespace in the input string should be ignored, as well as  before and after each table row. The box for each bit of the diagram takes four chars "+--+". The code should perform a little of validation of the input string, but for brevity a full validation is not required.

Bonus: perform a thoroughly validation of the input string.

Solution:
"""
http://rosettacode.org/wiki/ASCII_art_diagram_converter

Python example based off Go example:

http://rosettacode.org/wiki/ASCII_art_diagram_converter#Go

"""

def validate(diagram):

    # trim empty lines

    rawlines = diagram.splitlines()
    lines = []
    for line in rawlines:
        if line != '':
            lines.append(line)

    # validate non-empty lines

    if len(lines) == 0:
        print('diagram has no non-empty lines!')
        return None

    width = len(lines[0])
    cols = (width - 1) // 3

    if cols not in [8, 16, 32, 64]:
        print('number of columns should be 8, 16, 32 or 64')
        return None

    if len(lines)%2 == 0:
        print('number of non-empty lines should be odd')
        return None

    if lines[0] != (('+--' * cols)+'+'):
            print('incorrect header line')
            return None

    for i in range(len(lines)):
        line=lines[i]
        if i == 0:
            continue
        elif i%2 == 0:
            if line != lines[0]:
                print('incorrect separator line')
                return None
        elif len(line) != width:
            print('inconsistent line widths')
            return None
        elif line[0] != '|' or line[width-1] != '|':
            print("non-separator lines must begin and end with '|'")
            return None

    return lines

"""

results is list of lists like:

[[name, bits, start, end],...

"""

def decode(lines):
    print("Name     Bits  Start  End")
    print("=======  ====  =====  ===")

    startbit = 0

    results = []

    for line in lines:
        infield=False
        for c in line:
            if not infield and c == '|':
                infield = True
                spaces = 0
                name = ''
            elif infield:
                if c == ' ':
                    spaces += 1
                elif c != '|':
                    name += c
                else:
                    bits = (spaces + len(name) + 1) // 3
                    endbit = startbit + bits - 1
                    print('{0:7}    {1:2d}     {2:2d}   {3:2d}'.format(name, bits, startbit, endbit))
                    reslist = [name, bits, startbit, endbit]
                    results.append(reslist)
                    spaces = 0
                    name = ''
                    startbit += bits

    return results

def unpack(results, hex):
    print("\nTest string in hex:")
    print(hex)
    print("\nTest string in binary:")
    bin = f'{int(hex, 16):0>{4*len(hex)}b}'
    print(bin)
    print("\nUnpacked:\n")
    print("Name     Size  Bit pattern")
    print("=======  ====  ================")
    for r in results:
        name = r[0]
        size = r[1]
        startbit = r[2]
        endbit = r[3]
        bitpattern = bin[startbit:endbit+1]
        print('{0:7}    {1:2d}  {2:16}'.format(name, size, bitpattern))

diagram = """
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      ID                       |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    QDCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ANCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    NSCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ARCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

"""

lines = validate(diagram)

if lines == None:
    print("No lines returned")
else:
    print(" ")
    print("Diagram after trimming whitespace and removal of blank lines:")
    print(" ")
    for line in lines:
        print(line)

    print(" ")
    print("Decoded:")
    print(" ")

    results = decode(lines)

    # test string

    hex = "78477bbf5496e12e1bf169a4"

    unpack(results, hex)