1#![forbid(unsafe_code)]
2
3use difference::Difference::{Add, Rem, Same};
4use difference::{Changeset, Difference};
5use prettytable::{format, row, Table};
6use std::error;
7use std::fs::File;
8use std::io::prelude::*;
9use std::io::{BufRead, BufReader};
10use std::path::Path;
11use std::path::PathBuf;
12use structopt::StructOpt;
13use textwrap::{fill, termwidth};
14
15type Result<T> = std::result::Result<T, Box<dyn error::Error>>;
16
17#[derive(StructOpt, Debug)]
19#[structopt(author, about)]
20pub struct Config {
21 #[structopt(short = "o", long)]
23 sort: bool,
24
25 #[structopt(short, long)]
27 lowercase: bool,
28
29 #[structopt(short, long, default_value = " ")]
32 separators: Vec<char>,
33
34 #[structopt(short, long, parse(from_os_str))]
36 file: Option<PathBuf>,
37
38 file1: Option<PathBuf>,
40
41 file2: Option<PathBuf>,
43
44 #[structopt(long)]
46 line1: Option<String>,
47
48 #[structopt(long)]
50 line2: Option<String>,
51
52 #[structopt(long, short = "m")]
54 output_file1: Option<PathBuf>,
55
56 #[structopt(long, short = "n")]
58 output_file2: Option<PathBuf>,
59}
60
61impl Config {
62 pub fn from_cmd_args() -> Config {
64 let mut c = Config::from_args();
65 c.separators.push('\n');
66 c
67 }
68
69 pub fn from_lines(
76 sort: bool,
77 lowercase: bool,
78 separators: Vec<char>,
79 l1: &str,
80 l2: &str,
81 ) -> Config {
82 Config {
83 sort,
84 lowercase,
85 separators,
86 file: Option::None,
87 file1: Option::None,
88 file2: Option::None,
89 line1: Option::Some(l1.to_string()),
90 line2: Option::Some(l2.to_string()),
91 output_file1: Option::None,
92 output_file2: Option::None,
93 }
94 }
95
96 pub fn from_file(
102 sort: bool,
103 lowercase: bool,
104 separators: Vec<char>,
105 filepath: PathBuf,
106 ) -> Config {
107 Config {
108 sort,
109 lowercase,
110 separators,
111 file: Option::Some(filepath),
112 file1: Option::None,
113 file2: Option::None,
114 line1: Option::None,
115 line2: Option::None,
116 output_file1: Option::None,
117 output_file2: Option::None,
118 }
119 }
120}
121
122struct LineData {
123 name: String,
124 line: String,
125 preprocessed: String,
126}
127
128impl LineData {
129 fn new(name: &str, line: &str) -> LineData {
130 LineData {
131 name: name.to_string(),
132 line: line.to_string(),
133 preprocessed: "".to_string(),
134 }
135 }
136
137 fn length(&self) -> usize {
138 self.line.chars().count()
139 }
140
141 fn number_chunks(&self) -> usize {
142 self.preprocessed.matches('\n').count() + 1
143 }
144
145 fn preprocess_chunks(&mut self, separator: &[char], sort: bool, lowercase: bool) {
146 let case_adjusted = if lowercase {
147 self.line.to_lowercase()
148 } else {
149 self.line.to_owned()
150 };
151 let mut chunks: Vec<&str> = case_adjusted.split(|c| separator.contains(&c)).collect();
152 if sort {
153 chunks.sort_unstable();
154 }
155 self.preprocessed = chunks.join("\n");
156 }
157}
158
159fn verify_existing_file(path: &Path) -> Result<()> {
160 if !path.exists() {
161 Err(format!("Cannot find file1: {}", path.display()).into())
162 } else if !path.is_file() {
163 Err(format!("Is not a file: {}", path.display()).into())
164 } else {
165 Ok(())
166 }
167}
168
169fn get_lines_from_file(path: &Path) -> Result<LineData> {
170 verify_existing_file(path)?;
171
172 let file = File::open(path)?;
173 let mut reader = BufReader::new(file);
174
175 let mut s = "".to_owned();
176 reader.read_to_string(&mut s)?;
177
178 let file_name = if let Some(file_name) = path.file_name() {
179 if let Ok(file_name) = file_name.to_os_string().into_string() {
180 file_name
181 } else {
182 "".into()
183 }
184 } else {
185 "".into()
186 };
187 Ok(LineData::new(&file_name, &s))
188}
189
190fn get_two_lines_from_file(path: &Path) -> Result<(LineData, LineData)> {
191 verify_existing_file(path)?;
192
193 let file = File::open(path).unwrap();
194 let reader = BufReader::new(file);
195
196 let mut s1 = "".to_owned();
197 let mut s2 = "".to_owned();
198 for (index, line) in reader.lines().enumerate() {
199 let line = line.unwrap();
200
201 if index == 0 {
202 s1 = line.to_owned();
203 } else if index == 1 {
204 s2 = line.to_owned();
205 } else {
206 println!("File contains additional lines that will be ignored");
207 break;
208 }
209 }
210 Ok((LineData::new("Line 1", &s1), LineData::new("Line 2", &s2)))
211}
212
213fn get_line_from_cmd(line_number: i32) -> LineData {
214 println!("Please provide line #{}: ", line_number);
215 let mut buffer = String::new();
216 std::io::stdin().read_line(&mut buffer).expect("");
217 LineData::new(&format!("Line {}", line_number), buffer.trim())
218}
219
220fn get_line(line_number: i32, filepath: Option<PathBuf>) -> Result<LineData> {
221 match filepath {
222 Some(filepath) => get_lines_from_file(&filepath),
223 None => Ok(get_line_from_cmd(line_number)),
224 }
225}
226
227fn print_results(l1: &LineData, l2: &LineData, diffs: Vec<Difference>) {
228 let mut table = Table::new();
229 table.set_format(*format::consts::FORMAT_BOX_CHARS);
230 table.add_row(prettytable::row![bFgc => l1.name, "Same", l2.name]);
231 let iterator = diffs.iter();
232 let mut row_index = 0;
233 let mut previous: Option<String> = None;
234 let column_width = (termwidth() - 8) / 3;
235
236 for d in iterator {
237 match d {
238 Same(line) => {
239 previous = None;
240 table.add_row(row!["", fill(line, column_width), ""])
241 }
242 Add(line) => {
243 if let Some(previous_line) = previous {
244 table.remove_row(row_index);
245 row_index -= 1;
246 let new_row = table.add_row(row![
247 fill(&previous_line, column_width),
248 "",
249 fill(line, column_width)
250 ]);
251 previous = None;
252 new_row
253 } else {
254 previous = None;
255 table.add_row(row!["", "", fill(line, column_width)])
256 }
257 }
258 Rem(line) => {
259 previous = Some(line.to_string());
260 table.add_row(row![fill(line, 18), "", ""])
261 }
262 };
263 row_index += 1;
264 }
265 table.add_row(row![bFgc => l1.length(), "Characters", l2.length()]);
266 table.add_row(row![bFgc => l1.number_chunks(), "Chunks", l2.number_chunks()]);
267 table.printstd();
268}
269
270fn write_output(file: Option<PathBuf>, content: &str) {
271 if let Some(file) = &file {
272 match File::create(file) {
273 Ok(mut file) => {
274 if let Err(error) = file.write_all(content.as_bytes()) {
275 println!("couldn't write to {:?}: {:?}", file, error)
276 }
277 }
278 Err(error) => println!("couldn't write to {:?}: {:?}", file, error),
279 }
280 }
281}
282
283pub fn compare_lines(config: Config) -> Result<()> {
287 let (mut s1, mut s2) = if let Some(filepath) = config.file {
288 verify_existing_file(&filepath)?;
289 get_two_lines_from_file(&filepath)?
290 } else {
291 let l1 = if let Some(l1) = config.line1 {
292 LineData::new("Line 1", &l1)
293 } else {
294 get_line(1, config.file1)?
295 };
296 let l2 = if let Some(l2) = config.line2 {
297 LineData::new("Line 2", &l2)
298 } else {
299 get_line(2, config.file2)?
300 };
301 (l1, l2)
302 };
303
304 s1.preprocess_chunks(&config.separators, config.sort, config.lowercase);
308 s2.preprocess_chunks(&config.separators, config.sort, config.lowercase);
309
310 write_output(config.output_file1, &s1.preprocessed);
311 write_output(config.output_file2, &s2.preprocessed);
312
313 let changeset = Changeset::new(&s1.preprocessed, &s2.preprocessed, "\n");
314 print_results(&s1, &s2, changeset.diffs);
315 Ok(())
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 #[test]
322 fn preprocess_no_sorting() {
323 let mut data = LineData::new("Line 1", "hello world");
324 data.preprocess_chunks(&vec![' '], false, false);
325 assert_eq!("hello\nworld", data.preprocessed);
326
327 let mut data = LineData::new("Line 1", "hello world");
328 data.preprocess_chunks(&vec![';'], false, false);
329 assert_eq!("hello world", data.preprocessed);
330
331 let mut data = LineData::new("Line 1", "hello world");
332 data.preprocess_chunks(&vec!['o'], false, false);
333 assert_eq!("hell\n w\nrld", data.preprocessed);
334 }
335
336 #[test]
337 fn preprocess_lowercase() {
338 let mut data = LineData::new("Line 1", "hello world");
339 data.preprocess_chunks(&vec![' '], false, true);
340 assert_eq!("hello\nworld", data.preprocessed);
341
342 let mut data = LineData::new("Line 1", "Hello wOrld");
343 data.preprocess_chunks(&vec![';'], false, true);
344 assert_eq!("hello world", data.preprocessed);
345
346 let mut data = LineData::new("Line 1", "HELLO WORLD");
347 data.preprocess_chunks(&vec!['o'], false, true);
348 assert_eq!("hell\n w\nrld", data.preprocessed);
349 }
350
351 #[test]
352 fn preprocess_sorting() {
353 let mut data = LineData::new("Line 1", "a b c");
354 data.preprocess_chunks(&vec![' '], true, false);
355 assert_eq!("a\nb\nc", data.preprocessed);
356
357 let mut data = LineData::new("Line 1", "c b a");
358 data.preprocess_chunks(&vec![' '], true, false);
359 assert_eq!("a\nb\nc", data.preprocessed);
360 }
361
362 #[test]
363 fn preprocess_multiple_separators() {
364 let mut data = LineData::new("Line 1", "a b;c");
365 data.preprocess_chunks(&vec![' '], true, false);
366 assert_eq!("a\nb;c", data.preprocessed);
367
368 let mut data = LineData::new("Line 1", "c b a");
369 data.preprocess_chunks(&vec![' ', ';'], true, false);
370 assert_eq!("a\nb\nc", data.preprocessed);
371 }
372
373 #[test]
374 fn read_one_line() -> Result<()> {
375 let l1 = get_lines_from_file(Path::new("examples/test.txt"))?;
376 assert_eq!("test.txt", l1.name);
377 assert_eq!("Hello world 1 3 .\nas the %+3^ night", l1.line);
378 Ok(())
379 }
380
381 #[test]
382 fn read_two_lines() -> Result<()> {
383 let (l1, l2) = get_two_lines_from_file(Path::new("examples/test.txt"))?;
384 assert_eq!("Line 1", l1.name);
385 assert_eq!("Line 2", l2.name);
386 assert_eq!("Hello world 1 3 .", l1.line);
387 assert_eq!("as the %+3^ night", l2.line);
388 Ok(())
389 }
390}