A little while back, data was frequently stored on punched card. A typical card would have 80 columns of 12 possible 'punch' positions. A plethora of encodings was used, each for their own specific purpose.

These days we like to think of a byte as 8 bits. In this challenge, you're tasked to convert an 8-bit byte to a 12-bit word so that it may be stored on a punched card. Since too many holes per column would weaken a punched card considerably, your encoding must adhere to the following rules,

  • No more than 3 holes punched per column (i.e., no more than 3 bits set per 12-bit word)
  • No more than 2 holes punched consecutively in the same column (i.e., no more than 2 consecutive bits set in each word)

There are 289 valid 12-bit punch cards, of which 1 has 0 holes, 12 have 1 hole, 66 have 2 holes, and 210 have 3 holes. Compare this to 256 8-bit sequences.

You must create two programs/functions: one of which takes a 8-bit byte and converts it to a 12-bit word according to an encoding of your choice that follows the above rules; and one of which takes a 12-bit word according to your encoding and converts it back to an 8-bit byte. The latter program may exhibit undefined behaviour for any input which is not contained in your encoding.

Input and output is flexible according to the defaults for I/O, and may even differ between your programs. Your score is the sum of the bytecount of both programs. The programs must work independently, so they may not 'share' code between them.

  • 8
    \$\begingroup\$ It'll be a significant achievement if someone finds a simple enough way to do this without searching for all valid words. \$\endgroup\$ – Bubbler Sep 15 '20 at 3:50
  • 1
    \$\begingroup\$ @xnor No, they must work independently. \$\endgroup\$ – Sanchises Sep 15 '20 at 5:52
  • 2
    \$\begingroup\$ @JonathanAllan Sure that seems ideal for making punched cards by hand. \$\endgroup\$ – Sanchises Sep 15 '20 at 8:19
  • 1
    \$\begingroup\$ @Bubbler There are 27 tredecillion valid encodings. Surely one would lend itself well for compression :) \$\endgroup\$ – Sanchises Sep 15 '20 at 9:02
  • 3
    \$\begingroup\$ @Sanchises Since the order matters here, there are 2.40×10⁵⁵⁰ valid encodings. \$\endgroup\$ – Vaelus Sep 15 '20 at 14:19

12 Answers 12


JavaScript (ES6),  103  100 bytes

See also my other answer for less bitwise operations, more maths, and a closed-form expression

Both functions expect and return an integer.

8-bit → 12-bit ( 56  53 bytes)


Try it online!


This is a recursive function that computes the next valid 12-bit word \$w_{n+1}\$ directly from the previous valid word \$w_n\$.

Step 1: Update w according to the total number of bits set

Given w from the previous iteration we first compute:

x = w & (w - 1)

which is w without its least significant bit.

Likewise, we remove the least significant bit from x by doing:

x & (x - 1)

If the result is zero, we just increment w.

If the result is non-zero, it means that there are already 3 bits set in w and we are not allowed to insert a 4th one. In that case, we add the least significant bit of w (which is w - x) to w. This results in at most as many bits set in the updated value than in the original one, or less in case of carry propagation.


110010 +  10 -> 110100
101100 + 100 -> 110000

Step 2: Update w according to the number of consecutive bits set

We now have to make sure that there are not 3 consecutive bits set in w. We do that by adding the result of a bitwise AND between w, w / 2 and w / 4.


01110 + (01110 & 00111 & 00011) -> 01110 + 10 -> 10000

12-bit → 8-bit (47 bytes)


Try it online!


In this version, we decrement w at each iteration and increment the final result whenever w is a valid 12-bit word. We stop when w = 0.

We use the following methods to test the validity of w. They are similar to the ones used above.

  • we compute w & w / 2 & w / 4 to make sure that there are not 3 consecutive bits set
  • we compute a chain of three w & (w - 1) to make sure that there are no more than 3 bits set
  • 1
    \$\begingroup\$ Wow, tight. Thanks for the longer explanation. \$\endgroup\$ – xer0x Sep 17 '20 at 15:52

