use crate::error::Input;
use crate::{eigentrust_with_options, EigenTrustError, EigenTrustOptions};
use crate::{PreTrust, TrustEdge, TrustScores};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct Network {
peers: Vec<String>,
local_trust: Vec<TrustEdge>,
pre_trust: Vec<PreTrust>,
}
impl Network {
pub fn from_csv(local_trust: &str, pre_trust: &str) -> Result<Network, Error> {
let mut peers: Vec<String> = Vec::new();
let mut index: HashMap<String, usize> = HashMap::new();
let mut edges = Vec::new();
{
let mut peer_index = |name: &str| {
if let Some(&i) = index.get(name) {
return i;
}
let i = peers.len();
index.insert(name.to_string(), i);
peers.push(name.to_string());
i
};
for_each_record(local_trust, Input::LocalTrust, 2, |line, fields| {
let error = |kind| Error::new(Input::LocalTrust, line, kind);
let [from, to, rest @ ..] = fields else {
return Err(error(ErrorKind::TooFewFields));
};
let weight = parse_weight(rest.first().copied()).map_err(error)?;
let from = peer_index(from);
let to = peer_index(to);
edges.push(TrustEdge::new(from, to, weight));
Ok(())
})?;
}
let mut seeds = Vec::new();
for_each_record(pre_trust, Input::PreTrust, 1, |line, fields| {
let error = |kind| Error::new(Input::PreTrust, line, kind);
let peer = *index
.get(fields[0])
.ok_or_else(|| error(ErrorKind::UnknownPeer(fields[0].to_string())))?;
let weight = parse_weight(fields.get(1).copied()).map_err(error)?;
seeds.push(PreTrust::new(peer, weight));
Ok(())
})?;
Ok(Network {
peers,
local_trust: edges,
pre_trust: seeds,
})
}
pub fn peers(&self) -> &[String] {
&self.peers
}
pub fn local_trust(&self) -> &[TrustEdge] {
&self.local_trust
}
pub fn pre_trust(&self) -> &[PreTrust] {
&self.pre_trust
}
pub fn eigentrust(&self, options: &EigenTrustOptions) -> Result<TrustScores, EigenTrustError> {
eigentrust_with_options(
self.local_trust.iter().copied(),
self.pre_trust.iter().copied(),
options,
)
}
pub fn ranking<'a>(&'a self, scores: &TrustScores) -> Vec<(&'a str, f64)> {
scores
.ranking()
.into_iter()
.filter_map(|(i, s)| self.peers.get(i).map(|name| (name.as_str(), s)))
.collect()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Error {
input: Input,
line: u64,
kind: ErrorKind,
}
impl Error {
fn new(input: Input, line: u64, kind: ErrorKind) -> Self {
Error { input, line, kind }
}
pub fn input(&self) -> Input {
self.input
}
pub fn line(&self) -> u64 {
self.line
}
pub fn kind(&self) -> &ErrorKind {
&self.kind
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum ErrorKind {
Malformed(String),
TooFewFields,
InvalidWeight(String),
NonFiniteWeight(String),
NegativeWeight(String),
UnknownPeer(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} CSV, line {}: ", self.input, self.line)?;
match &self.kind {
ErrorKind::Malformed(msg) => write!(f, "malformed CSV: {}", msg),
ErrorKind::TooFewFields => f.write_str("too few fields"),
ErrorKind::InvalidWeight(w) => write!(f, "weight {:?} is not a number", w),
ErrorKind::NonFiniteWeight(w) => write!(f, "weight {:?} must be finite", w),
ErrorKind::NegativeWeight(w) => {
write!(f, "weight {:?} is negative; distrust is not supported", w)
}
ErrorKind::UnknownPeer(p) => write!(f, "peer {:?} does not appear in local trust", p),
}
}
}
impl std::error::Error for Error {}
const HEADER_NAMES: &[&str] = &[
"i", "j", "v", "from", "to", "value", "weight", "trust", "level", "peer", "id", "score",
"source", "target", "src", "dst", "truster", "trustee",
];
fn clean_field(field: &str) -> &str {
let field = field.trim();
field
.strip_prefix('"')
.and_then(|f| f.strip_suffix('"'))
.unwrap_or(field)
.trim()
}
fn is_header(fields: &[&str], value_column: usize) -> bool {
match fields.get(value_column) {
Some(value) => value.parse::<f64>().is_err(),
None => fields
.iter()
.all(|f| HEADER_NAMES.contains(&f.to_ascii_lowercase().as_str())),
}
}
fn for_each_record<F>(data: &str, input: Input, value_column: usize, mut f: F) -> Result<(), Error>
where
F: FnMut(u64, &[&str]) -> Result<(), Error>,
{
let data = data.trim_start_matches('\u{feff}');
let mut reader = ::csv::ReaderBuilder::new()
.has_headers(false)
.flexible(true)
.from_reader(data.as_bytes());
let bytes = data.as_bytes();
let mut counted = (0usize, 1u64);
let mut line_at = |byte: u64| {
let mut byte = (byte as usize).min(bytes.len());
while byte < bytes.len() && (bytes[byte] == b'\n' || bytes[byte] == b'\r') {
byte += 1;
}
if byte >= counted.0 {
counted.1 += bytes[counted.0..byte]
.iter()
.filter(|&&b| b == b'\n')
.count() as u64;
counted.0 = byte;
}
counted.1
};
let mut record = ::csv::ByteRecord::new();
let mut first = true;
loop {
let more = match reader.read_byte_record(&mut record) {
Ok(more) => more,
Err(e) => {
let line = e.position().map_or(0, |p| line_at(p.byte()));
return Err(Error::new(input, line, ErrorKind::Malformed(e.to_string())));
}
};
if !more {
return Ok(());
}
let line = record.position().map_or(0, |p| line_at(p.byte()));
let mut buf = [""; 3];
let mut n = 0;
for field in record.iter().take(buf.len()) {
let field = std::str::from_utf8(field)
.map_err(|e| Error::new(input, line, ErrorKind::Malformed(e.to_string())))?;
buf[n] = clean_field(field);
n += 1;
}
let fields = &buf[..n];
if fields.iter().all(|f| f.is_empty()) {
continue;
}
if first {
first = false;
if is_header(fields, value_column) {
continue;
}
}
f(line, fields)?;
}
}
fn parse_weight(field: Option<&str>) -> Result<f64, ErrorKind> {
let Some(field) = field else {
return Ok(1.0);
};
let weight = field
.parse::<f64>()
.map_err(|_| ErrorKind::InvalidWeight(field.to_string()))?;
if !weight.is_finite() {
return Err(ErrorKind::NonFiniteWeight(field.to_string()));
}
if weight < 0.0 {
return Err(ErrorKind::NegativeWeight(field.to_string()));
}
Ok(weight)
}
#[cfg(test)]
mod tests {
use super::*;
fn records(data: &str, value_column: usize) -> Result<Vec<(u64, Vec<String>)>, Error> {
let mut out = vec![];
for_each_record(data, Input::LocalTrust, value_column, |line, f| {
out.push((line, f.iter().map(|s| s.to_string()).collect()));
Ok(())
})?;
Ok(out)
}
fn rows(data: &str, value_column: usize) -> Vec<Vec<String>> {
records(data, value_column)
.unwrap()
.into_iter()
.map(|(_, f)| f)
.collect()
}
#[test]
fn headers() {
assert_eq!(rows("i,j,v\na,b,1\n", 2), [["a", "b", "1"]]);
assert_eq!(rows("i,j,v\r\na,b,1\r\n", 2), [["a", "b", "1"]]);
assert_eq!(rows("\u{feff}from,to,weight\na,b,1", 2), [["a", "b", "1"]]);
assert_eq!(rows("a,b\nb,c", 2), [["a", "b"], ["b", "c"]]);
assert_eq!(rows("i,j\na,b", 2), [["a", "b"]]);
assert_eq!(rows("i,v\nalice,1", 1), [["alice", "1"]]);
assert_eq!(rows("alice\nbob", 1), [["alice"], ["bob"]]);
assert_eq!(rows("peer\nalice", 1), [["alice"]]);
assert!(rows("", 1).is_empty());
}
#[test]
fn quoting() {
let data = "\"Smith, J\",\"O\"\"Neil\",2\n\n a , b , 3 \n \"c\" , \"d\" ,1\n";
assert_eq!(
rows(data, 2),
[
["Smith, J", "O\"Neil", "2"],
["a", "b", "3"],
["c", "d", "1"]
]
);
}
#[test]
fn line_numbers_survive_blank_lines() {
let lines: Vec<u64> = records("i,j,v\na,b,1\n\nc,d,2\r\n\r\n\r\ne,f", 2)
.unwrap()
.into_iter()
.map(|(l, _)| l)
.collect();
assert_eq!(lines, [2, 4, 7]);
}
#[test]
fn weights() {
assert_eq!(parse_weight(None), Ok(1.0));
assert_eq!(parse_weight(Some("2.5")), Ok(2.5));
assert!(matches!(
parse_weight(Some("x")),
Err(ErrorKind::InvalidWeight(_))
));
for bad in ["NaN", "inf", "-inf"] {
assert!(matches!(
parse_weight(Some(bad)),
Err(ErrorKind::NonFiniteWeight(_))
));
}
assert!(matches!(
parse_weight(Some("-1")),
Err(ErrorKind::NegativeWeight(_))
));
}
#[test]
fn clean_fields() {
assert_eq!(clean_field(" alice "), "alice");
assert_eq!(clean_field("\"alice\""), "alice");
assert_eq!(clean_field(" \" 0.5 \" "), "0.5");
assert_eq!(clean_field("\""), "\"");
}
}