Extended Straddling Checkerboard

Revision as of 16:08, 25 March 2024 by Petelomax (talk | contribs) (added transcription of ct37w)

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.

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.
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', so we would then have F/L (98) and SUPP (99) as follows:

  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
 000 111 222 333 444 555 666 777 888 999

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

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^3
                    else
                        figs = true
                        add *= fsl * c^3
                    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 += 3
            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:
0727923909290884848290798374919062919097907384825751829098222000000000989099905490758190708890981119890790827175

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

Python

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

from functools import reduce

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

# web site CT37w, but '/' (FIG) char is 98 not 99 to help differentiate from code for 9 of '999'
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', ' ', '.', 'α', 'ρ', 'μ', 'ν', 'γ', 'σ', '/', 'π',],]


def xcb_encode(message, nchangemode='98', code='κ', table=CT37w, 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
    # 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: # numeric numbers are triplicate encoded so '3' -> '333'
                if not numericmode:
                    numericmode = True
                    encoded.append(nchangemode) # FIG

                encoded.append(c*3)

        else:
            codemode = False
            if numericmode:
                encoded.append(nchangemode) # end numericmode with the FIG numeric code for '/' (98)
                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, nchangemode='98', code='κ', table=CT37w, sdict=SDICT):
    """ Decode extended straddling checkerboard """
    numbers = {c*3: c for c in list('0123456789')}
    prefixes = sorted([row[0] for row in table], reverse=True)
    pos = 0
    numericmode = False
    codemode = False
    decoded = []
    while pos < len(s):
        if numericmode:
            if s[pos:pos+3] in numbers:
                decoded.append(numbers[s[pos:pos+3]])
                pos += 2
            elif s[pos:pos+2] == nchangemode:
                numericmode = False
                pos += 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)))
Output:
Admin ACK your MSG. CODE291 SEND further 2000 SUPP to HQ by 1 March
Encoded:  072792390929088484829094919062919097907384825751829098222000000000989099905490758190708890981119890790827175
Decoded:  ADMIN ACK YOUR MSG. CODE291 SEND FURTHER 2000 SUPP TO HQ BY 1 MARCH

Wren

Library: Wren-str

For consistency with the Python example, this uses the CT-37w checkerboard with F/L changed to 98 and SUPP to 99.

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 * 3
                    } else {
                        figs = true
                        add = add + fsl + c * 3
                    }
                } 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
                i = i + 3
            } 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:
072792390929088484829094919062919097907384825751829098222000000000989099905490758190708890981119890790827175

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