leetcode_cli/cmds/
pick.rs

1//! Pick command
2use super::Command;
3use crate::cache::models::Problem;
4use crate::err::Error;
5use async_trait::async_trait;
6use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand};
7/// Abstract pick command
8///
9/// ```sh
10/// leetcode-pick
11/// Pick a problem
12///
13/// USAGE:
14///     leetcode pick [OPTIONS] [id]
15///
16/// FLAGS:
17///     -h, --help       Prints help information
18///     -V, --version    Prints version information
19///
20/// OPTIONS:
21///     -q, --query <query>    Filter questions by conditions:
22///                            Uppercase means negative
23///                            e = easy     E = m+h
24///                            m = medium   M = e+h
25///                            h = hard     H = e+m
26///                            d = done     D = not done
27///                            l = locked   L = not locked
28///                            s = starred  S = not starred
29///
30/// ARGS:
31///     <id>    Problem id
32/// ```
33pub struct PickCommand;
34
35static QUERY_HELP: &str = r#"Filter questions by conditions:
36Uppercase means negative
37e = easy     E = m+h
38m = medium   M = e+h
39h = hard     H = e+m
40d = done     D = not done
41l = locked   L = not locked
42s = starred  S = not starred"#;
43
44#[async_trait]
45impl Command for PickCommand {
46    /// `pick` usage
47    fn usage() -> ClapCommand {
48        ClapCommand::new("pick")
49            .about("Pick a problem")
50            .visible_alias("p")
51            .arg(
52                Arg::new("name")
53                    .short('n')
54                    .long("name")
55                    .value_parser(clap::value_parser!(String))
56                    .help("Problem name")
57                    .num_args(1),
58            )
59            .arg(
60                Arg::new("id")
61                    .value_parser(clap::value_parser!(i32))
62                    .help("Problem id")
63                    .num_args(1),
64            )
65            .arg(
66                Arg::new("plan")
67                    .short('p')
68                    .long("plan")
69                    .num_args(1)
70                    .help("Invoking python scripts to filter questions"),
71            )
72            .arg(
73                Arg::new("query")
74                    .short('q')
75                    .long("query")
76                    .num_args(1)
77                    .help(QUERY_HELP),
78            )
79            .arg(
80                Arg::new("tag")
81                    .short('t')
82                    .long("tag")
83                    .num_args(1)
84                    .help("Filter questions by tag"),
85            )
86            .arg(
87                Arg::new("daily")
88                    .short('d')
89                    .long("daily")
90                    .help("Pick today's daily challenge")
91                    .action(ArgAction::SetTrue),
92            )
93    }
94
95    /// `pick` handler
96    async fn handler(m: &ArgMatches) -> Result<(), Error> {
97        use crate::cache::Cache;
98        use rand::Rng;
99
100        let cache = Cache::new()?;
101        let mut problems = cache.get_problems()?;
102        if problems.is_empty() {
103            cache.download_problems().await?;
104            Self::handler(m).await?;
105            return Ok(());
106        }
107
108        // filtering...
109        // pym scripts
110        #[cfg(feature = "pym")]
111        {
112            if m.contains_id("plan") {
113                let ids = crate::pym::exec(m.get_one::<String>("plan").unwrap_or(&"".to_string()))?;
114                crate::helper::squash(&mut problems, ids)?;
115            }
116        }
117
118        // tag filter
119        if m.contains_id("tag") {
120            let ids = cache
121                .clone()
122                .get_tagged_questions(m.get_one::<String>("tag").unwrap_or(&"".to_string()))
123                .await?;
124            crate::helper::squash(&mut problems, ids)?;
125        }
126
127        // query filter
128        if m.contains_id("query") {
129            let query = m.get_one::<String>("query").ok_or(Error::NoneError)?;
130            crate::helper::filter(&mut problems, query.to_string());
131        }
132
133        let daily = m.get_one::<bool>("daily").unwrap_or(&false);
134        let daily_id = if *daily {
135            Some(cache.get_daily_problem_id().await?)
136        } else {
137            None
138        };
139
140        let fid = match m.contains_id("name") {
141            // check for name specified, or closest name
142            true => {
143                match m.get_one::<String>("name") {
144                    Some(quesname) => closest_named_problem(&problems, quesname).unwrap_or(1),
145                    None => {
146                        // Pick random without specify id
147                        let problem = &problems[rand::thread_rng().gen_range(0..problems.len())];
148                        problem.fid
149                    }
150                }
151            }
152            false => {
153                m.get_one::<i32>("id")
154                    .copied()
155                    .or(daily_id)
156                    .unwrap_or_else(|| {
157                        // Pick random without specify id
158                        let problem = &problems[rand::thread_rng().gen_range(0..problems.len())];
159                        problem.fid
160                    })
161            }
162        };
163
164        let r = cache.get_question(fid).await;
165
166        match r {
167            Ok(q) => println!("{}", q.desc()),
168            Err(e) => {
169                eprintln!("{:?}", e);
170                if let Error::Reqwest(_) = e {
171                    Self::handler(m).await?;
172                }
173            }
174        }
175
176        Ok(())
177    }
178}
179
180// Returns the closest problem according to a scoring algorithm
181// taking into account both the longest common subsequence and the size
182// problem string (to compensate for smaller strings having smaller lcs).
183// Returns None if there are no problems in the problem list
184fn closest_named_problem(problems: &Vec<Problem>, lookup_name: &str) -> Option<i32> {
185    let max_name_size: usize = problems.iter().map(|p| p.name.len()).max()?;
186    // Init table to the max name length of all the problems to share
187    // the same table allocation
188    let mut table: Vec<usize> = vec![0; (max_name_size + 1) * (lookup_name.len() + 1)];
189
190    // this is guaranteed because of the earlier max None propegation
191    assert!(!problems.is_empty());
192    let mut max_score = 0;
193    let mut current_problem = &problems[0];
194    for problem in problems {
195        // In case bug becomes bugged, always return the matching string
196        if problem.name == lookup_name {
197            return Some(problem.fid);
198        }
199
200        let this_lcs = longest_common_subsequence(&mut table, &problem.name, lookup_name);
201        let this_score = this_lcs * (max_name_size - problem.name.len());
202
203        if this_score > max_score {
204            max_score = this_score;
205            current_problem = problem;
206        }
207    }
208
209    Some(current_problem.fid)
210}
211
212// Longest commong subsequence DP approach O(nm) space and time. Table must be at least
213// (text1.len() + 1) * (text2.len() + 1) length or greater and is mutated every call
214fn longest_common_subsequence(table: &mut [usize], text1: &str, text2: &str) -> usize {
215    assert!(table.len() >= (text1.len() + 1) * (text2.len() + 1));
216    let height: usize = text1.len() + 1;
217    let width: usize = text2.len() + 1;
218
219    // initialize base cases to 0
220    for i in 0..height {
221        table[i * width + (width - 1)] = 0;
222    }
223    for j in 0..width {
224        table[((height - 1) * width) + j] = 0;
225    }
226
227    let mut i: usize = height - 1;
228    let mut j: usize;
229    for c0 in text1.chars().rev() {
230        i -= 1;
231        j = width - 1;
232        for c1 in text2.chars().rev() {
233            j -= 1;
234            if c0.to_lowercase().next() == c1.to_lowercase().next() {
235                table[i * width + j] = 1 + table[(i + 1) * width + j + 1];
236            } else {
237                let a = table[(i + 1) * width + j];
238                let b = table[i * width + j + 1];
239                table[i * width + j] = std::cmp::max(a, b);
240            }
241        }
242    }
243    table[0]
244}