Extended Straddling Checkerboard

From Rosetta Code
Extended Straddling Checkerboard is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

An extended Straddling Checkerboard, is like the regular Straddling checkerboard, but allows word dictionaries and arbitrary functional codes such as FIGURE, where you can specify a number literally.

Task

Implement encoding and decoding of a message using the extended straddling checkerboard, CT-37w, as described in the reference below.

You may switch the codes for F/L (99) and SUPP (98) to help differentiate the code for '9' from that of '999' and, if you do that, then digits only needed to be doubled rather than tripled. So we would then have the following checkerboard:

  A   E   I   N   O   T  CODE
  0   1   2   3   4   5   6

  B   C   D   F   G   H   J   K   L   M
 70  71  72  73  74  75  76  77  78  79

  P   Q   R   S   U   V   W   X   Y   Z
 80  81  82  83  84  85  86  87  88  89

SPC (.) ACK REQ MSG  RV GRD SND F/L SUP
 90  91  92  93  94  95  96  97  98  99

  0   1   2   3   4   5   6   7   8   9
 00  11  22  33  44  55  66  77  88  99

There is no need to create a word dictionary for CODE (6). It suffices to just include CODE followed by some 3 digit number in the message to be encoded.

Test your solution by encoding and decoding the message:

'Admin ACK your MSG. CODE291 SEND further 2000 SUPP to HQ by 1 March'

Related task
Reference

jq

Adapted from Wren

Works with jq, the C implementation of jq

Works with gojq, the Go implementation of jq

Works with jaq, the Rust implementation of jq

### Generic utility
# Emit a stream of the constituent codepoints:
def chars: explode[] | [.] | implode;

### The checkerboard
def efigs: "0123456789";

def drow1: "012345";

def checkerboard:
  def row1: "AEINOT";
  def row2: "BCDFGHJKLM";
  def row3: "PQRSUVWXYZ";
  def row4: " .";
  { ewords: {
     "SPC":  "90",  "DOT": "91",
     "ACK":  "92",  "REQ": "93", "MSG": "94", "RV": "95",
     "GRID": "96", "SEND": "97", "FSL": "98", "SUPP": "99"
    },
    emap: {},
    dmap: {},
    dwords:{}
  }
  | reduce range(0; row1|length) as $i (.; .emap[row1[$i:$i+1]] = ($i|tostring) )
  | reduce range(0; row2|length) as $i (.; .emap[row2[$i:$i+1]] = ((70 + $i)|tostring))
  | reduce range(0; row3|length) as $i (.; .emap[row3[$i:$i+1]] = ((80 + $i)|tostring))
  | reduce range(0; row4|length) as $i (.; .emap[row4[$i:$i+1]] = ((90 + $i)|tostring))

  | reduce (.emap|keys[])   as $k (.; .dmap[.emap[$k]] = $k)
  | reduce (.ewords|keys[]) as $k (.; .dwords[.ewords[$k]] = $k) ;

def encode:
  (ascii_upcase|split(" ")) as $words
  | ($words|length) as $wc
  | checkerboard
  | .res = ""
  | reduce range(0; $wc) as $i (.;
      $words[$i] as $word
      | .add = ""
      |  if .ewords[$word]
         then .add = .ewords[$word]
         elif .ewords[$word[0:-1]] and ($word|endswith("."))
         then .add = .ewords[$word[0:-1]] + .ewords["DOT"]
         elif $word|startswith("CODE")
         then .add = "6" + $word[4:]
         else .figs = false
         | reduce ($word|chars) as $c (.;
             if (efigs|contains($c))
             then if .figs
                  then .add += 2 * $c
                  else .figs = true
                  | .add += .ewords["FSL"] + 2 * $c
                  end
             else .emap[$c] as $ec
             | if ($ec|not) 
               then  "Message contains unrecognized character '\($c)'" | error
               else if .figs
                    then .add += .ewords["FSL"] + $ec
                    | .figs = false
                    else .add += $ec
                    end
                end
             end )
         | if .figs and ($i < $wc - 1)
           then .add += .ewords["FSL"]
           else .
           end
         end 
         | .res += .add
         | if ($i < $wc - 1) then .res += .ewords["SPC"] else . end
  )
  | .res ;

