Created
April 15, 2018 16:04
-
-
Save PurpleMyst/68606c18870218ee799c3a428d08b5ae to your computer and use it in GitHub Desktop.
Solution to https://jmarcher.io/programming-challenge-dictionary-compression/ in Rust
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env run-cargo-script | |
// cargo-deps: quickcheck | |
use std::{borrow::Borrow, io::{self, BufRead}}; | |
#[cfg(test)] | |
#[macro_use] | |
extern crate quickcheck; | |
fn longest_common_prefix_length(x: &str, y: &str) -> usize { | |
x.chars() | |
.zip(y.chars()) | |
.take_while(|(x_char, y_char)| x_char == y_char) | |
.count() | |
} | |
fn compress(words: &[impl Borrow<str>]) -> Vec<String> { | |
words | |
.into_iter() | |
.scan(None, |maybe_last: &mut Option<String>, word| { | |
let word = word.borrow(); | |
assert!( | |
!word.chars().any(char::is_numeric), | |
"Words can not contain numeric characters, but {:?} did.", | |
word | |
); | |
let new_word: String = if let Some(ref last) = *maybe_last { | |
let lcp_length = longest_common_prefix_length(last, word); | |
if lcp_length == 0 { | |
// no prefix in common, just return the word | |
word.to_owned() | |
} else { | |
// factor out the common part of the word | |
format!("{}{}", lcp_length, &word[lcp_length..]) | |
} | |
} else { | |
word.to_owned() | |
}; | |
// we want to take the full word as "last", so for cases where we have multiple | |
// consecutive compression opportunities we take advantage of those. | |
*maybe_last = Some(word.to_owned()); | |
Some(new_word) | |
}) | |
.collect::<Vec<String>>() | |
} | |
fn decompress(words: &[impl Borrow<str>]) -> Vec<String> { | |
words | |
.into_iter() | |
.scan(None, |maybe_last: &mut Option<String>, word| { | |
let word = word.borrow(); | |
let new_word: String = if let Some(ref last) = *maybe_last { | |
let lcp_length = word.chars() | |
.take_while(|c| c.is_numeric()) | |
.collect::<String>(); | |
let extra_chars = String::from(&word[lcp_length.len()..]); | |
let lcp_length = lcp_length.parse::<usize>().unwrap_or(0); | |
format!("{}{}", &last[..lcp_length], extra_chars) | |
} else { | |
// just return the last word cloned | |
word.to_owned() | |
}; | |
*maybe_last = Some(new_word.clone()); | |
Some(new_word) | |
}) | |
.collect::<Vec<String>>() | |
} | |
fn main() { | |
let stdin = io::stdin(); | |
stdin | |
.lock() | |
.lines() | |
.collect::<io::Result<Vec<String>>>() | |
.map(|words| { | |
compress(&words) | |
.into_iter() | |
.for_each(|word| println!("{}", word)) | |
}) | |
.unwrap(); | |
} | |
#[cfg(test)] | |
mod tests { | |
use quickcheck::TestResult; | |
use {compress, decompress}; | |
#[test] | |
fn invariant_on_enable1() { | |
use std::{env, fs::File, io::{self, BufRead, BufReader}, path::Path}; | |
let enable1_path = env::var_os("HOME") | |
.map(|home| Path::join(Path::new(&home), Path::new("Documents/enable1.txt"))) | |
.unwrap(); | |
File::open(enable1_path) | |
.and_then(|file| { | |
BufReader::new(file) | |
.lines() | |
.collect::<io::Result<Vec<String>>>() | |
}) | |
.map(|words| assert_eq!(words.as_slice(), decompress(&compress(&words)).as_slice())) | |
.unwrap(); | |
} | |
quickcheck! { | |
fn invariant_with_quickcheck(xs: Vec<String>) -> TestResult { | |
if xs.iter().any(|word| word.chars().any(char::is_numeric)) { | |
TestResult::must_fail(move || decompress(&compress(&xs))) | |
} else { | |
TestResult::from_bool(xs == decompress(&compress(&xs))) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment