Skip to content

Instantly share code, notes, and snippets.

@non7247
Last active May 4, 2024 06:35
Show Gist options
  • Save non7247/2a42f9a456044b3422f375a51b5ce5bf to your computer and use it in GitHub Desktop.
Save non7247/2a42f9a456044b3422f375a51b5ce5bf to your computer and use it in GitHub Desktop.
関数型プログラムの設計(テストコードを追加)
use reqwest::blocking::Client;
use reqwest::header::{HeaderMap, USER_AGENT};
use serde::Deserialize;
use std::ops::Deref;
use std::error;
type Result<T> = std::result::Result<T, Box<dyn error::Error>>;
#[derive(Debug, Clone)]
struct LocationId(String);
impl LocationId {
fn from(s: &str) -> Self {
LocationId(String::from(s))
}
}
impl Deref for LocationId {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct Location {
id: LocationId,
name: String,
population: u64
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct Attraction {
name: String,
description: Option<String>,
location: Location
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
enum PopCultureSubject {
Artist {name: String, followers: u64},
Movie {name: String, box_office: u64}
}
#[derive(Debug, Clone)]
struct TravelGuide {
attraction: Attraction,
subjects: Vec<PopCultureSubject>
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
enum AttractionOrdering {
ByName,
ByLocationPopulation
}
#[derive(Debug, Clone)]
struct SearchReport {
bad_guides: Vec<TravelGuide>,
problems: Vec<String>
}
impl SearchReport {
fn new() -> Self {
Self { bad_guides: vec![], problems: vec![] }
}
}
impl std::error::Error for SearchReport {}
impl std::fmt::Display for SearchReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}, {:?}", self.bad_guides, self.problems)
}
}
#[derive(Debug, Deserialize)]
struct WDInstance {
value: String,
}
impl WDInstance {
fn get_local_name(&self) -> String {
match self.value.rfind("/") {
Some(i) => self.value[i + 1..].to_string(),
None => String::new()
}
}
}
#[derive(Debug, Deserialize)]
struct WDLabel {
value: String,
}
#[derive(Debug, Deserialize)]
struct WDNumber {
value: String,
}
impl WDNumber {
fn get_number(&self) -> u64 {
match self.value.parse::<u64>() {
Ok(n) => n,
Err(_) => 0,
}
}
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct BindingAttraction {
attraction: WDInstance,
#[serde(rename = "attractionLabel")]
attraction_label: WDLabel,
description: Option<String>,
location: WDInstance,
#[serde(rename = "locationLabel")]
location_label: WDLabel,
population: WDNumber,
}
#[derive(Debug, Deserialize)]
struct ResultsAttraction {
bindings: Vec<BindingAttraction>,
}
#[derive(Debug, Deserialize)]
struct SolutionAttraction {
results: ResultsAttraction,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct BindingArtist {
artist: WDInstance,
#[serde(rename = "artistLabel")]
artist_label: WDLabel,
followers: WDNumber,
}
#[derive(Debug, Deserialize)]
struct ResultsArtist {
bindings: Vec<BindingArtist>,
}
#[derive(Debug, Deserialize)]
struct SolutionArtist {
results: ResultsArtist,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct BindingSubject {
subject: WDInstance,
#[serde(rename = "subjectLabel")]
subject_label: WDLabel,
#[serde(rename = "boxOffice")]
box_office: WDNumber,
}
#[derive(Debug, Deserialize)]
struct ResultSubject {
bindings: Vec<BindingSubject>,
}
#[derive(Debug, Deserialize)]
struct SolutionSubject {
results: ResultSubject,
}
fn parse_artist(artist: BindingArtist) -> PopCultureSubject {
PopCultureSubject::Artist {
name: artist.artist_label.value,
followers: artist.followers.get_number()
}
}
fn find_artists_from_location(
client: &Client,
url: &str,
headers: HeaderMap,
location_id: &LocationId,
limit: u32
) -> Result<Vec<PopCultureSubject>> {
let response = client
.get(url)
.headers(headers)
.query(&[
("format", "json"),
("query", format!("
PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX schema: <http://schema.org>
SELECT DISTINCT ?artist ?artistLabel ?followers WHERE {{
?artist wdt:P136 ?genre;
wdt:P8687 ?followers;
rdfs:label ?artistLabel.
FILTER(LANG(?artistLabel) = \"en\").
?artist wdt:P740 wd:{}
}} ORDER BY DESC(?followers) LIMIT {}
", **location_id, limit).trim())
])
.send()?;
let body = response.text()?;
let artists: SolutionArtist = serde_json::from_str(&body)?;
let mut result = vec![];
for artist in artists.results.bindings.into_iter() {
result.push(parse_artist(artist));
}
Ok(result)
}
fn parse_movie(binding: BindingSubject) -> PopCultureSubject {
PopCultureSubject::Movie {
name: binding.subject_label.value,
box_office: binding.box_office.get_number()
}
}
fn find_movies_about_location(
client: &Client,
url: &str,
headers: HeaderMap,
location_id: &LocationId,
limit: u32
) -> Result<Vec<PopCultureSubject>> {
let response = client
.get(url)
.headers(headers)
.query(&[
("format", "json"),
("query", format!("
PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX schema: <http://schema.org>
SELECT DISTINCT ?subject ?subjectLabel ?boxOffice WHERE {{
?subject wdt:P31 wd:Q11424;
wdt:P2142 ?boxOffice;
rdfs:label ?subjectLabel.
?subject wdt:P840 wd:{}
FILTER(LANG(?subjectLabel) = \"en\").
}} ORDER BY DESC(?boxOffice) LIMIT {}
", **location_id, limit).trim())
])
.send()?;
let body = response.text()?;
let subjects: SolutionSubject = serde_json::from_str(&body)?;
let mut result = vec![];
for subject in subjects.results.bindings.into_iter() {
result.push(parse_movie(subject));
}
Ok(result)
}
fn parse_attraction(attraction: BindingAttraction) -> Attraction {
let id = LocationId::from(&attraction.location.get_local_name());
Attraction {
name: attraction.attraction_label.value,
description: attraction.description,
location: Location {
id,
name: attraction.location_label.value,
population: attraction.population.get_number()
}
}
}
fn find_attractions(
client: &Client,
url: &str,
headers: HeaderMap,
name: &str,
ordering: AttractionOrdering,
limit: u32
) -> Result<Vec<Attraction>> {
let order_by = match ordering {
AttractionOrdering::ByName => "?attractionLabel",
AttractionOrdering::ByLocationPopulation => "DESC(?population)",
};
let query = format!("
PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX schema: <http://schema.org>
SELECT DISTINCT ?attraction ?attractionLabel ?description
?location ?locationLabel ?population WHERE {{
?attraction wdt:P31 wd:Q570116;
rdfs:label ?attractionLabel;
wdt:P131 ?location.
FILTER(LANG(?attractionLabel) = \"en\").
OPTIONAL{{
?attraction schema:description ?description.
FILTER(LANG(?description) = \"en\").
}}
?location wdt:P1082 ?population;
rdfs:label ?locationLabel;
FILTER(LANG(?locationLabel) = \"en\").
FILTER(CONTAINS(?attractionLabel, \"{}\")).
}} ORDER BY {} LIMIT {}
", name, order_by, limit);
let query = query.trim();
let response = client
.get(url)
.headers(headers)
.query(&[("format", "json"), ("query", query)])
.send()?;
let body = response.text()?;
let attractions: SolutionAttraction = serde_json::from_str(&body)?;
let mut result = vec![];
for attraction in attractions.results.bindings.into_iter() {
result.push(parse_attraction(attraction));
}
Ok(result)
}
fn guide_score(guide: &TravelGuide) -> u64 {
let description_score = if guide.attraction.description.is_some() { 30 } else { 0 };
let quantity_score = std::cmp::min(40, guide.subjects.len() * 10) as u64;
let total_followers: u64 = guide.subjects.iter()
.map(|s| { match s {
PopCultureSubject::Artist { name: _, followers } => *followers,
_ => 0
}})
.sum();
let total_box_office: u64 = guide.subjects.iter()
.map(|s| { match s {
PopCultureSubject::Movie { name: _, box_office } => *box_office,
_ => 0
}})
.sum();
let followers_score = std::cmp::min(15, total_followers / 100_000);
let box_office_score = std::cmp::min(15, total_box_office / 10_000_000);
description_score + quantity_score + followers_score + box_office_score
}
fn travel_guide(attraction_name: &str) -> Result<TravelGuide> {
let client = Client::new();
let url = "https://query.wikidata.org/sparql";
let user_agent = "User-Agent: reqwest";
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, user_agent.parse()?);
let attractions = find_attractions(
&client, url, headers.clone(), attraction_name, AttractionOrdering::ByLocationPopulation, 3
)?;
let mut guides = vec![];
for attraction in attractions {
let mut subjects = vec![];
match find_artists_from_location(
&client, url, headers.clone(), &attraction.location.id, 2
) {
Ok(artists) => subjects.extend(artists),
Err(e) => println!("{:?}", e)
};
match find_movies_about_location(
&client, url, headers.clone(), &attraction.location.id, 2
) {
Ok(movies) => subjects.extend(movies),
Err(e) => println!("{:?}", e)
};
guides.push(TravelGuide {attraction, subjects});
}
guides.sort_by(|l, r| {guide_score(r).cmp(&guide_score(l))});
guides.first()
.ok_or(SearchReport::new().into())
.cloned()
}
fn main() {
match travel_guide("Yosemite") {
Ok(guide) => println!("{:?}", guide),
Err(e) => println!("Not found: {}", e)
}
match travel_guide("Yellowstone") {
Ok(guide) => println!("{:?}", guide),
Err(e) => println!("Not found: {}", e)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn test_guide_score() {
// score of a guide with a description, 0 artists, and 2 movies should be 65.
let guide = TravelGuide {
attraction: Attraction {
name: "Yellowstone National Park".to_string(),
description: Some("first national park in the world".to_string()),
location: Location {
id: LocationId::from("Q1214"),
name: "Wyoming".to_string(),
population: 586107
}
},
subjects: vec![
PopCultureSubject::Movie{
name: "The Hateful Eight".to_string(),
box_office: 155760117
},
PopCultureSubject::Movie{
name: "Heaven's Gate".to_string(),
box_office: 3484331
}
]
};
// 30 (description) + 0 (0 artists) + 20 (2 movies) + 15 (159 million box office)
assert_eq!(guide_score(&guide), 65);
}
#[test]
fn test_guide_score2() {
// score of a guide with no description, 0 artists, and 0 movies should be 0.
let guide = TravelGuide {
attraction: Attraction {
name: "Yellowstone National Park".to_string(),
description: None,
location: Location {
id: LocationId::from("Q1214"),
name: "Wyoming".to_string(),
population: 586107
}
},
subjects: vec![]
};
// 0 (description) + 0 (0 artists) + 0 (0 movies)
assert_eq!(guide_score(&guide), 0);
}
#[test]
fn test_guide_score3() {
// score of a guide with no description, 0 artists, and 2 movies with
// no box office earnings should be 20.
let guide = TravelGuide {
attraction: Attraction {
name: "Yellowstone National Park".to_string(),
description: None,
location: Location {
id: LocationId::from("Q1214"),
name: "Wyoming".to_string(),
population: 586107
}
},
subjects: vec![
PopCultureSubject::Movie{
name: "The Hateful Eight".to_string(),
box_office: 0
},
PopCultureSubject::Movie{
name: "Heaven's Gate".to_string(),
box_office: 0
}
]
};
// 0 (description) + 0 (0 artists) + 20 (2 movies) + 0 (0 million box office)
assert_eq!(guide_score(&guide), 20);
}
proptest! {
#[test]
fn proptest_guide_score(name: String, description: String) {
// guide score should not depend on its attraction's name and description strings.
let guide = TravelGuide {
attraction: Attraction {
name,
description: Some(description),
location: Location {
id: LocationId::from("Q1214"),
name: "Wyoming".to_string(),
population: 586107
}
},
subjects: vec![
PopCultureSubject::Movie{
name: "The Hateful Eight".to_string(),
box_office: 155760117
},
PopCultureSubject::Movie{
name: "Heaven's Gate".to_string(),
box_office: 3484331
}
]
};
// 30 (description) + 0 (0 artists) + 20 (2 movies) + 15 (159 million box office)
assert_eq!(guide_score(&guide), 65);
}
#[test]
fn proptest_guide_score2(amount_of_movies: u8) {
// guide score should always be between 30 and 70 if it has a description and
// some bad movies.
let guide = TravelGuide {
attraction: Attraction {
name: "Yellowstone National Park".to_string(),
description: Some("first national park in the world".to_string()),
location: Location {
id: LocationId::from("Q1214"),
name: "Wyoming".to_string(),
population: 586107
}
},
subjects: [
PopCultureSubject::Movie{name: "Random Movie".to_string(), box_office: 0}
].into_iter()
.cycle()
.take(amount_of_movies as usize)
.collect::<Vec<_>>()
};
let score = guide_score(&guide);
// greater equal 30 (description) and less equal 70 (0 artists, 0 million box office)
assert!(score >= 30 && score <= 70);
}
#[test]
fn proptest_guide_score3(followers: u64, box_office: u64) {
// guide score should always be between 20 and 50 if there is an artist and
// a movie but no description.
let guide = TravelGuide {
attraction: Attraction {
name: "Yellowstone National Park".to_string(),
description: None,
location: Location {
id: LocationId::from("Q1214"),
name: "Wyoming".to_string(),
population: 586107
}
},
subjects: vec![
PopCultureSubject::Artist { name: "Chris LeDoux".to_string(), followers },
PopCultureSubject::Movie { name: "The Hateful Eight".to_string(), box_office }
]
};
let score = guide_score(&guide);
// greater equal 20 = 0 (description) + 10 (1 artist) + 10 (1 movie)
// and less equal 50 (many followers or many box office)
assert!(score >= 20 && score <= 50);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment