Ever take one of those tests where you're given a sequence of integers, and you have to tell them the next one in the sequence? For example, you might be shown something like this:
```
7 1 3 9 3 5 25 19 _ ```
The pattern, in this case, is to subtract six, then add two, then square, which means the correct answer is 21.
Obviously, people have varying experiences when attempting to solve problems like this, ranging from "oh, this is fun" to "no thanks, I'd rather go put some nails in my blender and listen to that for a while". However, I think we can all agree that however easy or difficult you may find these, they are so abstract and blatantly pointless that there are better things to be doing than taking a test that has these problems in it. That's where I got the idea for SeaCanal.
SeaCanal is a "sequence analyzer" written in Rust ("seq-anal"...get it? Look, if you don't want to deal with awful puns, you probably shouldn't be reading things on my Github). Basically, you give it a sequence of numbers, and it tells you any patterns it can find in it. Theoretically, if you were to be taking one of those dumb tests,and you had access to a computer, you could plug in the sequence, and SeaCanal would tell you what the pattern is (don't actually do that though, cheating is bad).
SeaCanal is published on crates.io, so you can use it like any other Cargo package:
Add the following to your Cargo.toml
under the [dependencies]
section:
sea-canal = "0.2"
Add the following to the root of your project:
extern crate sea_canal;
The next time you run cargo build
, the package will be installed for that project.
Simply run cargo install sea-canal
.
First, import Analyzer
, as well as the Analyze
trait:
use sea_canal::{Analyze, Analyzer};
Create an analyzer for whichever sequence you want to analyze:
let analyzer = Analyzer::from_seq(&[7, 1, 3, 9, 3, 5, 25, 19]);
Then call either one of the find_patterns
methods to find all patterns, or one of the find_any_pattern
methods to find a single pattern (giving either a maximum length or exact length of the pattern, depending on the method):
println!("{:?}", analyzer.find_any_pattern(7))
Assuming you've set up your path correctly for your cargo install
binaries, you can run SeaCanal with scnl
. Then just type in a (whitespace-delimited) sequence of integers, and hit "enter".
Alternately, to see a (very small) sample of SeaCanal analyzing some preset sequences, run scnl --sample
.
(This is really verbose, so feel free to skip it, especially if you find math boring).
First, SeaCanal looks at each pair of adjacent numbers in the sequence and computes the possible operations that could lead from one to another. Right now, the operations it supports are rather limited (see Operation Types). For example, the analysis of the first sequence above would look like this:
7 -> 1: =1, -6, /7
1 -> 3: =3, +2, *3
3 -> 9: =9, +6, *3, ^2
9 -> 3: =3, -6, /3, root 2
3 -> 5: =5, +2
5 -> 25: =25, +20, *5, ^2
25 -> 19: =19, -6
To make it easier to talk about, we'll call the adjacent numbers in a sequence a "transition", and we'll call the set of possible operations describing a transition a "choice".
SeaCanal then starts trying to find a pattern with the fewest operations possible. This means that it first tries to find a pattern of one operation; if it can't find one that fits, then it tries to find one with two operations. This keeps repeating until it finds a pattern than matches or it has reaches an upper bound (which is passed in by the user). This is to ensure that it doesn't generate useless patterns. For instance, if for this example, the pattern [/7, *3, *3, /3, +2, *5, -6]
is not very meaningful, despite the fact that these numbers could technically be the start of a sequence with that pattern.
To identify a pattern of a given length n
, SeaCanal divides up the transitions into slices of size n
(the last slice might be smaller, which is fine), and then groups together the ones that occur in the same location in the slices. Finally, SeaCanal tries to find a common choice among all the transitions in a given group.
Following our example, the slicing for n = 1
would just be each transition in its own slice, so all of the transitions would be grouped together. Finding whether there is a pattern of length 1 would just mean trying to find a choice that's in every transition. Obviously, in this case, there is no such choice:
```
7 -> 1: -6, /7 1 -> 3: =3, +2, *3 3 -> 9: =9, +6, *3, ^2 9 -> 3: =3, -6, /3, root 2 3 -> 5: =5, +2 5 -> 25: =25, +20, *5, ^2 25 -> 19: =19, -6 ```
We move on to n = 2
. The slicing looks like this:
Slice 1: 7 -> 1, 1 -> 3
Slice 2: 3 -> 9, 9 -> 3
Slice 3: 3 -> 5, 5 -> 25
Slice 4: 25 -> 19
and the groups would be:
Group 1: 7 -> 1, 3 -> 9, 3 -> 5, 25 -> 19
Group 2: 1 -> 3, 9 -> 3, 5 -> 25
(If you evenly space out each slice in a row, then the groups are just the individual columns).
In the first group, our list of choices is this:
7 -> 1: =1, -6, /7
3 -> 9: =9, +6, *3, ^2
3 -> 5: =5, +2
25 -> 19: =19, -6
We still don't have any operation that appears in all of the choices, which means there aren't any patterns of length 2.
With n = 3
, the slices would be:
Slice 1: 7 -> 1, 1 -> 3, 3 -> 9
Slice 2: 9 -> 3, 3 -> 5, 5 -> 25
Slice 3: 25 -> 19
And the groups would be:
Group 1: 7 -> 1, 9 -> 3, 25 -> 19
Group 2: 1 -> 3, 3 -> 5
Group 3: 3 -> 9, 5 -> 25
Looking at the first group, we find the common operation -6
:
7 -> 1: =1, -6, /7
9 -> 3: =3, -6, /3, root 2
25 -> 19: =19, -6
In the second group, we find +2
:
1 -> 3: =3, +2, *3
3 -> 5: =5, +2
And finally, in the last group, we find ^2
, which completes the pattern:
3 -> 9: =9, +6, *3, ^2
5 -> 25: =25, +20, *5, ^2
(Note that the common operations for each transition don't actually have to be in the same "column" like they are for these three groups; I just put them like that so it would be visually easier to notice).
NOTE: Modulus is not yet implemented
A constant element in a sequence. For example, the sequence 2 6 8 4 12 8 4
could be described by the pattern *3, =8, -4
(where =8
means a constant value of 8
).
You can define custom operation by making an instance of the CustomPatternElem
struct. Doing this requires defining a function
of type (i32, i32) -> bool
, which acts as a test to determine if a given pair of adjacent numbers in a sequence can be decribed by that operations. For example, the following code tests a sequence with a custom operation for raising a number to the fourth power:
```rust
extern crate sea_canal;
use seacanal::{Analyze, Analyzer}; use seacanal::pattern::{CustomPatternElem, Pattern}; use sea_canal::pattern::PatternElem::*;
fn pow4(i: i32, j: i32) -> bool { i * i * i * i == j }
let pow4pattern = CustomPatternElem::new(pow4, "^4"); let slice = &[2, 16, 3, 81, 68]; let analyzer = Analyzer::withcustompatterns(slice, vec![pow4pattern.clone()]);
asserteq!(Some(pat![Custom(pow4pattern), Minus(13)]), analyzer.findanypattern(4)); ```