def decode:
  {s: .} + checkerboard
  | .ewords["FSL"] as $fsl
  | .res = ""
  | .figs = false
  | until (.s == "";
      .s[0:1] as $c
      | .ix = -1
      | if .figs
        then if (.s | startswith($fsl) | not)
             then .res += $c
             else .figs = false
             end
             | .s |= .[2:]
        else .ix = (drow1|index($c))
        | if .ix and .ix >= 0
          then .res += .dmap[drow1[.ix:.ix+1]]
          | .s |= .[1:]
          elif $c == "6"
          then .res += "CODE" + .s[1:4]
          | .s |= .[4:]
          elif $c == "7" or $c == "8"
          then .s[1:2] as $d
          | .res += .dmap[$c + $d]
          | .s |= .[2:]
          elif $c == "9"
          then .s[1:2] as $d
          | if $d == "0"
            then .res += " "
            elif $d == "1"
            then .res +=  "."
            elif $d == "8"
            then .figs |= not
            else .res += .dwords[$c + $d]
            end
            | .s |= .[2:]
          end
         end )
  | .res ;

### Demonstration
def demo:
  "Message:\n\(.)",
   (encode
    | "\nEncoded:\n\(.)",
      "\nDecoded:\n\(decode)" );

"Admin ACK your MSG. CODE291 SEND further 2000 SUPP to HQ by 1 March"
| demo
Output:
Message:
Admin ACK your MSG. CODE291 SEND further 2000 SUPP to HQ by 1 March

Encoded:
0727923909290884848290949190629190979073848257518290982200000098909990549075819070889098119890790827175

Decoded:
ADMIN ACK YOUR MSG. CODE291 SEND FURTHER 2000 SUPP TO HQ BY 1 MARCH

Julia

Translation of: Wren
const row1, row2, row3, row4 = "AEINOT", "BCDFGHJKLM", "PQRSUVWXYZ", " ."
const emap = Dict{String,String}()
for (row, k) in [(row1, -1), (row2, 69), (row3, 79), (row4, 89)]
    for i in eachindex(row)
        emap[string(row[i])] = string(i + k)
    end
end
const dmap = Dict{String,String}(v => k for (k, v) in emap)

const ewords = Dict{String,String}(
    "ACK" => "92",
    "REQ" => "93",
    "MSG" => "94",
    "RV" => "95",
    "GRID" => "96",
    "SEND" => "97",
    "SUPP" => "99",
)
const dwords = Dict{String,String}(v => k for (k, v) in ewords)

const efigs, spc, dot, fsl, drow1 = "0123456789", "90", "91", "98", "012345"

function encode(s)
    s, res = uppercase(s), ""
    words = split(s, r"\s")
    wc = length(words)
    for i = 1:wc
        word, add = words[i], ""
        if haskey(ewords, word)
            add = ewords[word]
        elseif haskey(ewords, word[begin:end-1]) && word[end] == "."
            add = ewords[word[begin:end-1]] * dot
        elseif startswith(word, "CODE")
            add = "6" * word[begin+4:end]
        else
            figs = false
            for c in word
                if contains(efigs, c)
                    if figs
                        add *= c^2
                    else
                        figs = true
                        add *= fsl * c^2
                    end
                else
                    ec = get(emap, string(c), "")
                    isempty(ec) && error("Message contains unrecognized character $c.")
                    if figs
                        add *= fsl * ec
                        figs = false
                    else
                        add *= ec
                    end
                end
            end
            if figs && i <= wc - 1
                add *= fsl
            end
        end
        res *= add
        if i <= wc - 1
            res *= spc
        end
    end
    return res
end

function decode(s)
    res, sc, figs, i = "", length(s), false, 1
    while i <= sc
        ch = s[i]
        c = string(ch)
        if figs
            if s[i:i+1] != fsl
                res *= c
                i += 2
            else
                figs = false
                i += 2
            end
        elseif !((ix = findfirst(==(ch), drow1)) isa Nothing)
            res *= dmap[string(drow1[ix])]
            i += 1
        elseif c == "6"
            res *= "CODE" * s[i+1:i+3]
            i += 4
        elseif c == "7" || c == "8"
            d = string(s[i+1])
            res *= dmap[c*d]
            i += 2
        elseif c == "9"
            d = string(s[i+1])
            if d == "0"
                res *= " "
            elseif d == "1"
                res *= "."
            elseif d == "8"
                figs = !figs
            else
                res *= dwords[c*d]
            end
            i += 2
        end
    end
    return res
end

const msg = "Admin ACK your MSG. CODE291 SEND further 2000 SUPP to HQ by 1 March"
println("Message:\n$msg")
enc = encode(msg)
println("\nEncoded:\n$enc")
dec = decode(enc)
println("\nDecoded:\n$dec")
Output:
Message:
Admin ACK your MSG. CODE291 SEND further 2000 SUPP to HQ by 1 March

