leetcode_cli/cmd/
pick.rs

1//! Pick command
2use crate::cache::models::Problem;
3use crate::err::Error;
4use clap::Args;
5
6static QUERY_HELP: &str = r#"Filter questions by conditions:
7Uppercase means negative
8e = easy     E = m+h
9m = medium   M = e+h
10h = hard     H = e+m
11d = done     D = not done
12l = locked   L = not locked
13s = starred  S = not starred"#;
14
15/// Pick command arguments
16#[derive(Args)]
17pub struct PickArgs {
18    /// Problem id
19    #[arg(value_parser = clap::value_parser!(i32))]
20    pub id: Option<i32>,
21
22    /// Problem name
23    #[arg(short = 'n', long)]
24    pub name: Option<String>,
25
26    /// Invoking python scripts to filter questions
27    #[arg(short = 'p', long)]
28    pub plan: Option<String>,
29
30    /// Filter questions by conditions
31    #[arg(short, long, help = QUERY_HELP)]
32    pub query: Option<String>,
33
34    /// Filter questions by tag
35    #[arg(short, long)]
36    pub tag: Option<String>,
37
38    /// Pick today's daily challenge
39    #[arg(short = 'd', long)]
40    pub daily: bool,
41}
42
43impl PickArgs {
44    /// `pick` handler
45    pub async fn run(&self) -> Result<(), Error> {
46        use crate::cache::Cache;
47        use rand::Rng;
48
49        let cache = Cache::new()?;
50        let mut problems = cache.get_problems()?;
51        if problems.is_empty() {
52            cache.download_problems().await?;
53            return Box::pin(self.run()).await;
54        }
55
56        // filtering...
57        // pym scripts
58        #[cfg(feature = "pym")]
59        {
60            if let Some(ref plan) = self.plan {
61                let ids = crate::pym::exec(plan)?;
62                crate::helper::squash(&mut problems, ids)?;
63            }
64        }
65
66        // tag filter
67        if let Some(ref tag) = self.tag {
68            let ids = cache.clone().get_tagged_questions(tag).await?;
69            crate::helper::squash(&mut problems, ids)?;
70        }
71
72        // query filter
73        if let Some(ref query) = self.query {
74            crate::helper::filter(&mut problems, query.to_string());
75        }
76
77        let daily_id = if self.daily {
78            Some(cache.get_daily_problem_id().await?)
79        } else {
80            None
81        };
82
83        let fid = if let Some(ref quesname) = self.name {
84            // check for name specified, or closest name
85            closest_named_problem(&problems, quesname).unwrap_or(1)
86        } else {
87            self.id.or(daily_id).unwrap_or_else(|| {
88                // Pick random without specify id
89                let problem = &problems[rand::rng().random_range(0..problems.len())];
90                problem.fid
91            })
92        };
93
94        let r = cache.get_question(fid).await;
95
96        match r {
97            Ok(q) => println!("{}", q.desc()),
98            Err(e) => {
99                eprintln!("{:?}", e);
100                if let Error::Reqwest(_) = e {
101                    Box::pin(self.run()).await?;
102                }
103            }
104        }
105
106        Ok(())
107    }
108}
109
110// Returns the closest problem according to a scoring algorithm
111// taking into account both the longest common subsequence and the size
112// problem string (to compensate for smaller strings having smaller lcs).
113// Returns None if there are no problems in the problem list
114fn closest_named_problem(problems: &Vec<Problem>, lookup_name: &str) -> Option<i32> {
115    let max_name_size: usize = problems.iter().map(|p| p.name.len()).max()?;
116    // Init table to the max name length of all the problems to share
117    // the same table allocation
118    let mut table: Vec<usize> = vec![0; (max_name_size + 1) * (lookup_name.len() + 1)];
119
120    // this is guaranteed because of the earlier max None propegation
121    assert!(!problems.is_empty());
122    let mut max_score = 0;
123    let mut current_problem = &problems[0];
124    for problem in problems {
125        // In case bug becomes bugged, always return the matching string
126        if problem.name == lookup_name {
127            return Some(problem.fid);
128        }
129
130        let this_lcs = longest_common_subsequence(&mut table, &problem.name, lookup_name);
131        let this_score = this_lcs * (max_name_size - problem.name.len());
132
133        if this_score > max_score {
134            max_score = this_score;
135            current_problem = problem;
136        }
137    }
138
139    Some(current_problem.fid)
140}
141
142// Longest commong subsequence DP approach O(nm) space and time. Table must be at least
143// (text1.len() + 1) * (text2.len() + 1) length or greater and is mutated every call
144fn longest_common_subsequence(table: &mut [usize], text1: &str, text2: &str) -> usize {
145    assert!(table.len() >= (text1.len() + 1) * (text2.len() + 1));
146    let height: usize = text1.len() + 1;
147    let width: usize = text2.len() + 1;
148
149    // initialize base cases to 0
150    for i in 0..height {
151        table[i * width + (width - 1)] = 0;
152    }
153    for j in 0..width {
154        table[((height - 1) * width) + j] = 0;
155    }
156
157    let mut i: usize = height - 1;
158    let mut j: usize;
159    for c0 in text1.chars().rev() {
160        i -= 1;
161        j = width - 1;
162        for c1 in text2.chars().rev() {
163            j -= 1;
164            if c0.to_lowercase().next() == c1.to_lowercase().next() {
165                table[i * width + j] = 1 + table[(i + 1) * width + j + 1];
166            } else {
167                let a = table[(i + 1) * width + j];
168                let b = table[i * width + j + 1];
169                table[i * width + j] = std::cmp::max(a, b);
170            }
171        }
172    }
173    table[0]
174}