1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
use nu_cmd_base::input_handler::{operate, CmdArgument};
use nu_engine::CallExt;
use nu_protocol::{
    ast::{Call, CellPath},
    engine::{Command, EngineState, Stack},
    levenshtein_distance, record, Category, Example, PipelineData, ShellError, Signature, Span,
    SyntaxShape, Type, Value,
};

#[derive(Clone)]
pub struct SubCommand;

struct Arguments {
    compare_string: String,
    cell_paths: Option<Vec<CellPath>>,
}

impl CmdArgument for Arguments {
    fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
        self.cell_paths.take()
    }
}

impl Command for SubCommand {
    fn name(&self) -> &str {
        "str distance"
    }

    fn signature(&self) -> Signature {
        Signature::build("str distance")
            .input_output_types(vec![
                (Type::String, Type::Int),
                (Type::Table(vec![]), Type::Table(vec![])),
                (Type::Record(vec![]), Type::Record(vec![])),
            ])
            .required(
                "compare-string",
                SyntaxShape::String,
                "The first string to compare.",
            )
            .rest(
                "rest",
                SyntaxShape::CellPath,
                "For a data structure input, check strings at the given cell paths, and replace with result.",
            )
            .category(Category::Strings)
    }

    fn usage(&self) -> &str {
        "Compare two strings and return the edit distance/Levenshtein distance."
    }

    fn search_terms(&self) -> Vec<&str> {
        vec!["edit", "levenshtein"]
    }

    fn run(
        &self,
        engine_state: &EngineState,
        stack: &mut Stack,
        call: &Call,
        input: PipelineData,
    ) -> Result<PipelineData, ShellError> {
        let compare_string: String = call.req(engine_state, stack, 0)?;
        let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 1)?;
        let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
        let args = Arguments {
            compare_string,
            cell_paths,
        };
        operate(action, args, input, call.head, engine_state.ctrlc.clone())
    }

    fn examples(&self) -> Vec<Example> {
        vec![Example {
            description: "get the edit distance between two strings",
            example: "'nushell' | str distance 'nutshell'",
            result: Some(Value::test_int(1)),
        },
        Example {
            description: "Compute edit distance between strings in table and another string, using cell paths",
            example: "[{a: 'nutshell' b: 'numetal'}] | str distance 'nushell' 'a' 'b'",
            result: Some(Value::test_list (
                vec![
                    Value::test_record(record! {
                        "a" => Value::test_int(1),
                        "b" => Value::test_int(4),
                    })])),
        },
        Example {
            description: "Compute edit distance between strings in record and another string, using cell paths",
            example: "{a: 'nutshell' b: 'numetal'} | str distance 'nushell' a b",
            result: Some(
                    Value::test_record(record! {
                        "a" => Value::test_int(1),
                        "b" => Value::test_int(4),
                    })),
        }]
    }
}

fn action(input: &Value, args: &Arguments, head: Span) -> Value {
    let compare_string = &args.compare_string;
    match input {
        Value::String { val, .. } => {
            let distance = levenshtein_distance(val, compare_string);
            Value::int(distance as i64, head)
        }
        Value::Error { .. } => input.clone(),
        _ => Value::error(
            ShellError::OnlySupportsThisInputType {
                exp_input_type: "string".into(),
                wrong_type: input.get_type().to_string(),
                dst_span: head,
                src_span: input.span(),
            },
            head,
        ),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_examples() {
        use crate::test_examples;

        test_examples(SubCommand {})
    }
}