Encoded:
07279239092908848482907983749190629190979073848257518290982200000098909990549075819070889098119890790827175

Decoded:
ADMIN ACK YOUR MSG. CODE291 SEND FURTHER 2000 SUPP TO HQ BY 1 MARCH

Phix

Translation of: Julia

Note this includes the two tiny tweaks to encode digits in two characters, as per "Moot point" on the talk page.

with javascript_semantics
constant emap = new_dict(),
         dmap = new_dict(),
         ewds = new_dict(),
         dwds = new_dict(),
         spc = "90", dot = "91", fsl = "98"
for d in {{"AEINOT",-1},{"BCDFGHJKLM",69},{"PQRSUVWXYZ",79},{" .",89}} do
    integer k = d[2]
    for i,ch in d[1] do
        string ik = sprintf("%d",i+k)
        setd(ch,ik,emap)
        setd(ik,ch,dmap)
    end for
end for
for d in {{"ACK","92"},{"REQ","93"},{"MSG","94"},{"RV","95"},
          {"GRID","96"},{"SEND","97"},{"SUPP","99"}} do
    string {k,v} = d
    setd(k,v,ewds)          
    setd(v,k,dwds)          
end for

function encode(string s)
    string res = ""
    sequence words = split(upper(s))
    integer wc = length(words)
    for i=1 to wc do
        string wrd = words[i], a = ""
        if getd_index(wrd,ewds) then
            a = getd(wrd,ewds)
        elsif getd_index(wrd[1..-2]) and wrd[$] == '.' then
            a = getd(wrd[1..-2],ewds) & dot
        elsif begins("CODE",wrd) then
            a = "6" & wrd[5..$]
        else
            bool figs = false
            for c in wrd do
                if find(c,"0123456789") then -- (efigs)
                    if not figs then figs = true; a &= fsl end if
--                  a &= repeat(c,3)
                    a &= repeat(c,2)
                elsif not getd_index(c,emap) then
                    throw("Message contains unrecognized character %c.",{c})
                else
                    if figs then figs = false; a &= fsl end if
                    a &= getd(c,emap)
                end if
            end for
            if figs and i<=wc-1 then a &= fsl end if
        end if
        res &= a
        if i <= wc-1 then res &= spc end if
    end for
    return res
end function

function decode(string s)
    string res = ""
    integer sc = length(s), figs = false, i = 1
    while i <= sc do
        integer c = s[i]
        if figs then
            if s[i..i+1] != fsl then
                res &= c
--              i += 3
                i += 2
            else
                figs = false
                i += 2
            end if
        elsif find(c,"012345") then -- row 1
            res &= getd(c&"",dmap)
            i += 1
        elsif c == '6' then
            res &= "CODE" & s[i+1..i+3]
            i += 4
        elsif c == '7'
           or c == '8' then
            res &= getd(s[i..i+1],dmap)
            i += 2
        elsif c == '9' then
            integer d = s[i+1]
            if d == '0' then
                res &= " "
            elsif d == '1' then
                res &= "."
            elsif d == '8' then
                figs = not figs
            else
                res &= getd(s[i..i+1],dwds)
            end if
            i += 2
        end if
    end while
    return res
end function

constant msg = "Admin ACK your MSG. CODE291 SEND further 2000 SUPP to HQ by 1 March",
         enc = encode(msg),
         unc = decode(enc)
printf(1,"Message:\n%s\n\nEncoded:\n%s\n\nDecoded:\n%s\n",{msg,enc,unc})
Output:
Message:
Admin ACK your MSG. CODE291 SEND further 2000 SUPP to HQ by 1 March

Encoded:
07279239092908848482907983749190629190979073848257518290982200000098909990549075819070889098119890790827175

Decoded:
ADMIN ACK YOUR MSG. CODE291 SEND FURTHER 2000 SUPP TO HQ BY 1 MARCH

Python

See the discussion for the different checkerboard table handling.

""" rosettacode.org/wiki/Extended_Straddling_Checkerboard """

from functools import reduce

WDICT = {
    'CODE': 'κ',
    'ACK': 'α',
    'REQ': 'ρ',
    'MSG': 'μ',
    'RV': 'ν',
    'GRID': 'γ',
    'SEND': 'σ',
    'SUPP': 'π',
}
# reversed WDICT for reverse lookup on decode
SDICT = {v: k for (k, v) in WDICT.items()}

