Skip to content

Commit

Permalink
Add support for custom unit definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
printfn committed Nov 10, 2023
1 parent e6b8d2d commit bc1c9fe
Show file tree
Hide file tree
Showing 9 changed files with 344 additions and 30 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
## Changelog

### Unreleased

* Add support for custom unit definitions: you can now define your own units
in the `~/.config/fend/config.toml` configuration file. For example:

```toml
[[custom-units]]
singular = 'fortnight'
plural = 'fortnights' # plural form is optional, defaults
# to singular if not specified
definition = '14 days'
attribute = 'allow-long-prefix' # this makes it possible to combine this
# unit with prefixes like 'milli-' or 'giga-'
```

See the [default config file](https://github.com/printfn/fend/blob/main/cli/src/default_config.toml) for more examples.
* You can now tab-complete greek letters in the CLI, e.g. `\alpha` becomes α
(by [@Markos-Th09](https://github.com/Markos-Th09))
* Add CGS units (by [@Markos-Th09](https://github.com/Markos-Th09))

### v1.3.1 (2023-10-26)

* Add support for additional imperial and US customary units
Expand Down
14 changes: 12 additions & 2 deletions cli/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::color;
use crate::{color, custom_units::CustomUnitDefinition};
use std::{env, fmt, fs, io};

#[derive(Debug, Eq, PartialEq)]
Expand All @@ -10,6 +10,7 @@ pub struct Config {
pub max_history_size: usize,
pub enable_internet_access: bool,
pub exchange_rate_source: ExchangeRateSource,
pub custom_units: Vec<CustomUnitDefinition>,
unknown_settings: UnknownSettings,
unknown_keys: Vec<String>,
}
Expand Down Expand Up @@ -75,6 +76,7 @@ impl<'de> serde::de::Visitor<'de> for ConfigVisitor {
let mut seen_max_hist_size = false;
let mut seen_enable_internet_access = false;
let mut seen_exchange_rate_source = false;
let mut seen_custom_units = false;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"prompt" => {
Expand Down Expand Up @@ -148,6 +150,13 @@ impl<'de> serde::de::Visitor<'de> for ConfigVisitor {
}
};
}
"custom-units" => {
if seen_custom_units {
return Err(serde::de::Error::duplicate_field("custom-units"));
}
result.custom_units = map.next_value()?;
seen_custom_units = true;
}
unknown_key => {
// this may occur if the user has multiple fend versions installed
map.next_value::<toml::Value>()?;
Expand Down Expand Up @@ -184,8 +193,9 @@ impl Default for Config {
max_history_size: 1000,
enable_internet_access: true,
unknown_settings: UnknownSettings::Warn,
unknown_keys: vec![],
exchange_rate_source: ExchangeRateSource::UnitedNations,
custom_units: vec![],
unknown_keys: vec![],
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions cli/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ impl InnerCtx {
if config.coulomb_and_farad {
res.core_ctx.use_coulomb_and_farad();
}
for custom_unit in &config.custom_units {
res.core_ctx.define_custom_unit_v1(
&custom_unit.singular,
&custom_unit.plural,
&custom_unit.definition,
custom_unit.attribute.to_fend_core(),
);
}
res
}
}
Expand Down
162 changes: 162 additions & 0 deletions cli/src/custom_units.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
use std::fmt;

#[derive(Clone, PartialEq, Eq, Debug, Hash, Default)]
pub enum CustomUnitAttribute {
#[default]
None,
AllowLongPrefix,
AllowShortPrefix,
IsLongPrefix,
Alias,
}

struct CustomUnitAttributeVisitor;

impl<'de> serde::de::Visitor<'de> for CustomUnitAttributeVisitor {
type Value = CustomUnitAttribute;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(
"`none`, `allow-long-prefix`, `allow-short-prefix`, `is-long-prefix` or `alias`",
)
}

fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(match v {
"none" => CustomUnitAttribute::None,
"allow-long-prefix" => CustomUnitAttribute::AllowLongPrefix,
"allow-short-prefix" => CustomUnitAttribute::AllowShortPrefix,
"is-long-prefix" => CustomUnitAttribute::IsLongPrefix,
"alias" => CustomUnitAttribute::Alias,
unknown => {
return Err(serde::de::Error::unknown_variant(
unknown,
&[
"none",
"allow-long-prefix",
"allow-short-prefix",
"is-long-prefix",
"alias",
],
))
}
})
}
}

impl<'de> serde::Deserialize<'de> for CustomUnitAttribute {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(CustomUnitAttributeVisitor)
}
}

impl CustomUnitAttribute {
pub fn to_fend_core(&self) -> fend_core::CustomUnitAttribute {
match self {
CustomUnitAttribute::None => fend_core::CustomUnitAttribute::None,
CustomUnitAttribute::AllowLongPrefix => fend_core::CustomUnitAttribute::AllowLongPrefix,
CustomUnitAttribute::AllowShortPrefix => {
fend_core::CustomUnitAttribute::AllowShortPrefix
}
CustomUnitAttribute::IsLongPrefix => fend_core::CustomUnitAttribute::IsLongPrefix,
CustomUnitAttribute::Alias => fend_core::CustomUnitAttribute::Alias,
}
}
}

#[derive(Debug, Eq, PartialEq)]
pub struct CustomUnitDefinition {
pub singular: String,
pub plural: String,
pub definition: String,
pub attribute: CustomUnitAttribute,
}

impl<'de> serde::Deserialize<'de> for CustomUnitDefinition {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
const FIELDS: &[&str] = &["singular", "plural", "definition", "attribute"];

struct CustomUnitDefinitionVisitor;

impl<'de> serde::de::Visitor<'de> for CustomUnitDefinitionVisitor {
type Value = CustomUnitDefinition;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a custom unit definition, with properties `singular`, `plural`, `definition` and `attribute`")
}

fn visit_map<V: serde::de::MapAccess<'de>>(
self,
mut map: V,
) -> Result<CustomUnitDefinition, V::Error> {
let mut result = CustomUnitDefinition {
attribute: CustomUnitAttribute::None,
definition: String::new(),
singular: String::new(),
plural: String::new(),
};
let mut seen_singular = false;
let mut seen_plural = false;
let mut seen_definition = false;
let mut seen_attribute = false;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"singular" => {
if seen_singular {
return Err(serde::de::Error::duplicate_field("singular"));
}
result.singular = map.next_value()?;
seen_singular = true;
}
"plural" => {
if seen_plural {
return Err(serde::de::Error::duplicate_field("plural"));
}
result.plural = map.next_value()?;
if result.plural.is_empty() {
return Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Str(&result.plural),
&"a non-empty string describing the plural form of this unit",
));
}
seen_plural = true;
}
"definition" => {
if seen_definition {
return Err(serde::de::Error::duplicate_field("definition"));
}
result.definition = map.next_value()?;
if result.definition.is_empty() {
return Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Str(&result.definition),
&"a non-empty string that contains the definition of this unit",
));
}
seen_definition = true;
}
"attribute" => {
if seen_attribute {
return Err(serde::de::Error::duplicate_field("attribute"));
}
result.attribute = map.next_value()?;
seen_attribute = true;
}
unknown_key => {
map.next_value::<toml::Value>()?;
return Err(serde::de::Error::unknown_field(unknown_key, FIELDS));
}
}
}
if result.singular.is_empty() {
return Err(serde::de::Error::missing_field("singular"));
}
if result.definition.is_empty() {
return Err(serde::de::Error::missing_field("definition"));
}
Ok(result)
}
}

deserializer.deserialize_struct("CustomUnitDefinition", FIELDS, CustomUnitDefinitionVisitor)
}
}
44 changes: 44 additions & 0 deletions cli/src/default_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,47 @@ keyword = { foreground = 'blue', bold = true }
built-in-function = { foreground = 'blue', bold = true }
date = {}
other = {}

# This section defines custom units. If there's a unit you
# would like to see added to fend, please consider making
# a pull request to add it to the built-in unit definitions.
[custom_units]

# Example syntax:
# ```
# [[custom_units]]
# singular = 'mile'
# plural = 'miles' # plural name can be omitted if it is
# # the same as the singular name
# definition = '1609.344 meters'
# attribute = 'allow_long_prefix'
# ```
#
# If the singular and plural names are the same, you can omit
# the `plural` setting.
#
# The `attribute` setting is optional. It can be set
# to one of these values:
# * 'none': this unit cannot be used with prefixes (default)
# * 'allow-long-prefix': allow prefixes like
# 'milli-' or 'giga-' with this unit
# * 'allow-short-prefix': allow abbreviated
# prefixes like 'm' or 'G' (for 'milli' and 'giga' respectively)
# * 'is-long-prefix': allow using this unit
# as a long prefix with another unit
# * 'alias': always expand this unit to its definition
#
# Here are some more examples of how you could define custom units:
#
# ```
# [[custom_units]]
# singular = 'milli'
# definition = '0.001'
# attribute = 'is_long_prefix'
#
# [[custom_units]]
# singular = 'byte'
# plural = 'bytes'
# definition = '!' # an exclamation mark defines a new base unit
# attribute = 'allow_long_prefix'
# ```
1 change: 1 addition & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod args;
mod color;
mod config;
mod context;
mod custom_units;
mod exchange_rates;
mod file_paths;
mod helper;
Expand Down
38 changes: 38 additions & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ pub struct Context {
random_u32: Option<fn() -> u32>,
output_mode: OutputMode,
get_exchange_rate: Option<Arc<dyn ExchangeRateFn + Send + Sync>>,
custom_units: Vec<(String, String, String)>,
}

impl fmt::Debug for Context {
Expand Down Expand Up @@ -235,6 +236,7 @@ impl Context {
random_u32: None,
output_mode: OutputMode::SimpleText,
get_exchange_rate: None,
custom_units: vec![],
}
}

Expand Down Expand Up @@ -335,6 +337,42 @@ impl Context {
) {
self.get_exchange_rate = Some(Arc::new(get_exchange_rate));
}

pub fn define_custom_unit_v1(
&mut self,
singular: &str,
plural: &str,
definition: &str,
attribute: CustomUnitAttribute,
) {
let definition_prefix = match attribute {
CustomUnitAttribute::None => "",
CustomUnitAttribute::AllowLongPrefix => "l@",
CustomUnitAttribute::AllowShortPrefix => "s@",
CustomUnitAttribute::IsLongPrefix => "lp@",
CustomUnitAttribute::Alias => "=",
};
self.custom_units.push((
singular.to_string(),
plural.to_string(),
format!("{definition_prefix}{definition}"),
));
}
}

/// These attributes make is possible to change the behaviour of custom units
#[non_exhaustive]
pub enum CustomUnitAttribute {
/// Don't allow using prefixes with this custom unit
None,
/// Support long prefixes (e.g. `milli-`, `giga-`) with this unit
AllowLongPrefix,
/// Support short prefixes (e.g. `k` for `kilo`) with this unit
AllowShortPrefix,
/// Allow using this unit as a long prefix with another unit
IsLongPrefix,
/// This unit definition is an alias and will always be replaced with its definition.
Alias,
}

/// This function evaluates a string using the given context. Any evaluation using this
Expand Down
Loading

0 comments on commit bc1c9fe

Please sign in to comment.