dwyl / dwyl/learn-zig

Comparison Elixir, JS, Zig on deciphering a text.

Open
#4 0 comments 2 reactions 0 assignees View on GitHub
Dominant language
No language data
Stars
4
Forks
1
PR merge metrics
No merged PRs in 30d

Description

This last homework today is about a kinda Caesar deciphering. It displays a bit on how to use strings and charcodes on the long learning journey of `Zig` (and also the ongoing learning of `Elixir` and `JS` by the way).

## What?

Given an input:
```
"zpv gpvoe uif nfttbhf Abd",
````
and given the cipher

```
"the"
```
you have to decipher this input, knowing that the cipher gives the Caesar shift.

You want to find:

```
"you found the message Zac"
```

You understand that you must find a word in the input such that every letter is equidistant to those of the cipher. Then, just shift the rest.

## Why?

Two reasons. Firstly practice `Zig`, but also `Elixir` and `JS` primitives on strings. And discover more data structures in `Zig`.

The most important learning maybe when (and a bit how) to use data structures in Zig such as `ArrayList` or slices (of arrays). This code only gives an idea of this problem.

This page is short but useful about allocators.

The second reason maybe more personal is that you could be expected to pass these kind of tests for a job, and I did, and failed on this 🤷‍♂️. Never too late.

## How? The algorithm (at least the one I came up with 😬)

- firstly splitting the input on the blank separator,
- then finding the possible shift by comparing every word with the cipher, so that the distance between each letter is the same,
- then with this shift, converting each char of each word into integers, and doing some calculus modulo 26 with this shift while separating the lower letters (97 to 122) from capital letters (65 to 90).

If charCode >=65 or <= 90, it's a capital letter, so ((charCode-65 + shift + 26) mod 26)+65
If charCode >=97 or <= 122, it's lowercase, so ((charCode-97 + shift + 26) mod 26)+97

Why adding 26 inside ? Because you may get negative numbers, depending on the shift, so you don't want to use `-1 mod 26`, but instead `25 mod 26`.

## Code in `JS`, `Elixir`, `Zig`

### Zig

`Zig` demands to be (very) careful with the **types** and
with the data structures you use: `array`, `slice` or`ArrayList`,
and of course allocate (and de-allocate) memory.

These data structures have different methods, in particular on how to allocate memory, append data, and how you access them.
- the `ArrayList` is used when you need a dynamic array and don't now the size in advance. This is the case when we decipher the list of words. We use the methods `append` for a single character, and `appendSlice` for several (a "word").
- the slice is used when you know in advance the size. For example, in the "deCipher" function. You simply use `[i]` to append/assign an element to the slice.

To transform an ArrayList into a slice, you use `toOwnedSlice` (this remove all the overhead of the struct and cleans the memory).

Another "tricky" point. I use 2 times a splitting on the input "words_iter". It seems that once Zig used it with an iterator, you must reset the iterator by reassigning its value.

`Zig` considers strings as a slice of `u8`. It has primitives `std.ascii.isUpper` to check the case of a letter, but you need to be careful with the type casting.

You have a primitive `std.mem.spitAny` to split the input string.
You get an "iterator". You can simply use a `while` loop on `word_iterator.next()` and compute the shift.

```zig
const std = @import("std");
const print = std.debug.print;
const ascii = std.ascii;
const ArenaAllocator = std.heap.ArenaAllocator;

fn guessShift(word: []const u8, cipher: []const u8) ?i8 {
if (word.len != cipher.len) return null; // Ensure lengths match

const guessedShift = @as(i8, @intCast(cipher[0])) - @as(i8, @intCast(word[0]));
for (word, cipher) |w, c| {
if (@as(i8, @intCast(c)) - @as(i8, @intCast(w)) != guessedShift) {
return null;
}
}
return guessedShift;
}

fn deCipher(input: []const u8, shift: i8, allocator: std.mem.Allocator) ![]u8 {
var result = try allocator.alloc(u8, input.len);
errdefer result.deinit();

for (input,0..) |char, i| {
result[i] = rotate(char, shift);
}

return result;
}

fn rotate(char: u8, shift: i8) u8 {
if (ascii.isAlphabetic(char)) {
const base: u8 = if (ascii.isUpper(char)) 'A' else 'a';
const shiftedChar = @as(i16, @intCast(char)) - @as(i16, @intCast(base)) + @as(i16, shift);
// we add 26 to "shiftedChar to avoid negative values, then we take the modulo 26, so this addition is harmless
const rotatedChar = @as(u8, @intCast(@rem(shiftedChar + 26, 26))) + base;
return rotatedChar;
}
return char;
}

