Skip to main content

wikipedia_infobox_analyzer/
lib.rs

1#[doc = include_str!("../README.md")]
2pub mod wikipedia_infobox_analyzer {
3    use regex::Regex;
4    use itertools::Itertools;
5
6    /// Looks for a used template that does lists wikidata properties listing
7    pub fn extract_used_properties_from_template(template: String) -> Vec<u64> {
8        let mut lines = template.lines();
9        let used = lines
10            .find(|line| line.starts_with("{{") && line.contains("Wikidata|"))
11            .expect("Template should have a line declaring which properties are used");
12
13        // The properties are listed inside the template with their number: P1|P2|P...
14        let re = Regex::new(r"P(\d+)").unwrap();
15
16        // Find all matches and collect the properties into vector
17        re.captures_iter(used)
18          .map(|m| m[1].parse().unwrap())
19          .collect()
20    }
21
22    /// Properties will be displayed in the right column if they are in the list for that column
23    ///
24    /// All items in the left columns will be displayed
25    /// The remaining items in the right columns not in the left columns will trail in the right column
26    /// ```md
27    /// ┌───────┬───────┐
28    /// │   1   │   1   │
29    /// │   3   │       │
30    /// │       │   2   │
31    /// │       │   4   │
32    /// └───────┴───────┘
33    /// ```
34    /// So for example, [1, 3] and [1, 2, 4] become [1, 2, 3, 4], as shown above
35    pub fn join_columns<T: Copy + Eq>(vec1: &[T], vec2: &[T]) -> Vec<(Option<T>, Option<T>)> {
36        vec1.iter()
37            .flat_map(|n1| {
38                if let Some(&n2) = vec2.iter().find(|&x| x == n1) {
39                    std::iter::once((Some(*n1), Some(n2)))
40                } else {
41                    std::iter::once((Some(*n1), None))
42                }
43            })
44            .chain(vec2.iter()
45                   .filter(|&n2| !vec1.contains(n2))
46                   .map(|n2| (None, Some(*n2))))
47            .collect()
48    }
49
50    /// Fetches the wikidata label for the wiki property
51    pub async fn fetch_name_for_wiki_property(pid: u64) -> String {
52        let query = format!("
53        SELECT ?label
54        WHERE
55        {{
56          wd:P{pid} rdfs:label ?label.
57          FILTER(lang(?label) = \"en\")
58          SERVICE wikibase:label {{ bd:serviceParam wikibase:language 'en'.}}
59        }}
60        ");
61
62        let api = mediawiki::api::Api::new("https://www.wikidata.org/w/api.php")
63            .await
64            .expect("Wikidata should return the api endpoint");
65        let res = api.sparql_query(&query)
66                     .await
67                     .expect("Query should be able to retrieve label");
68        let res_s = serde_json::to_string_pretty(&res).unwrap();
69
70        let re = Regex::new("\"value\": \"(.+)\"").unwrap();
71        let Some(label) = re.captures(&res_s) else { return "<unknown>".to_string() };
72
73        label[1].to_string()
74    }
75
76    /// Fetches the properties which have claims for the given wiki item inside of wikidata
77    pub fn fetch_properties_for_wiki_item(qid: u64) -> Vec<u64> {
78        let mut ids: Vec<u64> = vec![];
79        let uri = format!("https://www.wikidata.org/wiki/Special:EntityData/Q{qid}.json");
80        let res = reqwest::blocking::get(uri).unwrap();
81        let text = res.text().unwrap();
82        if text.contains("<h1>Not Found</h1><p>No entity with ID ") {
83            return vec![];
84        }
85        let ent = wikidata::Entity::from_json(serde_json::from_str(&text).unwrap()).unwrap();
86        if ent.claims.is_empty() {
87            return vec![];
88        }
89
90        for i in 0..ent.claims.len() {
91            let (wikidata::Pid(pid), _) = &ent.claims[i];
92            ids.push(*pid);
93        }
94
95        ids.sort();
96
97        ids.iter()
98           .map(|n| n.to_owned())
99           .unique()
100           .collect::<Vec<u64>>()
101    }
102
103    /// Fetches the wikidata identifier associated with the wikipedia article
104    pub fn fetch_wiki_item_by_article_title(title: String, language_code: String) -> u64 {
105        let uri = format!("https://{}.wikipedia.org/w/api.php?action=query&prop=pageprops&format=json&titles={}", &language_code, &title);
106
107        let res = reqwest::blocking::get(uri).unwrap();
108        let text = res.text().unwrap();
109        if text.contains("-1") {
110            return 0;
111        }
112
113        let re = Regex::new("\"wikibase_item\":\"Q(\\d+)\"").unwrap();
114        let Some(qid) = re.captures(&text) else { return 0 };
115
116        qid[1].parse().expect("Should be a numeric value")
117    }
118}
119
120#[cfg(test)]
121mod tests_lib {
122    use super::wikipedia_infobox_analyzer::*;
123    use std::fs;
124
125    #[test]
126    fn test_extract_used_properties_from_template() {
127        let contents = fs::read_to_string("examples/templates/nl_infobox_bedrijf")
128            .expect("Template file should exist");
129
130        assert_eq!(
131            extract_used_properties_from_template(contents),
132            vec![
133                18, 154, 1128, 4103, 2139, 2295, 2226, 856, 169,
134                1448, 159, 749, 355, 17, 1056, 452, 576, 112, 127,
135                856, 2096
136            ]
137        );
138
139        let contents = fs::read_to_string("examples/templates/nl_infobox_software")
140            .expect("Template file should exist");
141
142        assert_eq!(
143            extract_used_properties_from_template(contents),
144            vec![
145                18, 154, 170, 178, 275, 277, 306, 348, 400, 548,
146                571, 577, 856, 1324, 2096
147            ]
148        );
149    }
150
151    #[tokio::test]
152    async fn test_fetch_name_for_wiki_property() {
153        let name = fetch_name_for_wiki_property(31).await;
154        assert_eq!(name, "instance of")
155    }
156
157    #[test]
158    fn test_fetch_properties_for_wiki_item() {
159        let qid = 16639197; // identifier for GitLab on wikidata
160        let properties = fetch_properties_for_wiki_item(qid);
161
162        // Properties which should be present:
163        assert!(properties.contains(&10));  // property: video
164        assert!(properties.contains(&18));  // property: image
165        assert!(properties.contains(&154)); // property: logo image
166        assert!(properties.contains(&178)); // property: developer
167
168        // Properties which should not be present:
169        assert!(!properties.contains(&19)); // property: place of birth
170        assert!(!properties.contains(&30)); // property: continent
171    }
172
173    #[test]
174    fn test_fetch_wiki_item_by_article_title() {
175        assert_eq!(fetch_wiki_item_by_article_title("Earth".to_string(), "en".to_string()), 2);
176        assert_eq!(fetch_wiki_item_by_article_title("Train".to_string(), "en".to_string()), 870);
177        assert_eq!(fetch_wiki_item_by_article_title("Baum".to_string(),  "de".to_string()), 10884);
178    }
179}