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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//! z_rs implements a directory jumping command line utility
//!
//! The original is a 200 line shell classic
//! https://github.com/rupa/z/blob/master/z.sh
//!
//! This is an **opinionated** rewrite instead of being 1:1
//! with the original. Most notably, this does not override
//! `cd` in any way, and needs to be used instead of `cd` to
//! build the cache.
//!
//! This provides a `z_rs` binary, which can be invoked
//! for configuration and lookup.
//!
//! ## Installation
//!
//! Currently unpublished, do
//!
//! ```sh
//! cargo install https://github.com/suyash/z-rs
//! ```
//!
//! ## Configuration
//!
//! add
//!
//! ```sh
//! eval "$(z_rs init --cmd z --cache $HOME/.z_rs)"
//! ```
//!
//! in your shell configuration file.
//!
//! The `--cmd` option is the name of the command that will
//! be added to the shell. For example, in this case, a `z`
//! command will be created.
//!
//! ## Usage
//!
//! If configured with the command name `z`, then simply do
//!
//! ```sh
//! z foo bar
//! ```
//!
//! will try to match `foo` and `bar` and if a match is found
//! in the cache, will `cd` to it, otherwise error out.
//!
//! Matches occur based on "frecency". The rank of a path is
//! how many times it has been accessed. The command will return
//! the highest ranked matching record. If there are multiple,
//! it will return the most recently accessed one.

#![deny(missing_docs)]

use std::collections::HashMap;

use bstr::ByteSlice;
use serde::{Deserialize, Serialize};

#[cfg(test)]
mod tests;

#[derive(Serialize, Deserialize, Debug)]
/// Records contains a record of all stored values
pub struct Records {
    map: HashMap<Vec<u8>, (f64, u64)>,
    sum: f64,
}

impl Records {
    /// new creates a new Records instance
    pub fn new() -> Records {
        Self::default()
    }

    /// update increments the rank of the provided path
    /// and updates its timestamp
    pub fn update<T>(&mut self, path: T, ts: u64)
    where
        T: AsRef<[u8]>,
    {
        let v = self.map.entry(path.as_ref().to_owned()).or_insert((0.0, 0));
        v.0 += 1.0;
        v.1 = ts;

        self.sum += 1.0;

        // TODO: implement pruning and reorging
    }

    /// query matches a record from the saved records from the given
    /// arguments. The rank of a record is how many times it has been used.
    /// The match is returned based on "frecency", i.e.
    /// the highest ranked matching record is given priority,
    /// if there are multiple then the most recently accessed record
    /// with the same rank is returned.
    pub fn query<T>(&self, arguments: &[T]) -> Option<Vec<u8>>
    where
        T: AsRef<[u8]>,
    {
        let mut ans = None;

        // NOTE: since rust hash maps are swiss tables now,
        // assuming straight up iteration is a feasible op
        for (path, (score, timestamp)) in self.map.iter() {
            let mut index = 0;

            for argument in arguments {
                match path[index..].find(argument) {
                    None => {
                        index = path.len() + 1;
                        break;
                    }
                    Some(fix) => {
                        index += fix + argument.as_ref().len();
                    }
                }
            }

            if index <= path.len() {
                ans = match ans {
                    None => Some((path, (score, timestamp))),
                    Some((ppath, (pscore, ptimestamp))) => {
                        if score > pscore
                            || ((score - pscore).abs() < std::f64::EPSILON
                                && timestamp > ptimestamp)
                        {
                            Some((path, (score, timestamp)))
                        } else {
                            Some((ppath, (pscore, ptimestamp)))
                        }
                    }
                };
            }
        }

        match ans {
            None => None,
            Some((path, _)) => Some(path.clone()),
        }
    }

    /// query_and_update runs a query followed by an update
    /// with the result of the query.
    pub fn query_and_update<T>(&mut self, arguments: &[T], ts: u64) -> Option<Vec<u8>>
    where
        T: AsRef<[u8]>,
    {
        match self.query(arguments) {
            None => None,
            Some(v) => {
                self.update(&v, ts);
                Some(v)
            }
        }
    }
}

impl Default for Records {
    fn default() -> Self {
        Records {
            map: HashMap::new(),
            sum: 0.0,
        }
    }
}