APL (Dyalog Unicode), 21 + 22 = 62 43 bytes




Try it online!

Pretty much the same programs, with indexing into an array () of valid punch cards for the encoder and finding the index (⍳⍨) for the decoder. Both the encoder and the decoder take a number and output a number.


                ,⍳12/2     ⍝ Create every combination of 12 bits
    (         )¨           ⍝ Map each to
          3+/⊢             ⍝ Does the sum of each consecutive group of three bits
        3∊                 ⍝ Contain a 3 (1 if so, else 0)
     +/+                   ⍝ Add that to the total digit sum
   ⍋                       ⍝ And grade this list ascending
                           ⍝ This outputs the indexes (which are the same as parsing the original bits in base 2) in order of the smallest value to the largest

⎕⌷                         ⍝ Finally we either index the input into the list
⎕⍳⍨                        ⍝ Or find the index of the input in the list

  • \$\begingroup\$ Nice use of grade. Btw, using full programs can save bytes from both: {⍵⌷...} → ⎕⌷..., (...)⍳⊢ → ⎕⍳⍨... \$\endgroup\$ – Bubbler Sep 15 '20 at 8:55
  • \$\begingroup\$ @Bubbler Yeah, I was already considering it (since you showed me how to format it on a different answer), but I couldn't figure out how to convert my existing testing structure. Oh well \$\endgroup\$ – Jo King Sep 15 '20 at 9:06

Jelly, 13 + 12 = 25 bytes

8-bit byte (integer) -> 12-bit word (list of 1-indexed set bits):


Try it online!

12-bit word (list of 1-indexed set bits) -> 8-bit byte (integer):


Try it online!

Here is a map (byte-integer word-as-12-bits word-as-1-indexed-set-bits) using the first Link.


Both Links create a list of all (\$286\$) ways to punch either two or three holes as lists of 1-indexed holes (e.g. [3,7] represents 001000100000), sorts these by the incremental differences between those indices (i.e. [3,7] (001000100000) maps to [4] while [2,3,4] (011100000000) maps to [1,1]), and reverses that result (placing the \$11\$ of the form 0*110* at the very end, preceded by the \$10\$ (invalid ones) of the form 0*1110*, with \$265\$ usable patterns at the start).

The first Link finds the 0-indexed entry, while the second finds the 1-indexed index of the representation of a word in a dequeued version of the list (this then gives us a 0 when the input is not found).

3Ḋ12œcẎIÞṚ - niladic chain
3          - three
 Ḋ         - dequeued -> [2,3]
  12       - twelve
    œc     - combinations (vectorises)
             -> [[[1,2]…[1,12],[2,3]…[11,12]],[[1,2,3],[1,2,4]…[10,11,12]]]
      Ẏ    - tighten
             -> [[1,2]…[1,12],[2,3]…[11,12],[1,2,3],[1,2,4]…[10,11,12]]
        Þ  - sort by:
       I   -   incremental differences
               -> [[1,2]…[11,12],[1,2,3]…[10,11,12],[1,2,4]…[1,12]]
         Ṛ - reverse
               -> [[1,12]…[1,2,4],[10,11,12]…[1,2,3],[11,12]…[1,2]]
                  <- 265 valid -> <-   10 invalid  -> <- 11 valid->

ị@‘ - rest of Link 1: byte (integer), b
  ‘ - increment -> b+1
 @  - with swapped arguments:
ị   -   (b+1) 1-indexed index into (the list above)

Ḋi  - rest of Link 2: word (list of hole indexes), w
Ḋ   - dequeue (the list above) - remove the first word (representing the zero byte)
 i  - index of (w) (in that) - yields zero if not found

Python 3, 160 bytes

Python 3, 82 bytes

f=lambda b,n=0:0**b*f'{n:012b}'or f(b-(sum(map(bin(~n).count,('111','1')))<4),n+1)

Try it online!

Python 3, 78 bytes

