line_diff/
line_diff.rs

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/// Configuration struct for comparing two lines
18#[derive(StructOpt, Debug)]
19#[structopt(author, about)]
20pub struct Config {
21    /// Whether or not the chunks should be sorted before comparing.
22    #[structopt(short = "o", long)]
23    sort: bool,
24
25    /// Whether or not to convert the chunks to lowercase before comparing
26    #[structopt(short, long)]
27    lowercase: bool,
28
29    /// Separator for splitting lines. It is possible to define multiple separators.
30    /// Newline is always a separator
31    #[structopt(short, long, default_value = " ")]
32    separators: Vec<char>,
33
34    /// A single file containing two lines. Additional lines will be ignored.
35    #[structopt(short, long, parse(from_os_str))]
36    file: Option<PathBuf>,
37
38    /// Path to file containing the first line. The complete file will be processed.
39    file1: Option<PathBuf>,
40
41    /// Path to file containing the second line. The complete file will be processed.
42    file2: Option<PathBuf>,
43
44    /// First line as string
45    #[structopt(long)]
46    line1: Option<String>,
47
48    /// Second line as string
49    #[structopt(long)]
50    line2: Option<String>,
51
52    /// File to write the first line after preprocessing to
53    #[structopt(long, short = "m")]
54    output_file1: Option<PathBuf>,
55
56    /// File to write the second line after preprocessing to
57    #[structopt(long, short = "n")]
58    output_file2: Option<PathBuf>,
59}
60
61impl Config {
62    /// Create a config struct by using command line arguments
63    pub fn from_cmd_args() -> Config {
64        let mut c = Config::from_args();
65        c.separators.push('\n');
66        c
67    }
68
69    /// Create a Config struct that can be used to compare two lines that are given as &str
70    /// * `sort` Whether or not to sort chunks before comparing
71    /// * `lowercase` Whether or not to convert chunks to lowercase before comparing
72    /// * `separators` List of separators to use for splitting lines into chunks
73    /// * `l1` The first line
74    /// * `l2` The second line
75    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    /// Create a Config struct that can be used to compare two lines that are stored in a single file
97    /// * `sort` Whether or not to sort chunks before comparing
98    /// * `lowercase` Whether or not to convert chunks to lowercase before comparing
99    /// * `separators` List of separators to use for splitting lines into chunks
100    /// * `filepath` Path to the file that contains the two lines
101    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
283/// Comapare two lines with given configuration.
284///
285/// * `config` - Configuration
286pub 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    //println!("{}: \n{}", s1.name, s1.line);
305    //println!("{}: \n{}", s2.name, s2.line);
306
307    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}