summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Burwell <ben@benburwell.com>2019-09-12 16:21:08 -0400
committerBen Burwell <ben@benburwell.com>2019-09-12 16:30:00 -0400
commit528352cddbe0290653c56a27a4134637ad0624e5 (patch)
tree4cb4b626ec97b3256e6187670d5888449cdf6350
Initial commit
-rw-r--r--caesar.go96
-rw-r--r--caesar_test.go17
-rw-r--r--go.mod3
3 files changed, 116 insertions, 0 deletions
diff --git a/caesar.go b/caesar.go
new file mode 100644
index 0000000..d62a26e
--- /dev/null
+++ b/caesar.go
@@ -0,0 +1,96 @@
+// Package caesar implements the Caesar cipher.
+//
+// The Caesar cipher is a basic monoalphabetic substitution cipher where each
+// letter in the plaintext is shifted by a fixed distance to form the
+// ciphertext. A ciphertext can be decrypted by reversing the shift.
+package caesar
+
+import (
+ "strings"
+)
+
+// A RuneRange represents a range of runes that are to be shifted.
+//
+// Since runes encompass the entire UTF-8 space, we don't necessarily want to
+// apply the Caesar shift to all runes in a plaintext lest we end up with
+// unprintable or otherwise difficult characters.
+type RuneRange struct {
+ // Start represents the beginning of the range (inclusive).
+ Start rune
+ // End represents the end of the range (inclusive).
+ End rune
+}
+
+// Contains checks whether r is in the rune range.
+func (rr RuneRange) Contains(r rune) bool {
+ return r >= rr.Start && r <= rr.End
+}
+
+// Shift shifts r by d within the range, modulo the size of the range.
+func (rr RuneRange) Shift(r rune, d int) rune {
+ if !rr.Contains(r) {
+ return r
+ }
+ return rr.Start + (r - rr.Start + rune(d)%(rr.End-rr.Start))
+}
+
+// A Coder encodes and decodes Caesar cipher messages according to its key and
+// valid rune ranges.
+//
+// Its zero value doesn't do any shifting, so both Encode and Decode are simply
+// the identity function.
+type Coder struct {
+ // Key determines the shift (+Key for encoding, -Key for decoding).
+ Key int
+ // Ranges determine which rune ranges should be shifted.
+ Ranges []RuneRange
+}
+
+// Encode takes the message and returns a Caesar shifted version.
+func (c Coder) Encode(msg string) string {
+ return strings.Map(c.shifter(c.Key), msg)
+}
+
+// Decode takes a shifted message and returns a plaintext version.
+func (c Coder) Decode(msg string) string {
+ return strings.Map(c.shifter(-c.Key), msg)
+}
+
+type shiftFunc func(rune) rune
+
+// shifter returns a shiftFunc for the coder's rune ranges.
+func (c Coder) shifter(delta int) shiftFunc {
+ return func(r rune) rune {
+ for _, rr := range c.Ranges {
+ if rr.Contains(r) {
+ return rr.Shift(r, delta)
+ }
+ }
+ return r
+ }
+}
+
+// DefaultCoder is a Coder which uses the historical key of 3 and encodes
+// uppercase and lowercase characters a-z and the digits 0-9.
+//
+// DefaultCoder is used by Encode and Decode.
+//
+// Define your own Coder to use custom rune ranges or keys.
+var DefaultCoder = Coder{
+ Key: 3,
+ Ranges: []RuneRange{
+ {Start: 'a', End: 'z'},
+ {Start: 'A', End: 'Z'},
+ {Start: '0', End: '9'},
+ },
+}
+
+// Encode encodes msg using DefaultCoder.
+func Encode(msg string) string {
+ return DefaultCoder.Encode(msg)
+}
+
+// Decode decodes msg using DefaultCoder.
+func Decode(msg string) string {
+ return DefaultCoder.Decode(msg)
+}
diff --git a/caesar_test.go b/caesar_test.go
new file mode 100644
index 0000000..e7dc3c0
--- /dev/null
+++ b/caesar_test.go
@@ -0,0 +1,17 @@
+package caesar
+
+import (
+ "testing"
+)
+
+func TestCaesar(t *testing.T) {
+ msg := "Attack at dawn"
+ encoded := Encode(msg)
+ if encoded == msg {
+ t.Errorf("expected ciphertext and plaintext to differ")
+ }
+ decoded := Decode(encoded)
+ if decoded != msg {
+ t.Errorf("expected recovered plaintext to match message, but got: %s", decoded)
+ }
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..89c5530
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module go.burwell.io/caesar
+
+go 1.13