[f'{i:012b}'for i in range(2181)if sum(map(bin(i).count,('111','1')))<4].index

Try it online!

The first function (8-to-12-bit) increments numbers resursively until the binary representation satisfies all criteria. The second function (12-to-8-bit) uses the index method of a filtered list.

The list in the second function has a __getitem__ method which could replace the first function, but this is 2 bytes longer.

Thanks to ovs and xnor for saving 6 bytes in total.

  • \$\begingroup\$ sum(map(bin(n+1).count,('111','1')))<4 saves 2 bytes. \$\endgroup\$ – ovs Sep 15 '20 at 10:58
  • \$\begingroup\$ bin(n+1) can be bin(~n), since negating only prepends a - to the binary representation. \$\endgroup\$ – xnor Sep 15 '20 at 11:01
  • \$\begingroup\$ Thanks @ovs @xnor! \$\endgroup\$ – Jitse Sep 15 '20 at 11:08

JavaScript (ES7),  167 144  143 bytes

8-bit → 12-bit ( 120 97  96 bytes)

This answer is definitely not very competitive, but I was curious to see how the conversion could be done without any recursion nor trial-and-error.

So here is a closed-form expression!

Expects and returns an integer.


Try it online!


This is based on the observation that the positions of the bits in a valid 12-bit word are correlated to tetrahedral and triangular numbers.

If \$n\$ is less than \$5\$, we just return \$n\$. Otherwise, we go through the following steps:

  • We first figure out what is the lowest \$k\$ such that \$T(k-1)\ge n-3\$, where \$T(k)\$ is the \$k\$-th tetrahedral number, defined as:


    \$k\$ is the real root of the following cubic equation, rounded towards \$0\$:

    $$x^3 + 3x^2 + 2x - 6(n-3) = 0$$

    Once solved with WolframAlpha's Cubic Equation Solver and after a few approximations (safe for our operating range), we end up with:

    $$\cases{ t=\left(\sqrt{\strut729(n-3)^2-3}+27(n-3)\right)^{1/3}\\ k = \left\lfloor\dfrac{t}{2.08} + \dfrac{0.694}{t}\right\rfloor }$$

    Fortunately, we don't need so much precision and it can be simplified to the following JS expression, which gives the correct result for \$4<n<256\$:

    k = ~~((54 * n - 166) ** 0.333 / 2.1 + 0.17)
  • \$k\$ is the 0-indexed position of the leading bit in the punch card word.

  • We define:


  • If \$p>0\$, the position of the 2nd bit in the punch card word is given by:


    which is based on the reverse formula for triangular numbers.

    The JS implementation uses a right-shift. It means that we actually get \$-1\$ if \$p=0\$, in which case there's no 2nd bit.

  • The position of the 3rd bit in the punch card word is:


    There's no 3rd bit if \$r<0\$.

We use … | 2 ** expr rather than … | 1 << expr so that negative bit positions are ignored as expected.

Hybrid version (90 bytes)

For the record, below is my first attempt which was still using a limited number of recursive calls (up to 12) to compute \$k\$.


Try it online!

12-bit → 8-bit (47 bytes)

I didn't write a specific 12-bit to 8-bit converter for this answer. But because the sequence is exactly the same, we can just use the one we already have.


Try it online!

  • 1
    \$\begingroup\$ If it's worth doing it's worth overdoing... \$\endgroup\$ – Sanchises Sep 15 '20 at 17:34

Brachylog, 27 + 27 = 54 bytes


Try it online!

It's the same program! To decode 2240, give it as an argument and set the input to Z like here.

How it works

∧{                }ᶠ²⁵⁶     get the first 256 values of …
  ḃ                          in the base 2 representation …
   s₃ᵇ                        every 3 consecutive elements …
      +ᵐ≤ᵛ2                   has a sum ≤ 2
           &ḃ+≤3             and the base representation has a sum ≤ 3
                &≜           turn the constraints to a number
                       ;?∋₎ the number at index (input) is the output

Charcoal, 21 + 21 = 42 bytes



Try it online! Link is to verbose version of code.



Try it online! Link is to verbose version of code.

The only difference between the two programs is the second byte, which is § (AtIndex) for the encoder and (Find) for the decoder. Explanation:

     φ                  Predefined variable 1000
   ׳                   Multiplied by 3
  Φ                     Filter on implicit range
        ⁴               Literal `4`
       ›                Is greater than
           ι            Current index
          ⍘ ²           Convert to a binary string
         Σ              Digital sum
      ›                 And not
               ι        Current index
              ⍘ ²       Convert to a binary string
             №          Count (contains)
                 111    Literal string `111`
                    N   Input as a number
 §                      Index into filtered results
 ⌕                      Find index in filtered results
I                       Cast to string
                        Implicitly print

C (gcc), 64 + 64 = 128 bytes

Saved 2 6 bytes thanks to Dominic van Essen!!!
Saved 9 bytes thanks to the man himself Arnauld!!!
Saved 2 bytes thanks to ceilingcat!!!

Encode \$8\$-bit to \$12\$-bit 85 \$\cdots\$ 66 64 bytes


Try it online!

Decode \$12\$-bit to \$8\$-bit 85 \$\cdots\$ 68 64 bytes


Try it online!


Sequentially goes through integers starting at \$1\$ only counting ones with valid bit patterns and returns the \$n^{th}\$ valid integer from \$1\$ for encoding, \$e(n)\$, \$8\$-bit to \$12\$-bit. Counts the number of valid integers from \$1\$ up to \$n\$ for decoding, \$d(n)\$, \$12\$-bit to \$8\$-bit.

GCC builtins used

__builtin_popcount(n) returns the number of 1-bits in \$n\$.

  • 1
    \$\begingroup\$ Nice. You can save 2x 1-byte by using <7 instead of !=7. I didn't know about the GCC-specific __builtins, but even without them this approach can be pretty golfy at 194 bytes \$\endgroup\$ – Dominic van Essen Sep 15 '20 at 15:16
  • \$\begingroup\$ @DominicvanEssen Nice one - thanks! :-) \$\endgroup\$ – Noodle9 Sep 15 '20 at 15:54
  • \$\begingroup\$ 126 bytes \$\endgroup\$ – Dominic van Essen Sep 17 '20 at 8:30
  • \$\begingroup\$ @DominicvanEssen Very nice, got rid of that !=7 again! Was just thinking about skewing it to get rid of that zero too - thanks! :D \$\endgroup\$ – Noodle9 Sep 17 '20 at 8:38
  • \$\begingroup\$ @DominicvanEssen Ah! This introduces a bug - last transform is greater than 12 bits. \$\endgroup\$ – Noodle9 Sep 17 '20 at 8:53

05AB1E, 36 (18 + 18) bytes

8-bit (integer) to 12-bit (string of 0s/1s) (18 bytes):


Try it online or verify all test cases.

12-bit (string of 0s/1s) to 8-bit (integer) (18 bytes):


Try it online or verify all test cases.

The only difference is the last byte (è vs k).


T           # Push 10
 12ã        # Take the cartesian product of "10" and 12:
            #  ["111111111111","111111111110","111111111101",...,"000000000000"]
    ʒ       # Filter it by:
     1¢     #  Count the amount of 1s
       4‹   #  And check that this count is smaller than 4
    }ʒ      # Filter again:
      ƵA    #  Push compressed integer 111
        å≠  #  And check that the string does NOT contain "111" as substring
     }I     # After the second filter: push the input
       è    # (8-bit to 12-bit) Get the string at the 0-based input index
       k    # (12-bit to 8-bit) Get the 0-based index of the input-string
            # (after which it is output implicitly as result)

See this 05AB1E tip of mine (section How to compress large integers?) to understand why ƵA is 111.


R, 42 + 61 = 103 bytes



Try it online!

