farmap 0.10.1

A library for working with Farcaster label datasets
Documentation
#![allow(refining_impl_trait)]
use crate::collidable::Collidable;
use crate::dated::Dated;
use crate::has_tag::HasTag;
use crate::user_value::AnyUserValue;
use crate::user_value::UserValue;
use crate::user_value::UserValueSeal;
use crate::{user::InvalidInputError, utils::distribution_from_counts, UnprocessedUserLine};
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum SpamScore {
    Zero,
    One,
    Two,
}

pub type SpamScoreWithSourceCommit = (SpamScore, CommitHash);
pub type SpamRecord = (SpamScore, NaiveDate);
pub type SpamRecordWithSourceCommit = ((SpamScore, NaiveDate), CommitHash);
pub type DatedSpamScoreCount = Dated<SpamScoreCount>;
pub type DatedSpamScoreDistribution = Dated<SpamScoreDistribution>;
pub type DatedSpamUpdate = Dated<SpamUpdate>;
pub type DatedSpamUpdateWithFid = Dated<SpamUpdateWithFid>;
pub type SpamUpdateWithFid = (SpamUpdate, u64);

impl HasTag<u64> for SpamUpdateWithFid {
    fn tag(&self) -> u64 {
        self.1
    }

    fn untag(self) -> (SpamUpdate, u64) {
        (self.0, self.1)
    }
}

impl HasTag<(NaiveDate, u64)> for DatedSpamUpdateWithFid {
    fn tag(&self) -> (NaiveDate, u64) {
        (self.date(), self.as_inner().1)
    }

    fn untag(self) -> (SpamUpdate, (NaiveDate, u64)) {
        (self.into_inner().into(), (self.date(), self.as_inner().1))
    }
}

impl From<SpamUpdateWithFid> for SpamUpdate {
    fn from(value: SpamUpdateWithFid) -> SpamUpdate {
        value.0
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct SpamScoreCount {
    nonspam: u64,
    maybe: u64,
    spam: u64,
}

impl SpamScoreCount {
    pub fn new(spam_count: u64, maybe_count: u64, nonspam_count: u64) -> Self {
        Self {
            spam: spam_count,
            maybe: maybe_count,
            nonspam: nonspam_count,
        }
    }

    pub fn add(&mut self, score: SpamScore) {
        match score {
            SpamScore::Zero => self.spam += 1,
            SpamScore::One => self.maybe += 1,
            SpamScore::Two => self.nonspam += 1,
        }
    }

    pub fn total(&self) -> u64 {
        self.spam + self.maybe + self.nonspam
    }

    pub fn spam(&self) -> u64 {
        self.spam
    }

    pub fn maybe_spam(&self) -> u64 {
        self.maybe
    }

    pub fn non_spam(&self) -> u64 {
        self.nonspam
    }
}

impl From<[u64; 3]> for SpamScoreCount {
    fn from(value: [u64; 3]) -> Self {
        Self {
            spam: value[0],
            maybe: value[1],
            nonspam: value[2],
        }
    }
}

impl From<SpamScoreCount> for [u64; 3] {
    fn from(value: SpamScoreCount) -> Self {
        [value.spam, value.maybe, value.nonspam]
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct SpamScoreDistribution {
    nonspam: f32,
    maybe: f32,
    spam: f32,
}

impl SpamScoreDistribution {
    pub fn spam(&self) -> f32 {
        self.spam
    }

    pub fn maybe_spam(&self) -> f32 {
        self.maybe
    }

    pub fn non_spam(&self) -> f32 {
        self.nonspam
    }
}

#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Hash)]
pub struct CommitHash(u32);

impl TryFrom<String> for CommitHash {
    type Error = InvalidHashError;

    fn try_from(full_commit_value: String) -> Result<Self, Self::Error> {
        if full_commit_value.len() != 40 {
            return Err(InvalidHashError(full_commit_value));
        };

        let shortened_commit = full_commit_value.chars().take(4).collect::<String>();
        let result = u32::from_str_radix(&shortened_commit, 16)
            .map_err(|_| InvalidHashError(full_commit_value))?;
        Ok(CommitHash(result))
    }
}

#[derive(Error, Debug)]
#[error("invalid hash: {0}")]
pub struct InvalidHashError(String);

impl TryFrom<usize> for SpamScore {
    type Error = InvalidInputError;

    fn try_from(value: usize) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(Self::Zero),
            1 => Ok(Self::One),
            2 => Ok(Self::Two),
            _ => Err(InvalidInputError::SpamScoreError { label: value }),
        }
    }
}

impl From<SpamScoreDistribution> for [f32; 3] {
    fn from(value: SpamScoreDistribution) -> Self {
        [value.spam, value.maybe, value.nonspam]
    }
}

impl TryFrom<SpamScoreCount> for SpamScoreDistribution {
    type Error = EmptyScoreCountError;
    fn try_from(value: SpamScoreCount) -> Result<Self, Self::Error> {
        let distribution =
            distribution_from_counts::<3>(&value.into()).ok_or(EmptyScoreCountError)?;
        Ok(SpamScoreDistribution {
            nonspam: distribution[2],
            maybe: distribution[1],
            spam: distribution[0],
        })
    }
}

#[derive(Debug, Error)]
#[error("trying to create a spam score distribution from an empty SpamScoreCount")]
pub struct EmptyScoreCountError;

#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone, Copy, Hash)]
pub enum SpamUpdate {
    WithSourceCommit(SpamScoreWithSourceCommit),
    WithoutSourceCommit(SpamScore),
}

impl SpamUpdate {
    pub fn score(&self) -> SpamScore {
        match self {
            Self::WithSourceCommit(x) => x.0,
            Self::WithoutSourceCommit(x) => *x,
        }
    }
}

impl From<SpamScore> for SpamUpdate {
    fn from(value: SpamScore) -> Self {
        Self::WithoutSourceCommit(value)
    }
}

impl UserValue for SpamUpdate {
    fn as_any_user_value(&self) -> AnyUserValue {
        AnyUserValue::SpamScore(self.score())
    }

    fn into_any_user_value(self) -> AnyUserValue {
        AnyUserValue::SpamScore(self.score())
    }

    fn from_any_user_value(any_user_value: AnyUserValue) -> Option<Self> {
        match any_user_value {
            AnyUserValue::SpamUpdate(x) => Some(x),
            _ => None,
        }
    }

    fn from_any_user_value_ref(any_user_value: &AnyUserValue) -> Option<&Self> {
        match any_user_value {
            AnyUserValue::SpamUpdate(x) => Some(x),
            _ => None,
        }
    }
}

impl UserValueSeal for SpamUpdate {}

impl UserValue for DatedSpamUpdate {
    fn as_any_user_value(&self) -> AnyUserValue {
        AnyUserValue::DatedSpamUpdate(*self)
    }

    fn into_any_user_value(self) -> AnyUserValue {
        AnyUserValue::DatedSpamUpdate(self)
    }

    fn from_any_user_value(any_user_value: AnyUserValue) -> Option<Self> {
        match any_user_value {
            AnyUserValue::DatedSpamUpdate(x) => Some(x),
            _ => None,
        }
    }

    fn from_any_user_value_ref(any_user_value: &AnyUserValue) -> Option<&Self> {
        match any_user_value {
            AnyUserValue::DatedSpamUpdate(x) => Some(x),
            _ => None,
        }
    }
}

impl From<(SpamScore, NaiveDate)> for DatedSpamUpdate {
    fn from(value: (SpamScore, NaiveDate)) -> Self {
        let spam_update: SpamUpdate = value.0.into();
        Dated::from(value.1, spam_update)
    }
}

impl UserValueSeal for DatedSpamUpdate {}

impl Collidable for DatedSpamUpdate {
    fn is_collision(&self, other: &Self) -> bool {
        self.date() == other.date() && self.score() != other.score()
    }
}

impl HasTag<u64> for DatedSpamUpdateWithFid {
    fn untag(self) -> (DatedSpamUpdate, u64) {
        let date = self.date();
        let spam_update = self.into_inner();
        let fid = spam_update.1;
        let res = DatedSpamUpdate::from(date, spam_update);
        (res, fid)
    }

    fn tag(&self) -> u64 {
        self.1
    }
}

impl TryFrom<UnprocessedUserLine> for DatedSpamUpdateWithFid {
    type Error = InvalidInputError;

    fn try_from(value: UnprocessedUserLine) -> Result<Self, Self::Error> {
        let spam_score = SpamScore::try_from(value.label_value())?;
        let spam_update = SpamUpdate::from(spam_score);
        let spam_update_with_fid = (spam_update, value.fid() as u64);
        let date = value.date()?;
        Ok(DatedSpamUpdateWithFid::from(date, spam_update_with_fid))
    }
}

#[derive(Error, Debug)]
#[error("trying to create a SpamEntries from an empty struct")]
pub struct EmptyEntriesError;

#[cfg(test)]
pub mod tests {
    use super::*;

    pub fn basic_spam_score_count() -> DatedSpamScoreCount {
        let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
        DatedSpamScoreCount::from(date, [100, 150, 200])
    }

    #[test]
    pub fn test_label_value_invalid_input() {
        assert!(SpamScore::try_from(0).is_ok());
        assert!(SpamScore::try_from(1).is_ok());
        assert!(SpamScore::try_from(2).is_ok());
        assert!(SpamScore::try_from(3).is_err());
        assert!(SpamScore::try_from(100).is_err());
    }

    #[test]
    pub fn basic_spam_score_count_test() {
        let count = basic_spam_score_count();
        assert_eq!(count.spam(), 100);
        assert_eq!(count.maybe_spam(), 150);
        assert_eq!(count.non_spam(), 200);
        assert_eq!(count.total(), 100 + 150 + 200);
    }
}