Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report distinction between default and non-default languages #12

Merged
merged 4 commits into from
Aug 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions benches/intersection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#![feature(test)]
extern crate accept_language;
extern crate test;

#[cfg(test)]
mod benches {
use super::accept_language::*;
use test::Bencher;

static MOCK_ACCEPT_LANGUAGE: &str = "en-US, nl, fr; q=0.3, de;q=0.7, zh-Hant: q=0.01, jp;q=0.1";
static AVIALABLE_LANGUAGES: &[&str] = &[
"aa", "ab", "ae", "af", "ak", "am", "an", "ar", "as", "av", "ay", "az", "ba", "be", "bg",
"bh", "bi", "bm", "bn", "bo", "br", "bs", "ca", "ce", "ch", "co", "cr", "cs", "cu", "cv",
"cy", "da", "de", "dv", "dz", "ee", "el", "en", "en-UK", "en-US", "eo", "es", "es-ar",
"et", "eu", "fa", "ff", "fi", "fj", "fo", "fr", "fy", "ga", "gd", "gl", "gn", "gu", "gv",
"gv", "ha", "he", "hi", "ho", "hr", "ht", "hu", "hy", "hz", "ia", "id", "ie", "ig", "ii",
"ii", "ik", "in", "io", "is", "it", "iu", "ja", "jp", "jv", "ka", "kg", "ki", "kj", "kk",
"kl", "kl", "km", "kn", "ko", "kr", "ks", "ku", "kv", "kw", "ky", "la", "lb", "lg", "li",
"ln", "lo", "lt", "lu", "lv", "mg", "mh", "mi", "mk", "ml", "mn", "mo", "mr", "ms", "mt",
"my", "na", "nb", "nd", "ne", "ng", "nl", "nn", "no", "nr", "nv", "ny", "oc", "oj", "om",
"or", "os", "pa", "pi", "pl", "ps", "pt", "qu", "rm", "rn", "ro", "ru", "rw", "sa", "sd",
"se", "sg", "sh", "si", "sk", "sl", "sm", "sn", "so", "sq", "sr", "ss", "ss", "st", "su",
"sv", "sw", "ta", "te", "tg", "th", "ti", "tk", "tl", "tn", "to", "tr", "ts", "tt", "tw",
"ty", "ug", "uk", "ur", "uz", "ve", "vi", "vo", "wa", "wo", "xh", "yi", "yo", "za", "zh",
"zh-Hans", "zh-Hant", "zu",
];

#[bench]
fn bench_parse(b: &mut Bencher) {
b.iter(|| parse(MOCK_ACCEPT_LANGUAGE));
}

#[bench]
fn bench_parse_with_quality(b: &mut Bencher) {
b.iter(|| parse_with_quality(MOCK_ACCEPT_LANGUAGE));
}

#[bench]
fn bench_intersections(b: &mut Bencher) {
b.iter(|| intersection(MOCK_ACCEPT_LANGUAGE, AVIALABLE_LANGUAGES));
}

#[bench]
fn bench_intersections_ordered(b: &mut Bencher) {
b.iter(|| intersection_ordered(MOCK_ACCEPT_LANGUAGE, AVIALABLE_LANGUAGES));
}

#[bench]
fn bench_intersections_with_quality(b: &mut Bencher) {
b.iter(|| intersection_with_quality(MOCK_ACCEPT_LANGUAGE, AVIALABLE_LANGUAGES));
}

#[bench]
fn bench_intersections_ordered_with_quality(b: &mut Bencher) {
b.iter(|| intersection_ordered_with_quality(MOCK_ACCEPT_LANGUAGE, AVIALABLE_LANGUAGES));
}
}
165 changes: 141 additions & 24 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//!
//! In order to help facilitate better i18n, a function is provided to return the intersection of
//! the languages the user prefers and the languages your application supports.
//! You can try the `cargo test` and `cargo bench` to verify the behaviour.
//!
//! # Example
//!
Expand All @@ -21,7 +22,7 @@ use std::str::FromStr;
#[derive(Debug)]
struct Language {
name: String,
quality: f64,
quality: f32,
}

impl Eq for Language {}
Expand Down Expand Up @@ -58,15 +59,13 @@ impl Language {
1 => 1.0,
_ => Language::quality_with_default(tag_parts[1]),
};

Language { name, quality }
}