Input as decimal number; outputs 3 positions to punch holes, with zero indicating no hole to punch.



Try it online!

Input as 3 positions of punched holes, in ascending order, prefixed with zero if only two holes are punched; outputs as decimal number.


a=combn(13,3) generates all picks of 3 integers from 1..13, without repetitions, in ascending order. We subtract 1 to get all 2- and 3-hole combinations.
a=a[a[1,]+2<a[3,]] keeps only combinations where the lowest minus the highest position is less than 2, thereby removing any sets of 3 holes in a row.
To encode b, we choose the bth combination. To decode w, we subtract w from each combination: the correct combination is now all-zero, so its logical inverse is all-TRUE, so we select the combination that sums to 3.

  • \$\begingroup\$ Unfortunately apply(a,2,var)>1 is longer than a[1,]+2<a[3,], so no improvement there. \$\endgroup\$ – Cong Chen Sep 15 '20 at 15:22
  • \$\begingroup\$ Yes. It would have been a cool approach that I hadn't thought-of, otherwise. Or even apply(a,2,sd)>1 at 1 byte shorter... \$\endgroup\$ – Dominic van Essen Sep 15 '20 at 15:26

Japt, 17 + 19 17 = 36 34 bytes

I'm not sure I've fully understood the challenge, but let's give it a go ...

8-bit to 12-bit, 17 bytes

Input as an integer, output as a binary string.

gI²o¤fÈè1 §3«Xø#o

Try it or run all test cases

12-bit to 8-bit, 19 17 bytes

Input as a binary string, output as an integer.

nI²o¤fÈè1 §3«Xø#o

Try it or run all test cases


gI²o¤fÈè1 §3«Xø#o     :Implicit input of integer (first programme)
nI²o¤fÈè1 §3«Xø#o     :Implicit input of binary string (second programme)
g                     :Index into (first programme)
n                     :Convert from base (second programme)
 I                    :64
  ²                   :Squared
   o                  :Range [0,I²)
    ¤                 :To binary strings
     f                :Filter by
      È               :Passing each X through the following function
       è1             :  Count the "1"s
          §3          :  Less than or equal to 3
            «         :  AND NOT
             Xø       :  X contains
               #o     :  111
                 Ã    :End filter (second programme)
                  bU  :First 0-based index of U (second programme)
  • \$\begingroup\$ Looks like your understanding is fine to me. \$\endgroup\$ – Jonathan Allan Sep 15 '20 at 15:57
  • \$\begingroup\$ Thanks, @JonathanAllan; I wasn't clear whether there was a specific means for the conversion of if it was entirely up to us. \$\endgroup\$ – Shaggy Sep 15 '20 at 16:07

Pyth, 14 + 14 = 28 bytes


@sDoS.+N^U2 12


xsDoS.+N^U2 12

Try encoding online! Try decoding online!

Both programs rely on the snippet sDoS.+N^U2 12, which creates a list of all possible length-12 lists of 0s and 1s, whose first 256 entries are all valid punch cards. This can be verified with this program: Verify it online!.

How the snippet works:

sDoS.+N^U2 12
       ^U2 12    Generate all length-12 lists of 0s and 1s.
  o              Sort by
    .+N            The deltas of the length-12 list
   S               sorted
sD               Sort by the sum of the length-12 list.

Sorting on the sorted deltas of the length-12 list moves lists with more alternations between runs of 0s and 1s to the front. This moves the lists with 3 1s in a row as their only 1s to the back. Sorting on the sum of the length-12 list then brings the lists with 0, 1, 2, or 3 1s to the front. Pyth's sort is stable, so this ends up with the valid punch sequences at the front.

I'm unsatisfied with the ^U2 12 part of the program to generate the base length-12 lists. If the rest of the program could handle strings, ^`T12 would save a character. If anyone can figure out how to save a character, I'd love to hear about it.

  • \$\begingroup\$ That's a very clever method. \$\endgroup\$ – Arnauld Sep 16 '20 at 9:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.