diff options
-rw-r--r-- | caesar.go | 96 | ||||
-rw-r--r-- | caesar_test.go | 17 | ||||
-rw-r--r-- | go.mod | 3 |
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) + } +} @@ -0,0 +1,3 @@ +module go.burwell.io/caesar + +go 1.13 |