fn quality_with_default(raw_quality: &str) -> f64 {
fn quality_with_default(raw_quality: &str) -> f32 {
let quality_parts: Vec<&str> = raw_quality.split('=').collect();

match quality_parts.len() {
2 => f64::from_str(quality_parts[1]).unwrap_or(0.0),
2 => f32::from_str(quality_parts[1]).unwrap_or(0.0),
_ => 0.0,
}
}
Expand All @@ -86,16 +85,38 @@ pub fn parse(raw_languages: &str) -> Vec<String> {
let stripped_languages = raw_languages.to_owned().replace(' ', "");
let language_strings: Vec<&str> = stripped_languages.split(',').collect();
let mut languages: Vec<Language> = language_strings.iter().map(|l| Language::new(l)).collect();

languages.sort();

languages
.iter()
.map(|l| l.name.to_owned())
.filter(|l| !l.is_empty())
.collect()
}

/// Similar to [`parse`](parse) but with quality `f32` appended to notice if it is a default value.
/// is used by [`intersection_with_quality`](intersection_with_quality) and
/// [`intersection_ordered_with_quality`](intersection_ordered_with_quality).
///
/// # Example
///
/// ```
/// use accept_language::parse_with_quality;
///
/// let user_languages = parse_with_quality("en-US, en-GB;q=0.5");
/// assert_eq!(user_languages,vec![(String::from("en-US"), 1.0), (String::from("en-GB"), 0.5)])
/// ```
pub fn parse_with_quality(raw_languages: &str) -> Vec<(String, f32)> {
let stripped_languages = raw_languages.to_owned().replace(' ', "");
let language_strings: Vec<&str> = stripped_languages.split(',').collect();
let mut languages: Vec<Language> = language_strings.iter().map(|l| Language::new(l)).collect();
languages.sort();
languages
.iter()
.map(|l| (l.name.to_owned(), l.quality))
.filter(|l| !l.0.is_empty())
.collect()
}

/// Compare an Accept-Language header value with your application's supported languages to find
/// the common languages that could be presented to a user.
///
Expand All @@ -108,23 +129,90 @@ pub fn parse(raw_languages: &str) -> Vec<String> {
/// ```
pub fn intersection(raw_languages: &str, supported_languages: &[&str]) -> Vec<String> {
let user_languages = parse(raw_languages);

user_languages
.into_iter()
.filter(|l| supported_languages.contains(&l.as_str()))
.collect()
}
/// Similar to [`intersection`](intersection) but using binary sort. The supported languages
/// MUST be in alphabetical order, to find the common languages that could be presented
/// to a user. Executes roughly 25% faster.
///
/// # Example
///
/// ```
/// use accept_language::intersection_ordered;
///
/// let common_languages = intersection_ordered("en-US, en-GB;q=0.5", &["de", "en-GB", "en-US"]);
/// ```
pub fn intersection_ordered(raw_languages: &str, supported_languages: &[&str]) -> Vec<String> {
let user_languages = parse(raw_languages);
user_languages
.into_iter()
.filter(|l| supported_languages.binary_search(&l.as_str()).is_ok())
.collect()
}
/// Similar to [`intersection`](intersection) but with the quality as `f32` appended for each language.
/// This enables distinction between the default language of a user (value 1.0) and the
/// best match. If you don't want to assign your users immediatly to a non-default choice and you plan to add
/// more languages later on in your webserver.
///
/// # Example
///
/// ```
/// use accept_language::intersection_with_quality;
///
/// let common_languages = intersection_with_quality("en-US, en-GB;q=0.5", &["en-US", "de", "en-GB"]);
/// assert_eq!(common_languages,vec![(String::from("en-US"), 1.0), (String::from("en-GB"), 0.5)])
/// ```
pub fn intersection_with_quality(
raw_languages: &str,
supported_languages: &[&str],
) -> Vec<(String, f32)> {
let user_languages = parse_with_quality(raw_languages);
user_languages
.into_iter()
.filter(|l| supported_languages.contains(&l.0.as_str()))
.collect()
}

/// Similar to [`intersection_with_quality`](intersection_with_quality). The supported languages MUST
/// be in alphabetical order, to find the common languages that could be presented to a user.
/// Executes roughly 25% faster.
///
/// # Example
///
/// ```
/// use accept_language::intersection_ordered_with_quality;
///
/// let common_languages = intersection_ordered_with_quality("en-US, en-GB;q=0.5", &["de", "en-GB", "en-US"]);
/// assert_eq!(common_languages,vec![(String::from("en-US"), 1.0), (String::from("en-GB"), 0.5)])
/// ```
pub fn intersection_ordered_with_quality(
raw_languages: &str,
supported_languages: &[&str],
) -> Vec<(String, f32)> {
let user_languages = parse_with_quality(raw_languages);
user_languages
.into_iter()
.filter(|l| supported_languages.binary_search(&l.0.as_str()).is_ok())
.collect()
}

#[cfg(test)]
mod tests {
use super::{intersection, parse, Language};
use super::{
intersection, intersection_ordered, intersection_ordered_with_quality,
intersection_with_quality, parse, Language,
};

static MOCK_ACCEPT_LANGUAGE: &str = "en-US, de;q=0.7, jp;q=0.1";
static MOCK_ACCEPT_LANGUAGE: &str = "en-US, de;q=0.7, zh-Hant, jp;q=0.1";
static AVIALABLE_LANGUAGES: &[&str] =
&["da", "de", "en-US", "it", "jp", "zh", "zh-Hans", "zh-Hant"];

#[test]
fn it_creates_a_new_language_from_a_string() {
let language = Language::new("en-US;q=0.7");

assert_eq!(
language,
Language {
Expand All @@ -137,7 +225,6 @@ mod tests {
#[test]
fn it_creates_a_new_language_from_a_string_with_lowercase_country() {
let language = Language::new("en-us;q=0.7");

assert_eq!(
language,
Language {
Expand All @@ -150,7 +237,6 @@ mod tests {
#[test]
fn it_creates_a_new_language_from_a_string_with_a_default_quality() {
let language = Language::new("en-US");

assert_eq!(
language,
Language {
Expand All @@ -163,25 +249,23 @@ mod tests {
#[test]
fn it_parses_quality() {
let quality = Language::quality_with_default("q=0.5");

assert_eq!(quality, 0.5)
}

#[test]
fn it_parses_an_invalid_quality() {
let quality = Language::quality_with_default("q=yolo");

assert_eq!(quality, 0.0)
}

#[test]
fn it_parses_a_valid_accept_language_header() {
let user_languages = parse(MOCK_ACCEPT_LANGUAGE);

assert_eq!(
user_languages,
vec![
String::from("en-US"),
String::from("zh-Hant"),
String::from("de"),
String::from("jp"),
]
Expand All @@ -191,7 +275,6 @@ mod tests {
#[test]
fn it_parses_an_empty_accept_language_header() {
let user_languages = parse("");

assert_eq!(user_languages.len(), 0)
}

Expand All @@ -201,7 +284,6 @@ mod tests {
let user_languages_two = parse(";q");
let user_languages_three = parse("q-");
let user_languages_four = parse("en;q=");

assert_eq!(user_languages_one, vec![String::from("q")]);
assert_eq!(user_languages_two.len(), 0);
assert_eq!(user_languages_three, vec![String::from("q-")]);
Expand All @@ -211,7 +293,6 @@ mod tests {
#[test]
fn it_sorts_languages_by_quality() {
let user_languages = parse("en-US, de;q=0.1, jp;q=0.7");

assert_eq!(
user_languages,
vec![
Expand All @@ -223,19 +304,55 @@ mod tests {
}

#[test]
fn it_returns_language_intersections() {
let common_languages = intersection(MOCK_ACCEPT_LANGUAGE, &["en-US", "jp"]);
fn it_returns_language_intersection() {
let common_languages = intersection(MOCK_ACCEPT_LANGUAGE, AVIALABLE_LANGUAGES);
assert_eq!(
common_languages,
vec![
String::from("en-US"),
String::from("zh-Hant"),
String::from("de"),
String::from("jp")
]
)
}

#[test]
fn it_returns_language_intersection_ordered() {
let common_languages = intersection_ordered(MOCK_ACCEPT_LANGUAGE, AVIALABLE_LANGUAGES);
assert_eq!(
common_languages,
vec![String::from("en-US"), String::from("jp")]
vec![
String::from("en-US"),
String::from("zh-Hant"),
String::from("de"),
String::from("jp")
]
)
}

#[test]
fn it_returns_an_empty_array_when_no_intersections() {
let common_languages = intersection(MOCK_ACCEPT_LANGUAGE, &["fr", "en-GB"]);
fn it_returns_language_intersection_with_quality() {
let common_languages = intersection_with_quality(MOCK_ACCEPT_LANGUAGE, &["en-US", "jp"]);
assert_eq!(
common_languages,
vec![(String::from("en-US"), 1.0), (String::from("jp"), 0.1)]
)
}

#[test]
fn it_returns_language_intersection_ordered_with_quality() {
let common_languages =
intersection_ordered_with_quality(MOCK_ACCEPT_LANGUAGE, &["en-US", "jp"]);
assert_eq!(
common_languages,
vec![(String::from("en-US"), 1.0), (String::from("jp"), 0.1)]
)
}

#[test]
fn it_returns_an_empty_array_when_no_intersection() {
let common_languages = intersection(MOCK_ACCEPT_LANGUAGE, &["fr", "en-GB"]);
assert_eq!(common_languages.len(), 0)
}

Expand Down