pub fn main() !void {
var arena = ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();

const input = "zpv gpvoe uif nfttbhf Abd";
const cipher = "the";

// Use splitAny to iterate over the words
var words_iter = std.mem.splitAny(u8, input, " ");
var shift: ?i8 = null;

// Find valid candidates and calculate shift
while (words_iter.next()) |word| {
if (word.len == cipher.len) {
shift = guessShift(word, cipher);
if (shift != null) break;
}
}

if (shift == null) {
return error.NoValidShiftFound;
}

print("Found shift: {}\n", .{shift.?});

// Output the deciphered message in a single pass, without extra allocations
var result = std.ArrayList(u8).init(allocator);
defer result.deinit();

// Re-instantiate the iterator
words_iter = std.mem.splitAny(u8, input, " ");
var i: usize = 0;
while (words_iter.next()) |word| : (i += 1) {
// put a space between words and not at the beginning
if (i != 0) {
try result.append(' ');
}

const deciphered = try deCipher(word, shift.?, allocator);
try result.appendSlice(deciphered);
}

print("Final result: {s}\n", .{result.items});
}
```

### JS

You can `.split(' ')`, and then use `.charCodeAt()` and `String.fromCharCode(..)`.

Javascript code

```js
function guessShift(word, cipher) {
// Calculate the shift using the first pair of characters
let guessedShift = cipher.charCodeAt(0) - word.charCodeAt(0);

// Check if all characters have the same shift
for (let i = 0; i < word.length; i++) {
if (cipher.charCodeAt(i) - word.charCodeAt(i) !== guessedShift) {
guessedShift = false;
}
}
return guessedShift;
}

function deCipher(input, shift) {
let result = "";
for (let i = 0; i < input.length; i++) {
let charCode = input.charCodeAt(i);
if (charCode >= 65 && charCode <= 90) {
// if in ["a",..,"z"]
result += String.fromCharCode(((charCode - 65 - shift + 26) % 26) + 65);
} else if (charCode >= 97 && charCode <= 122) {
// if in ["A",..,"Z"]
result += String.fromCharCode(((charCode - 97 - shift + 26) % 26) + 97);
} else {
result += input[i];
}
}
return result;
}

function main(input, cipher) {
const splittedInput = input.split(" ");
const shift = splittedInput
.map((candidate) => guessShift(candidate, cipher))
.find((e) => e != false);

return splittedInput.map((input) => deCipher(input, -shift)).join(" ");
}

// Example Usage
let res = main("zpv gpvoe uif nfttbhf Abd", "the");
res == "you found the message Zac";
console.log("res: ", res);
```


### Elixir

Demands more work.

You also have the primitive `String.split/2`.

The "secret" is to get the ascii values with a pattern matching:
`<>=word`
so that you can calculate on integers: `i1 - i2` and get the "shift".

Then to rebuild, you separate each word into a list of letters via `String.grapheme`, and convert this list into a charlist with `List.to_charlist`.

The elements are "char_codes", so you can do calculus on them:
`rem(char_code-65+shift+26, 26) + 65` for capital letters for example.

Then you can one by one apply the shift, and convert back with `List.to_string`.

Elixir code

```elixir
defmodule ExCipher do
def check_words(input, cipher) do
list_of_shifts =
Enum.map(input, fn word ->
if String.length(word) == String.length(cipher) do
check_word(word, cipher)
else
false
end
end)

Enum.find(list_of_shifts, & &1)
end

def check_word(word, cipher) do
{true, initial_shift, word_rest, cipher_rest} =
calc_shift(nil, String.graphemes(word), String.graphemes(cipher))

check_shift(initial_shift, word_rest, cipher_rest)
end

def check_shift(shift, word_rest, cipher_rest) do
case calc_shift(shift, word_rest, cipher_rest) do
{false, _, _, _} ->
false

{true, ^shift, [], []} ->
shift

{true, ^shift, rest1, rest2} ->
check_shift(shift, rest1, rest2)
end
end

def calc_shift(shift, word_rest, cipher_rest) do
[<> | rest1] = word_rest
[<> | rest2] = cipher_rest

if shift == nil do
{true, l1 - l2, rest1, rest2}
else
{shift == l1 - l2, l1 - l2, rest1, rest2}
end
end

def decipher(shift, input) do
Enum.map(input, fn word ->
word
|> String.graphemes()
|> List.to_charlist()
|> Enum.map(fn letter ->
rotate(letter, shift)
end)
|> List.to_string()
end)
end

def rotate(letter, shift) do
cond do
letter in ?a..?z ->
Integer.mod(letter - 97 - shift, 26) + 97

letter in ?A..?Z ->
Integer.mod(letter - 65 - shift, 26) + 65

true ->
letter
end
end
end

# Example
input = "zpv gpvoe uif nfttbhf Abd"
cipher = "the"

splitted_input = String.split(input, " ")

shift = ExCipher.check_words(splitted_input, cipher) |> dbg()

ExCipher.decipher(shift, splitted_input)
|> dbg()
```


Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.