| 382 |
382 |
|
//! that one page: audiofiles' library browser and goingson's filters are the
|
| 383 |
383 |
|
//! same shape.
|
| 384 |
384 |
|
//!
|
|
385 |
+ |
//! 0.34.0 gives an interval a description. [`FieldKind::Interval`] and
|
|
386 |
+ |
//! [`Field::upper_name`] say that two values are one question with two ends.
|
|
387 |
+ |
//! Ruled by Max 2026-08-21 against audiofiles' six filter axes and the MNW
|
|
388 |
+ |
//! server's price pair, which is HTML saying the grouping in an ARIA
|
|
389 |
+ |
//! `role="group"` and nowhere else.
|
|
390 |
+ |
//!
|
|
391 |
+ |
//! - **The ends constrain each other, and nothing else in the vocabulary could
|
|
392 |
+ |
//! say so.** Two [`FieldKind::Number`] fields are two questions: a renderer
|
|
393 |
+ |
//! draws two labels with no relationship, and [`Field::error`] attaches to one
|
|
394 |
+ |
//! side of a fault that belongs to both.
|
|
395 |
+ |
//! - **It is not [`FieldKind::Range`]**, which is the reading to resist and the
|
|
396 |
+ |
//! same resistance `Range` needed against `Number`. A range is one value
|
|
397 |
+ |
//! inside an extent; this is two, and the extent is a bound on each rather
|
|
398 |
+ |
//! than the question's meaning.
|
|
399 |
+ |
//! - **Both names are stated rather than derived.** Measured the same day, the
|
|
400 |
+ |
//! two sites disagree about affix order -- `bpm_min`/`bpm_max` against
|
|
401 |
+ |
//! `min_price`/`max_price` -- so any rule renames one of them. One member
|
|
402 |
+ |
//! instead of a naming convention this crate would own forever, and which
|
|
403 |
+ |
//! member a name sits in is what says which end it is.
|
|
404 |
+ |
//! - The crossing rule is not enforced, exactly as [`Field::min`] is not. What
|
|
405 |
+ |
//! the description buys is one place to report the fault rather than two.
|
|
406 |
+ |
//!
|
|
407 |
+ |
//! [`Field::upper_name`] is a member on a struct that is not
|
|
408 |
+ |
//! `#[non_exhaustive]`, so this is a formal break for crates.io and zero
|
|
409 |
+ |
//! call-site edits in the tree: every literal builds on a constructor.
|
|
410 |
+ |
//!
|
| 385 |
411 |
|
//! 0.33.0 gives a number its unit. [`Field::unit`] carries what the value is
|
| 386 |
412 |
|
//! measured in, and [`FieldKind::measurable`] says which kinds read it. Decided
|
| 387 |
413 |
|
//! by Max 2026-08-21 (`32215e21`) against eight sites across four audiofiles
|
| 3258 |
3284 |
|
/// picker (`fb93426b`), where four sliders were hand-rolled against a
|
| 3259 |
3285 |
|
/// vocabulary that could not say what they were.
|
| 3260 |
3286 |
|
Range,
|
|
3287 |
+ |
/// One question with two ends: a lower value and an upper one, submitted
|
|
3288 |
+ |
/// under two names.
|
|
3289 |
+ |
///
|
|
3290 |
+ |
/// "Show me samples between 90 and 130 BPM" has a single answer with two
|
|
3291 |
+ |
/// ends, and the ends constrain each other: a minimum above the maximum is
|
|
3292 |
+ |
/// not a wrong value, it is an empty result nobody asked for. Described as
|
|
3293 |
+ |
/// two [`Number`](Self::Number) fields that is unsayable — nothing says they
|
|
3294 |
+ |
/// are one question, so a renderer draws two controls with two labels and no
|
|
3295 |
+ |
/// relationship, and [`Field::error`] can only be attached to one side of a
|
|
3296 |
+ |
/// fault that belongs to both.
|
|
3297 |
+ |
///
|
|
3298 |
+ |
/// Not [`Range`](Self::Range), which was the reading to resist and the
|
|
3299 |
+ |
/// resistance is the same one `Range` itself needed against `Number`. A
|
|
3300 |
+ |
/// range describes *one* value inside an extent; this describes two, and the
|
|
3301 |
+ |
/// extent is a bound on each rather than the question's meaning. The two
|
|
3302 |
+ |
/// come apart in the answer: a range has a value, an interval has a pair,
|
|
3303 |
+ |
/// and either end may be absent while the other stands.
|
|
3304 |
+ |
///
|
|
3305 |
+ |
/// # It states both names
|
|
3306 |
+ |
///
|
|
3307 |
+ |
/// [`Field::name`] is the lower end and [`Field::upper_name`] is the upper
|
|
3308 |
+ |
/// one, stated rather than derived. Measured 2026-08-21, the two sites in
|
|
3309 |
+ |
/// this tree disagree about affix order — audiofiles submits `bpm_min` /
|
|
3310 |
+ |
/// `bpm_max` and the MNW server submits `min_price` / `max_price` — so any
|
|
3311 |
+ |
/// derived rule picks one and renames the other's parameters. One member
|
|
3312 |
+ |
/// instead of a naming convention this crate would then own forever.
|
|
3313 |
+ |
///
|
|
3314 |
+ |
/// Direction is carried by which member the name sits in, so nothing
|
|
3315 |
+ |
/// separate says which end is which.
|
|
3316 |
+ |
///
|
|
3317 |
+ |
/// # What it does not enforce
|
|
3318 |
+ |
///
|
|
3319 |
+ |
/// The crossing rule. A lower end above the upper one is describable here
|
|
3320 |
+ |
/// and always was, exactly as an out-of-[`min`](Field::min) number is: this
|
|
3321 |
+ |
/// crate carries constraints and never checks them, and deciding a value is
|
|
3322 |
+ |
/// wrong stays with whoever validated. What the description buys is that the
|
|
3323 |
+ |
/// fault now has one place to be reported rather than two.
|
|
3324 |
+ |
///
|
|
3325 |
+ |
/// # Both ends take the same facts
|
|
3326 |
+ |
///
|
|
3327 |
+ |
/// [`min`](Field::min), [`max`](Field::max), [`step`](Field::step) and
|
|
3328 |
+ |
/// [`unit`](Field::unit) describe the axis rather than one end of it, so
|
|
3329 |
+ |
/// they are read once and applied to both. Six of audiofiles' filter axes
|
|
3330 |
+ |
/// are exactly this: one extent, one unit, one granularity, two ends.
|
|
3331 |
+ |
///
|
|
3332 |
+ |
/// The bounds are optional here, unlike `Range`. They are a rule the answer
|
|
3333 |
+ |
/// is checked against rather than the control's extent, which is
|
|
3334 |
+ |
/// [`Number`](Self::Number)'s arrangement and not a slider's.
|
|
3335 |
+ |
///
|
|
3336 |
+ |
/// Added 0.34.0, ruled by Max 2026-08-21, from audiofiles' six filter axes
|
|
3337 |
+ |
/// and the MNW server's price pair.
|
|
3338 |
+ |
Interval,
|
| 3261 |
3339 |
|
/// An email address.
|
| 3262 |
3340 |
|
///
|
| 3263 |
3341 |
|
/// Distinct from [`Text`](Self::Text) because the distinction is not
|
| 3533 |
3611 |
|
|
| 3534 |
3612 |
|
/// Whether the value is a quantity, so [`Field::unit`] means something.
|
| 3535 |
3613 |
|
///
|
| 3536 |
|
- |
/// The two numeric kinds and nothing else. A date is a quantity in the sense
|
|
3614 |
+ |
/// The numeric kinds and nothing else. A date is a quantity in the sense
|
| 3537 |
3615 |
|
/// that it is ordered, and it is not one in the sense that matters here:
|
| 3538 |
3616 |
|
/// its unit is fixed by the kind, so `Date` carrying `days` would be the
|
| 3539 |
3617 |
|
/// description restating what [`kind`](Field::kind) already said.
|
| 3542 |
3620 |
|
/// renderers ask this before they decide where a unit goes, and a
|
| 3543 |
3621 |
|
/// `matches!` per renderer is where the next measurable kind goes missing.
|
| 3544 |
3622 |
|
///
|
| 3545 |
|
- |
/// Added 0.33.0 with [`Field::unit`].
|
|
3623 |
+ |
/// Added 0.33.0 with [`Field::unit`]. [`Interval`](Self::Interval) joined at
|
|
3624 |
+ |
/// 0.34.0: an axis is measured in something and both its ends are in it.
|
| 3546 |
3625 |
|
#[must_use]
|
| 3547 |
3626 |
|
pub const fn measurable(self) -> bool {
|
| 3548 |
|
- |
matches!(self, Self::Number | Self::Range)
|
|
3627 |
+ |
matches!(self, Self::Number | Self::Range | Self::Interval)
|
| 3549 |
3628 |
|
}
|
| 3550 |
3629 |
|
}
|
| 3551 |
3630 |
|
|
| 3965 |
4044 |
|
/// What kind of value it takes.
|
| 3966 |
4045 |
|
pub kind: FieldKind,
|
| 3967 |
4046 |
|
/// The name the value is submitted under.
|
|
4047 |
+ |
///
|
|
4048 |
+ |
/// The *lower* end's name for a [`FieldKind::Interval`], whose upper end is
|
|
4049 |
+ |
/// [`upper_name`](Self::upper_name). Every other kind submits one value and
|
|
4050 |
+ |
/// this is the whole of it.
|
| 3968 |
4051 |
|
pub name: &'a str,
|
|
4052 |
+ |
/// The name a [`FieldKind::Interval`]'s upper end is submitted under.
|
|
4053 |
+ |
///
|
|
4054 |
+ |
/// [`None`] for every other kind, and sayable-and-ignored there the way
|
|
4055 |
+ |
/// [`options`](Self::options) is on a kind that offers none.
|
|
4056 |
+ |
///
|
|
4057 |
+ |
/// Stated rather than derived from [`name`](Self::name), and
|
|
4058 |
+ |
/// [`FieldKind::Interval`] carries the measurement that decided it: the two
|
|
4059 |
+ |
/// sites in this tree disagree about affix order, so a derived rule would
|
|
4060 |
+ |
/// rename one of them. Which member a name sits in is also what says which
|
|
4061 |
+ |
/// end it is, so nothing separate carries the direction.
|
|
4062 |
+ |
///
|
|
4063 |
+ |
/// An interval missing it is an interval with one end that can be submitted,
|
|
4064 |
+ |
/// which is a description a renderer may draw honestly and no better than
|
|
4065 |
+ |
/// that. [`Field::interval`] is what makes forgetting it unsayable, on the
|
|
4066 |
+ |
/// same footing as [`Field::range`] and its bounds.
|
|
4067 |
+ |
///
|
|
4068 |
+ |
/// Added 0.34.0.
|
|
4069 |
+ |
pub upper_name: Option<&'a str>,
|
| 3969 |
4070 |
|
/// What the user is asked for.
|
| 3970 |
4071 |
|
pub label: &'a str,
|
| 3971 |
4072 |
|
/// Standing help, shown whether or not anything is wrong.
|
| 4122 |
4223 |
|
Self {
|
| 4123 |
4224 |
|
kind,
|
| 4124 |
4225 |
|
name,
|
|
4226 |
+ |
upper_name: None,
|
| 4125 |
4227 |
|
label,
|
| 4126 |
4228 |
|
hint: None,
|
| 4127 |
4229 |
|
error: None,
|
| 4160 |
4262 |
|
}
|
| 4161 |
4263 |
|
}
|
| 4162 |
4264 |
|
|
|
4265 |
+ |
/// One question with two ends, taking the name each end submits under.
|
|
4266 |
+ |
///
|
|
4267 |
+ |
/// A constructor for [`range`](Self::range)'s reason inverted: a range's
|
|
4268 |
+ |
/// bounds are what a call site cannot forget, and an interval's second name
|
|
4269 |
+ |
/// is. An interval built through [`new`](Self::new) has an upper end with
|
|
4270 |
+ |
/// nowhere to be submitted, and nothing downstream can invent one, so taking
|
|
4271 |
+ |
/// it as an argument is what makes that unsayable.
|
|
4272 |
+ |
///
|
|
4273 |
+ |
/// The extent, the granularity and the unit stay members. They describe the
|
|
4274 |
+ |
/// axis rather than either end and they are genuinely optional, which is
|
|
4275 |
+ |
/// [`FieldKind::Number`]'s arrangement and the one an interval takes.
|
|
4276 |
+ |
#[must_use]
|
|
4277 |
+ |
pub const fn interval(name: &'a str, upper_name: &'a str, label: &'a str) -> Self {
|
|
4278 |
+ |
Self {
|
|
4279 |
+ |
upper_name: Some(upper_name),
|
|
4280 |
+ |
..Self::new(FieldKind::Interval, name, label)
|
|
4281 |
+ |
}
|
|
4282 |
+ |
}
|
|
4283 |
+ |
|
| 4163 |
4284 |
|
/// A file field, taking the given accept list.
|
| 4164 |
4285 |
|
///
|
| 4165 |
4286 |
|
/// The fourth under-described kind and it gets a constructor for
|
| 4929 |
5050 |
|
}
|
| 4930 |
5051 |
|
|
| 4931 |
5052 |
|
#[test]
|
| 4932 |
|
- |
fn only_the_two_numeric_kinds_are_measurable() {
|
|
5053 |
+ |
fn only_the_numeric_kinds_are_measurable() {
|
| 4933 |
5054 |
|
assert!(FieldKind::Number.measurable());
|
| 4934 |
5055 |
|
assert!(FieldKind::Range.measurable());
|
|
5056 |
+ |
// An axis is measured in something and both its ends are in it, so the
|
|
5057 |
+ |
// unit is read once for the pair rather than per end.
|
|
5058 |
+ |
assert!(FieldKind::Interval.measurable());
|
| 4935 |
5059 |
|
// A date is ordered and is not a quantity with a unit to choose: its
|
| 4936 |
5060 |
|
// unit is fixed by the kind, so saying one would restate `kind`.
|
| 4937 |
5061 |
|
for kind in [
|
| 4959 |
5083 |
|
assert!(measured.kind.measurable());
|
| 4960 |
5084 |
|
}
|
| 4961 |
5085 |
|
|
|
5086 |
+ |
#[test]
|
|
5087 |
+ |
fn an_interval_states_both_ends_names() {
|
|
5088 |
+ |
// Stated rather than derived: the two measured sites disagree about
|
|
5089 |
+ |
// affix order, so a rule here would rename one of them.
|
|
5090 |
+ |
let suffixed = Field::interval("bpm_min", "bpm_max", "BPM");
|
|
5091 |
+ |
assert_eq!(suffixed.name, "bpm_min");
|
|
5092 |
+ |
assert_eq!(suffixed.upper_name, Some("bpm_max"));
|
|
5093 |
+ |
let prefixed = Field::interval("min_price", "max_price", "Price");
|
|
5094 |
+ |
assert_eq!(prefixed.name, "min_price");
|
|
5095 |
+ |
assert_eq!(prefixed.upper_name, Some("max_price"));
|
|
5096 |
+ |
assert_eq!(suffixed.kind, FieldKind::Interval);
|
|
5097 |
+ |
}
|
|
5098 |
+ |
|
|
5099 |
+ |
#[test]
|
|
5100 |
+ |
fn every_other_kind_has_no_upper_end() {
|
|
5101 |
+ |
// Additive: absent is what every field described before 0.34.0 meant.
|
|
5102 |
+ |
for kind in [FieldKind::Text, FieldKind::Number, FieldKind::Range] {
|
|
5103 |
+ |
assert_eq!(Field::new(kind, "n", "N").upper_name, None, "{kind:?}");
|
|
5104 |
+ |
}
|
|
5105 |
+ |
assert_eq!(Field::range("t", "T", "0", "1").upper_name, None);
|
|
5106 |
+ |
}
|
|
5107 |
+ |
|
|
5108 |
+ |
#[test]
|
|
5109 |
+ |
fn an_interval_owes_no_bounds_and_takes_the_axis_facts_once() {
|
|
5110 |
+ |
// A range's bounds are the control's extent and are owed; an interval's
|
|
5111 |
+ |
// are a rule on each end, which is Number's arrangement.
|
|
5112 |
+ |
let plain = Field::interval("bpm_min", "bpm_max", "BPM");
|
|
5113 |
+ |
assert!(!plain.bounded());
|
|
5114 |
+ |
let axis = Field {
|
|
5115 |
+ |
min: Some("0"),
|
|
5116 |
+ |
max: Some("300"),
|
|
5117 |
+ |
step: Some("1"),
|
|
5118 |
+ |
unit: Some("BPM"),
|
|
5119 |
+ |
..Field::interval("bpm_min", "bpm_max", "BPM")
|
|
5120 |
+ |
};
|
|
5121 |
+ |
assert!(axis.bounded());
|
|
5122 |
+ |
assert_eq!(axis.unit, Some("BPM"));
|
|
5123 |
+ |
// Nothing here checks the crossing rule, exactly as nothing checks
|
|
5124 |
+ |
// `min` for a number: the description carries constraints and whoever
|
|
5125 |
+ |
// validated decides a value is wrong.
|
|
5126 |
+ |
assert!(axis.error.is_none());
|
|
5127 |
+ |
}
|
|
5128 |
+ |
|
| 4962 |
5129 |
|
#[test]
|
| 4963 |
5130 |
|
fn only_a_subtree_prunes_and_only_the_listing_modes_offer_values() {
|
| 4964 |
5131 |
|
// Excluding a value from a flat facet is the same fact as not picking
|