# CT37w at https://www.ciphermachinesandcryptology.com/en/table.htm
CT37w = [['',  'A', 'E', 'I', 'N', 'O', 'T', 'κ', '',  '',  '',],
         ['7', 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M',],
         ['8', 'P', 'Q', 'R', 'S', 'U', 'V', 'W', 'X', 'Y', 'Z',],
         ['9', ' ', '.', 'α', 'ρ', 'μ', 'ν', 'γ', 'σ', 'π', '/'],]

# Modified CT37w: web site CT37w, but exchange '/' (FIG) char and 'π'
# to help differentiate the '999' encoding for a '9' from a terminator code
CT37w_mod = [['',  'A', 'E', 'I', 'N', 'O', 'T', 'κ', '',  '',  '',],
             ['7', 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M',],
             ['8', 'P', 'Q', 'R', 'S', 'U', 'V', 'W', 'X', 'Y', 'Z',],
             ['9', ' ', '.', 'α', 'ρ', 'μ', 'ν', 'γ', 'σ', '/', 'π',],]


def xcb_encode(message, table=CT37w, code='κ', wdict=WDICT):
    """
        Encode with extended straddling checkerboard. Default checkerboard is
        CT37w at https://www.ciphermachinesandcryptology.com/en/table.htm
        The numeric mode has the numbers as repeated in triplicate
        The CODE mode expects a 3-digit numeric code
    """
    encoded = []
    numericmode, codemode = False, False
    codemodecount = 0
    if table[-1][-1] == '/':
        nchangemode = '99'
        digit_repeats = 3
    else:
        nchangemode = '98'
        digit_repeats = 2

    # replace terms found in dictionary with a single char symbol that is in the table
    s = reduce(lambda x, p: x.replace(
        p[0], p[1]), wdict.items(), message.upper())
    for c in s:
        if c.isnumeric():
            if codemode:  # codemode symbols are preceded by the CODE digit '6' then as-is
                encoded.append(c)
                codemodecount += 1
                if codemodecount >= 3:
                    codemode = False

            else:  
                if not numericmode:
                    numericmode = True
                    encoded.append(nchangemode)  # FIG

                encoded.append(c*digit_repeats)

        else:
            codemode = False
            if numericmode:
                # end numericmode with the FIG numeric code for '/' (98)
                encoded.append(nchangemode)
                numericmode = False

            if c == code:
                codemode = True
                codemodecount = 0

            for row in table:
                if c in row:
                    k = row.index(c)
                    encoded.append(str(row[0]) + str(k-1))
                    break

    return ''.join(encoded)


def xcb_decode(s, table=CT37w, code='κ', sdict=SDICT):
    """ Decode extended straddling checkerboard """
    prefixes = sorted([row[0] for row in table], reverse=True)
    pos, numericmode, codemode = 0, False, False
    decoded = []
    if table[-1][-1] == '/':
        nchangemode = '99'
        digit_repeats = 3
    else:
        nchangemode = '98'
        digit_repeats = 2
    numbers = {c*digit_repeats: c for c in list('0123456789')}
    while pos < len(s):
        if numericmode:
            if s[pos:pos+digit_repeats] in numbers:
                decoded.append(numbers[s[pos:pos+digit_repeats]])
                pos += digit_repeats - 1
            elif s[pos:pos+2] == nchangemode:
                numericmode = False
                pos += 1
            elif decoded[-1] == '9':  # error, so backtrack if last was 9
                decoded.pop()
                numericmode = False
                pos -= digit_repeats - 1

        elif codemode:
            if (s[pos:pos+3]).isnumeric():
                decoded.append(s[pos:pos+3])
                pos += 2

            codemode = False

        elif s[pos:pos+2] == nchangemode:
            numericmode = not numericmode
            pos += 1

        else:
            for p in prefixes:
                if s[pos:].startswith(p):
                    n = len(p)
                    row = next(i for i, r in enumerate(table) if p == r[0])
                    c = table[row][int(s[pos+n])+1]
                    decoded.append(c)
                    if c == code:
                        codemode = True

                    pos += n
                    break

        pos += 1

    return reduce(lambda x, p: x.replace(p[0], p[1]), sdict.items(), ''.join(decoded))


if __name__ == '__main__':

    MESSAGE = 'Admin ACK your MSG. CODE291 SEND further 2000 SUPP to HQ by 1 March'
    print(MESSAGE)
    print('Encoded: ', xcb_encode(MESSAGE))
    print('Decoded: ', xcb_decode(xcb_encode(MESSAGE)))
    print('Encoded: ', xcb_encode(MESSAGE, CT37w_mod))
    print('Decoded: ', xcb_decode(xcb_encode(MESSAGE, CT37w_mod), CT37w_mod))
Output:
Admin ACK your MSG. CODE291 SEND further 2000 SUPP to HQ by 1 March
Encoded:  072792390929088484829094919062919097907384825751829099222000000000999098905490758190708890991119990790827175
Decoded:  ADMIN ACK YOUR MSG. CODE291 SEND FURTHER 2000 SUPP TO HQ BY 1 MARCH
Encoded:  0727923909290884848290949190629190979073848257518290982200000098909990549075819070889098119890790827175
Decoded:  ADMIN ACK YOUR MSG. CODE291 SEND FURTHER 2000 SUPP TO HQ BY 1 MARCH

Wren

Library: Wren-str
import "./str" for Str

var row1 = "AEINOT"
var row2 = "BCDFGHJKLM"
var row3 = "PQRSUVWXYZ"
var row4 = " ."

var emap = {}
for (i in 0...row1.count) emap[row1[i]] = i.toString
for (i in 0...row2.count) emap[row2[i]] = (70 + i).toString
for (i in 0...row3.count) emap[row3[i]] = (80 + i).toString
for (i in 0...row4.count) emap[row4[i]] = (90 + i).toString
var ewords = {
    "ACK": "92", "REQ": "93", "MSG": "94", "RV": "95",
    "GRID": "96", "SEND": "97", "SUPP": "99"
}
var efigs = "0123456789"
var spc = "90"
var dot = "91"
var fsl = "98"

var dmap = {}
var dwords = {}
for (k in emap.keys) dmap[emap[k]] = k
for (k in ewords.keys) dwords[ewords[k]] = k
var drow1 = "012345"

var encode = Fn.new { |s|
    s = Str.upper(s)
    var res = ""
    var words = s.split(" ")
    var wc = words.count
    for (i in 0...wc) {
        var word = words[i]
        var add = ""
        if (ewords.containsKey(word)) {
            add = ewords[word]
        } else if (ewords.containsKey(word[0...-1]) && word[-1] == ".") {
            add = ewords[word[0...-1]] + dot
        } else if (word.startsWith("CODE")) {
            add = "6" + word[4..-1]
        } else {
            var figs = false
            for (c in word) {
                if (efigs.contains(c)) {
                    if (figs) {
                        add = add + c * 2
                    } else {
                        figs = true
                        add = add + fsl + c * 2
                    }
                } else {
                    var ec = emap[c]
                    if (!ec) {
                        Fiber.abort("Message contains unrecognized character '%(c)'.")
                    }
                    if (figs) {
                        add = add + fsl + ec
                        figs = false
                    } else {
                        add = add + ec
                    }
                }
            }
            if (figs && i < wc - 1) add = add + fsl
        }
        res = res + add
        if (i < wc - 1) res = res + spc
    }
    return res
}

var decode = Fn.new { |s|
    var res = ""
    var sc = s.count
    var figs = false
    var i = 0
    while (i < sc) {
        var c = s[i]
        var ix = -1
        if (figs) {
            if (s[i..i+1] != fsl) {
                res = res + c
            } else {
                figs = false
            }
            i = i + 2
        } else if ((ix = drow1.indexOf(c)) >= 0) {
            res = res + dmap[drow1[ix]]
            i = i + 1
        } else if (c == "6") {
            res = res + "CODE" + s[i+1..i+3]
            i = i + 4
        } else if (c == "7" || c == "8") {
            var d = s[i+1]
            res = res + dmap[c + d]
            i = i + 2
        } else if (c == "9") {
            var d = s[i+1]
            if (d == "0") {
                res = res + " "
            } else if (d == "1") {
                res = res + "."
            } else if (d == "8") {
                figs = !figs
            } else {
                res = res + dwords[c + d]
            }
            i = i + 2
        }
    }
    return res
}
               
var msg = "Admin ACK your MSG. CODE291 SEND further 2000 SUPP to HQ by 1 March"
System.print("Message:\n%(msg)")
var enc = encode.call(msg)
System.print("\nEncoded:\n%(enc)")
var dec = decode.call(enc)
System.print("\nDecoded:\n%(dec)")
Output:
Message:
Admin ACK your MSG. CODE291 SEND further 2000 SUPP to HQ by 1 March

Encoded:
0727923909290884848290949190629190979073848257518290982200000098909990549075819070889098119890790827175

Decoded:
ADMIN ACK YOUR MSG. CODE291 SEND FURTHER 2000 SUPP TO HQ BY 1 MARCH