1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
45use std::cmp::max;
67use crate::interest::{Interest, InterestVector};
89/// Calculate score for a piece of categorized content based on a user interest vector.
10///
11/// This scoring function is of the following properties:
12/// - The score ranges from 0.0 to 1.0
13/// - The score is monotonically increasing for the accumulated interest count
14///
15/// # Params:
16/// - `interest_vector`: a user interest vector that can be fetched via
17/// `RelevancyStore::user_interest_vector()`.
18/// - `content_categories`: a list of categories (interests) of the give content.
19/// # Return:
20/// - A score ranges in [0, 1].
21#[uniffi::export]
22pub fn score(interest_vector: InterestVector, content_categories: Vec<Interest>) -> f64 {
23let n = content_categories
24 .iter()
25 .fold(0, |acc, &category| acc + interest_vector[category]);
2627// Apply base 10 logarithm to the accumulated count so its hyperbolic tangent is more
28 // evenly distributed in [0, 1]. Note that `max(n, 1)` is used to avoid negative scores.
29(max(n, 1) as f64).log10().tanh()
30}
3132#[cfg(test)]
33mod test {
34use crate::interest::{Interest, InterestVector};
3536use super::*;
3738const EPSILON: f64 = 1e-10;
39const SUBEPSILON: f64 = 1e-6;
4041#[test]
42fn test_score_lower_bound() {
43// Empty interest vector yields score 0.
44let s = score(InterestVector::default(), vec![Interest::Food]);
45let delta = (s - 0_f64).abs();
4647assert!(delta < EPSILON);
4849// No overlap also yields score 0.
50let s = score(
51 InterestVector {
52 animals: 10,
53 ..InterestVector::default()
54 },
55vec![Interest::Food],
56 );
57let delta = (s - 0_f64).abs();
5859assert!(delta < EPSILON);
60 }
6162#[test]
63fn test_score_upper_bound() {
64let score = score(
65 InterestVector {
66 animals: 1_000_000_000,
67 ..InterestVector::default()
68 },
69vec![Interest::Animals],
70 );
71let delta = (score - 1.0_f64).abs();
7273// Can get very close to the upper bound 1.0 but not over.
74assert!(delta < SUBEPSILON);
75 }
7677#[test]
78fn test_score_monotonic() {
79let l = score(
80 InterestVector {
81 animals: 1,
82 ..InterestVector::default()
83 },
84vec![Interest::Animals],
85 );
8687let r = score(
88 InterestVector {
89 animals: 5,
90 ..InterestVector::default()
91 },
92vec![Interest::Animals],
93 );
9495assert!(l < r);
96 }
9798#[test]
99fn test_score_multi_categories() {
100let l = score(
101 InterestVector {
102 animals: 100,
103 food: 100,
104 ..InterestVector::default()
105 },
106vec![Interest::Animals, Interest::Food],
107 );
108109let r = score(
110 InterestVector {
111 animals: 200,
112 ..InterestVector::default()
113 },
114vec![Interest::Animals],
115 );
116let delta = (l - r).abs();
117118assert!(delta < EPSILON);
119 }
120}