#[cfg(all(unix, not(target_env = "musl")))]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use colored::*;
use group_similar::{group_similar, normalize, Config, Threshold};
use std::collections::{BTreeMap, HashMap};
use std::io::{self, Read};
use std::str::FromStr;
use structopt::StructOpt;
#[derive(Debug, Clone, Copy)]
pub enum Metric {
Jaro,
Cosine,
CosinePos,
}
impl FromStr for Metric {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"jaro" => Ok(Metric::Jaro),
"cosine" => Ok(Metric::Cosine),
"cosine-pos" => Ok(Metric::CosinePos),
other => Err(format!(
"metric must be one of: jaro, cosine, cosine-pos; got '{}'",
other
)),
}
}
}
#[derive(Debug, StructOpt)]
#[structopt(
name = "group-similar",
about = "Hierarchical clustering by similarity, with batteries-included string metrics",
setting = structopt::clap::AppSettings::ColoredHelp
)]
pub struct Flags {
#[structopt(long, default_value = "0.3")]
pub threshold: Threshold,
#[structopt(long)]
pub all: bool,
#[structopt(long)]
pub json: bool,
#[structopt(long = "no-normalize")]
pub no_normalize: bool,
#[structopt(long, default_value = "cosine-pos")]
pub metric: Metric,
#[structopt(long = "no-blocked")]
pub no_blocked: bool,
#[structopt(long, default_value = "0.3")]
pub tau: f64,
}
fn read_from_stdin() -> io::Result<String> {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
Ok(buffer)
}
fn main() -> io::Result<()> {
let flags = Flags::from_args();
let stdin = read_from_stdin()?;
let input = stdin.lines().collect::<Vec<_>>();
let mut config: Config<&str> = match flags.metric {
Metric::Jaro => Config::jaro_winkler(flags.threshold.clone()),
Metric::Cosine => Config::token_cosine(&input, flags.threshold.clone()),
Metric::CosinePos => Config::token_cosine_positional(&input, flags.threshold.clone()),
};
if !flags.no_normalize {
config = config.with_normalizer(normalize::default_normalizer());
}
config = if flags.no_blocked {
config.without_blocking()
} else {
config.with_blocking(flags.tau)
};
let results: BTreeMap<&&str, Vec<&&str>> = group_similar(&input, &config);
if flags.json {
println!(
"{}",
serde_json::to_string(
&results
.iter()
.filter(|(_, v)| flags.all || !v.is_empty())
.collect::<HashMap<_, _>>()
)
.unwrap()
);
} else {
for (k, vs) in results.iter().filter(|(_, v)| flags.all || !v.is_empty()) {
println!("{}", k.green().bold());
for v in vs {
println!(" {}", v.dimmed().italic());
}
println!();
}
}
Ok(())
}