ixa_fips/fips_code.rs
1//! Defines the `FIPSCode` types to represent FIPS geographic region codes (and “code fragments”) very efficiently.
2//!
3//! # Encoding Scheme
4//!
5//! A table of how FIPS Geo IDs are structured is provided in the module-level documentation for [`crate::parser`]
6//! (slightly modified from
7//! [the source table in the standard](https://www.census.gov/programs-surveys/geography/guidance/geo-identifiers.html)).
8//! The rows in the table up to and including Block (that is, all but the last five rows) form a linear order with
9//! respect to prefix inclusion ("is prefix of"). This encoding scheme is for these codes. The last four rows are
10//! treated separately.
11//!
12//! In the following table, we describe the data "fragments" and their storage requirements.
13//!
14//! | | **Decimal Digits** | **Actual Max Value** | **Bits** | **Capacity (`2^bits - 1`)** |
15//! |:--------------------------------- | ------------------:| --------------------:| --------:| ------------------------------:|
16//! | **Sate** | 2 | 56 | 7 | 127 |
17//! | **County** | 3 | 840 | 10 | 1,023 |
18//! | **Tract** | 6 | 990,101 | 20 | 1,048,575 |
19//! | **Subtotal** | | | **37** | **Bits needed for tract code** |
20//! | | | | | |
21//! | **Monotonically Increasing Id's** | | | | |
22//! | **homeId** | 4 | 9,999 | 14 | 16,383 |
23//! | **publicschoolId** | 3 | 999 | 10 | 1,023 |
24//! | **privateschoolId** | 4 | 1,722 | 11 | 2,047 |
25//! | **workplaceId** | 5 | 14,938 | 14 | 16,383 |
26//! | **Max:** | | | **14** | |
27//! | **Total:** | | | **51** | |
28//!
29//! State codes for states have values <= 56, but there are "state codes" for outlying areas, some historic codes, and
30//! maritime extension codes in use in the wild. We therefore use an extra bit than strictly required to represent it.
31//! To the 51 bits apparently required to store this data we add an additional 4 bits for a category tag to distinguish
32//! between home, public school, private school, workplace, and cencus tract, a field useful for representing ASPR
33//! synthetic population data, for example. Only 2 bits are required to distinguish these 4 categories, so the additional
34//! 2 bits are left unused / for future use.
35//!
36//! We encode this data into a `u64` as follows:
37//!
38//! | **Data** | **State** | **County** | **Tract** | **Category Tag** | **Monotonically increasing ID number** | **Reserved / Unused** |
39//! |:---------------------- | -------------:| ----------:| ---------:| -----------------:| --------------------------------------:| ---------------------:|
40//! | **Bits** | 63…57 | 57…47 | 46…27 | 26…23 | 22…9 | 8…0 |
41//! | **Ex. Value** | `AK`, `AZ`, … | 258 | 223,100 | `Home`, `Work`, … | 12,345 | 0 |
42//! | **Bit Count** | 7 | 10 | 20 | 4 | 14 | 9 |
43//! | **Capacity** | 128 | 1,024 | 1,048,576 | 16 | 16,384 | 512 |
44//! | **Decimal Digits** | 2 | 3 | 6 | - | 3 to 5 | - |
45//! | **Max Observed Value** | 56 | 840 | 990,101 | 4 | 14,938 | - |
46//!
47//! Observe that:
48//!
49//! - We give the "category tag" 4 bits to allow up to 16 distinct categories. In some applications this field might be unused.
50//! - The least significant 9 bits is completely unused by this encoding. It may be used for application-specific storage.
51//! - The field for ID number only requires 10 bits for `publicschoolId`, for example. That is, the storage it requires
52//! depends on the category tag.
53//! - The category tag is encoded after the tract code but before the ID field so that numerical ordering coincides with
54//! the hierarchical ordering.
55//! - Likewise, the unused 9 bits are the least significant bits so that numerical ordering coincides with the
56//! hierarchical ordering modulo those bits.
57//!
58//! # Nonhierarchical FIPS Codes
59//!
60//! The encoding of the previous section excludes the nonhierarchical codes of the last five rows from the first table
61//! above:
62//!
63//! - Places
64//! - Congressional District (113th Congress)
65//! - State Legislative District (Upper Chamber)
66//! - State Legislative District (Lower Chamber)
67//! - ZCTA
68//!
69//! We could easily accommodate these codes as well in a variety of ways, e.g.:
70//! - assign each of these a category tag and store their corresponding code fragments in the ID field
71//! - use the 14 bits of the ID field and the unused 10 least significant bits, allowing the category tag to remain
72//! orthogonal
73//!
74//! We leave them unspecified until we have a use case for them.
75use std::cmp::Ordering;
76use std::fmt::{Debug, Display, Formatter};
77use std::num::NonZero;
78
79use crate::errors::FIPSError;
80use crate::states::USState;
81use crate::{
82 CountyCode, DataCode, IdCode, SettingCategoryCode, StateCode, TractCode, CATEGORY_OFFSET,
83 COUNTY_OFFSET, FOURTEEN_BIT_MASK, FOUR_BIT_MASK, ID_OFFSET, NINE_BIT_MASK, STATE_OFFSET,
84 TEN_BIT_MASK, TRACT_OFFSET, TWENTY_BIT_MASK,
85};
86
87/// Encodes a hierarchical FIPS geographic region code in 64 bits. Excludes the nonhierarchical codes places,
88/// congressional or state legislative districts, and ZIP code tabulation areas. (See the
89/// [module level documentation](`crate::fips_code`).)
90#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
91pub struct FIPSCode(NonZero<u64>);
92
93impl FIPSCode {
94 // region Constructors
95 /// Constructs a new [`FIPSCode`] from a [`USState`]. Unlike the other constructors, this constructor is infallible.
96 #[must_use]
97 pub fn with_state(state: USState) -> Self {
98 Self::new(state.into(), 0, 0, 0, 0, 0).unwrap()
99 }
100 /// Constructs a new [`FIPSCode`].
101 /// Returns `Err(())` if the data provided is out of range.
102 pub fn with_state_code(state_code: StateCode) -> Result<Self, FIPSError> {
103 Self::new(state_code, 0, 0, 0, 0, 0)
104 }
105 /// Constructs a new [`FIPSCode`].
106 /// Returns `Err(())` if the data provided is out of range.
107 pub fn with_county(state: StateCode, county: CountyCode) -> Result<Self, FIPSError> {
108 Self::new(state, county, 0, 0, 0, 0)
109 }
110 /// Constructs a new [`FIPSCode`].
111 /// Returns `Err(())` if the data provided is out of range.
112 pub fn with_tract(
113 state: StateCode,
114 county: CountyCode,
115 tract: TractCode,
116 ) -> Result<Self, FIPSError> {
117 Self::new(state, county, tract, 0, 0, 0)
118 }
119 /// Constructs a new [`FIPSCode`].
120 /// Returns `Err(())` if the data provided is out of range.
121 pub fn with_category(
122 state: StateCode,
123 county: CountyCode,
124 tract: TractCode,
125 category: SettingCategoryCode,
126 ) -> Result<Self, FIPSError> {
127 Self::new(state, county, tract, category, 0, 0)
128 }
129
130 pub fn new(
131 state: StateCode,
132 county: CountyCode,
133 tract: TractCode,
134 category: SettingCategoryCode,
135 id: IdCode,
136 data: DataCode,
137 ) -> Result<Self, FIPSError> {
138 let encoded: u64 = Self::encode_state(state)?
139 | Self::encode_county(county)?
140 | Self::encode_tract(tract)?
141 | Self::encode_category(category)?
142 | Self::encode_id(id)?
143 | Self::encode_data(data)?;
144 // At the very least, `USState.encode()` will return a non-zero value, so this unwrapping is safe.
145 let encoded = NonZero::new(encoded).unwrap();
146 Ok(Self(encoded))
147 }
148 // endregion Constructors
149
150 // region Accessors
151
152 /// Returns the FIPS STATE as a [`USState`] enum variant.
153 /// Returns `Err(())` if [`USState`] cannot represent the state code. Use [`FIPSCode::state_code()`] to
154 /// retrieve the state code in this case.
155 #[inline(always)]
156 pub fn state(&self) -> Result<USState, FIPSError> {
157 USState::decode(self.state_code())
158 }
159
160 /// Returns the FIPS STATE code as a [`StateCode`] (a `u8`)
161 #[inline(always)]
162 #[must_use]
163 pub fn state_code(&self) -> StateCode {
164 // The state code occupies the 7 most significant bits, bits 57..63
165 (self.0.get() >> STATE_OFFSET) as StateCode
166 }
167
168 /// Returns the numeric FIPS COUNTY code
169 #[inline(always)]
170 #[must_use]
171 pub fn county_code(&self) -> CountyCode {
172 // The county code occupies the 10 bits from bits 47..56
173 ((self.0.get() >> COUNTY_OFFSET) as CountyCode) & TEN_BIT_MASK
174 }
175
176 /// Returns the numeric FIPS CENSUS TRACT code
177 #[inline(always)]
178 #[must_use]
179 pub fn census_tract_code(&self) -> TractCode {
180 // The census tract code occupies the 20 bits from bits 27..46
181 ((self.0.get() >> TRACT_OFFSET) as TractCode) & TWENTY_BIT_MASK
182 }
183
184 /// Returns the numeric SETTING CATEGORY code
185 #[inline(always)]
186 #[must_use]
187 pub fn category_code(&self) -> SettingCategoryCode {
188 // The category code occupies the 4 bits from bits 23..26
189 ((self.0.get() >> CATEGORY_OFFSET) as SettingCategoryCode) & FOUR_BIT_MASK
190 }
191
192 /// Returns the monotonically increasing ID number as an [`IdCode`]
193 #[inline(always)]
194 #[must_use]
195 pub fn id(&self) -> IdCode {
196 // The ID number occupies the 14 bits from bits 9..22
197 ((self.0.get() >> ID_OFFSET) as IdCode) & FOURTEEN_BIT_MASK
198 }
199
200 /// Returns the unused data region occupying the 9 LSB
201 #[inline(always)]
202 #[must_use]
203 pub fn data(&self) -> DataCode {
204 self.0.get() as DataCode & NINE_BIT_MASK
205 }
206 // endregion Accessors
207
208 // region Setters
209
210 /// Creates a copy of `self` with the FIPS STATE set to `state`.
211 #[must_use]
212 pub fn set_state(&self, state: USState) -> Self {
213 self.set_state_code(state.into()).unwrap()
214 }
215
216 /// Creates a copy of `self` with the FIPS STATE set to `state`.
217 pub fn set_state_code(&self, state_code: StateCode) -> Result<Self, FIPSError> {
218 let mut expanded = ExpandedFIPSCode::from_fips_code(*self);
219 expanded.state = state_code;
220 expanded.to_fips_code()
221 }
222
223 /// Creates a copy of `self` with the FIPS COUNTY set to `county`.
224 pub fn set_county(&self, county: CountyCode) -> Result<Self, FIPSError> {
225 let mut expanded = ExpandedFIPSCode::from_fips_code(*self);
226 expanded.county = county;
227 expanded.to_fips_code()
228 }
229
230 /// Creates a copy of `self` with the FIPS CENSUS TRACT set to `tract`.
231 pub fn set_tract(&self, tract: TractCode) -> Result<Self, FIPSError> {
232 let mut expanded = ExpandedFIPSCode::from_fips_code(*self);
233 expanded.tract = tract;
234 expanded.to_fips_code()
235 }
236
237 /// Creates a copy of `self` with the setting category set to `category`.
238 pub fn set_category(&self, category: SettingCategoryCode) -> Result<Self, FIPSError> {
239 let mut expanded = ExpandedFIPSCode::from_fips_code(*self);
240 expanded.category = category;
241 expanded.to_fips_code()
242 }
243
244 /// Creates a copy of `self` with the ID number set to `id`.
245 pub fn set_id(&self, id: IdCode) -> Result<Self, FIPSError> {
246 let mut expanded = ExpandedFIPSCode::from_fips_code(*self);
247 expanded.id = id;
248 expanded.to_fips_code()
249 }
250
251 /// Creates a copy of `self` with the unused data region set to `data`.
252 pub fn set_data(&self, data: DataCode) -> Result<Self, FIPSError> {
253 let mut expanded = ExpandedFIPSCode::from_fips_code(*self);
254 expanded.data = data;
255 expanded.to_fips_code()
256 }
257
258 // endregion Setters
259
260 /// Sets the unused data region occupying the 10 LSB in place.
261 /// Returns `Ok(())` if `data` is in range, `Err(FIPSError)` otherwise.
262 #[inline(always)]
263 pub fn set_data_in_place(&mut self, data: DataCode) -> Result<(), FIPSError> {
264 if data <= NINE_BIT_MASK {
265 let inverse_mask = !(NINE_BIT_MASK as u64);
266 let code = (self.0.get() & inverse_mask) | ((data & NINE_BIT_MASK) as u64);
267 // The result is guaranteed to be nonzero if the original code was valid, so unwrap will succeed.
268 self.0 = NonZero::new(code).unwrap();
269 Ok(())
270 } else {
271 Err(FIPSError::from_data_code(data))
272 }
273 }
274
275 /// Compares the given values without respect to the data region (the Least Significant Bits). Use the usual
276 /// equality operators for comparing `FIPSCode`s including the data region.
277 #[inline(always)]
278 #[must_use]
279 pub fn compare_non_data(&self, other: Self) -> Ordering {
280 let inverse_mask = !(NINE_BIT_MASK as u64);
281 let this = self.0.get() & inverse_mask;
282 let other = other.0.get() & inverse_mask;
283
284 this.cmp(&other)
285 }
286
287 // region Encoding
288 // It is convenient to factor out the encode operations into their own functions.
289 // These functions take numeric values and return encoded `u64` values. To encode
290 // enum variants, call the `encode` function on the enum variant.
291
292 #[inline(always)]
293 fn encode_state(state: StateCode) -> Result<u64, FIPSError> {
294 // Validate: Two decimal digits
295 if state <= 99 && state != 0 {
296 Ok((state as u64) << STATE_OFFSET)
297 } else {
298 Err(FIPSError::from_state_code(state))
299 }
300 }
301
302 #[inline(always)]
303 fn encode_county(county: CountyCode) -> Result<u64, FIPSError> {
304 // Validate: Three decimal digits
305 if county <= 999 {
306 Ok((county as u64) << COUNTY_OFFSET)
307 } else {
308 Err(FIPSError::from_county_code(county))
309 }
310 }
311
312 #[inline(always)]
313 fn encode_tract(tract: TractCode) -> Result<u64, FIPSError> {
314 // Validate: Six decimal digits
315 if tract <= 999_999 {
316 Ok((tract as u64) << TRACT_OFFSET)
317 } else {
318 Err(FIPSError::from_tract_code(tract))
319 }
320 }
321
322 #[inline(always)]
323 fn encode_category(setting_category: SettingCategoryCode) -> Result<u64, FIPSError> {
324 // Validate
325 if setting_category <= FOUR_BIT_MASK {
326 Ok((setting_category as u64) << CATEGORY_OFFSET)
327 } else {
328 Err(FIPSError::from_setting_category_code(setting_category))
329 }
330 }
331
332 #[inline(always)]
333 fn encode_id(id: IdCode) -> Result<u64, FIPSError> {
334 // Validate
335 if id <= FOURTEEN_BIT_MASK {
336 Ok((id as u64) << ID_OFFSET)
337 } else {
338 Err(FIPSError::from_id_code(id))
339 }
340 }
341
342 #[inline(always)]
343 fn encode_data(data: DataCode) -> Result<u64, FIPSError> {
344 // Validate
345 if data <= NINE_BIT_MASK {
346 Ok(data as u64)
347 } else {
348 Err(FIPSError::from_data_code(data))
349 }
350 }
351 // endregion Encoding
352}
353
354impl Display for FIPSCode {
355 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
356 write!(f, "{}", ExpandedFIPSCode::from_fips_code(*self))
357 }
358}
359
360impl Debug for FIPSCode {
361 /// Format the code as a string of hex digits with fields separated by dashes. Note that this is different
362 /// from serializing to the original FIPS code encoding. Use `format_as_fips_code`/`format_as_fips_code`
363 /// for that purpose.
364 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
365 write!(f, "{:02}", self.state_code())?;
366 write!(f, "-{:03}", self.county_code())?;
367 write!(f, "-{:06}", self.census_tract_code())?;
368 write!(f, "-{:01}", self.category_code())?;
369 write!(f, "-{:05}", self.id())?;
370 write!(f, "-{:03x}", self.data())?;
371 Ok(())
372 }
373}
374
375/// A struct that holds an expanded version of a [`FIPSCode`] in which all fields are represented by
376/// their associated numeric types.
377///
378/// It is up to the client code to ensure the field values are within
379/// range. See the module level docs for range constraints.
380///
381/// This struct is useful for converting raw data to/from a [`FIPSCode`] for temporary direct field access.
382#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
383pub struct ExpandedFIPSCode {
384 pub state: StateCode,
385 pub county: CountyCode,
386 pub tract: TractCode,
387 pub category: SettingCategoryCode,
388 pub id: IdCode,
389 pub data: DataCode,
390}
391
392impl ExpandedFIPSCode {
393 #[must_use]
394 pub fn from_fips_code(fips_code: FIPSCode) -> Self {
395 Self {
396 state: fips_code.state_code(),
397 county: fips_code.county_code(),
398 tract: fips_code.census_tract_code(),
399 category: fips_code.category_code(),
400 id: fips_code.id(),
401 data: fips_code.data(),
402 }
403 }
404
405 pub fn to_fips_code(&self) -> Result<FIPSCode, FIPSError> {
406 FIPSCode::new(
407 self.state,
408 self.county,
409 self.tract,
410 self.category,
411 self.id,
412 self.data,
413 )
414 }
415}
416
417impl Display for ExpandedFIPSCode {
418 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
419 // Format the state if possible
420 if let Ok(state) = USState::decode(self.state) {
421 write!(f, "state: {}", state.as_ref())?;
422 } else {
423 write!(f, "state: {}", self.state)?;
424 }
425 // For the remaining fields, only print them if they are nonzero
426 if self.county != 0 {
427 write!(f, ", county: {}", self.county)?;
428 }
429 if self.tract != 0 {
430 write!(f, ", tract: {}", self.tract)?;
431 }
432 if self.category != 0 {
433 write!(f, ", setting: {}", self.category)?;
434 }
435 if self.id != 0 {
436 write!(f, ", id: {}", self.id)?;
437 }
438 if self.data != 0 {
439 write!(f, ", data field: {}", self.data)?;
440 }
441
442 Ok(())
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 #[repr(u8)]
451 enum SettingCategory {
452 Unspecified = 0,
453 Home,
454 School,
455 Work,
456 CensusTract,
457 }
458
459 impl From<SettingCategory> for SettingCategoryCode {
460 fn from(value: SettingCategory) -> Self {
461 value as SettingCategoryCode
462 }
463 }
464
465 #[test]
466 fn test_data_ranges() {
467 // Encode functions
468 assert!(FIPSCode::encode_state(99).is_ok());
469 assert!(FIPSCode::encode_state(100).is_err());
470 assert!(FIPSCode::encode_county(999).is_ok());
471 assert!(FIPSCode::encode_county(1000).is_err());
472 assert!(FIPSCode::encode_tract(999_999).is_ok());
473 assert!(FIPSCode::encode_tract(1_000_000).is_err());
474 assert!(FIPSCode::encode_category(FOUR_BIT_MASK).is_ok());
475 assert!(FIPSCode::encode_category(FOUR_BIT_MASK + 1).is_err());
476 assert!(FIPSCode::encode_id(FOURTEEN_BIT_MASK).is_ok());
477 assert!(FIPSCode::encode_id(FOURTEEN_BIT_MASK + 1).is_err());
478 assert!(FIPSCode::encode_data(NINE_BIT_MASK).is_ok());
479 assert!(FIPSCode::encode_data(NINE_BIT_MASK + 1).is_err());
480 // Constructors
481 assert!(FIPSCode::with_state_code(1).is_ok());
482 assert!(FIPSCode::with_state_code(0).is_err());
483 assert!(FIPSCode::with_state_code(100).is_err());
484 assert!(FIPSCode::with_county(1, 0).is_ok());
485 assert!(FIPSCode::with_county(1, 1000).is_err());
486 assert!(FIPSCode::with_tract(1, 0, 0).is_ok());
487 assert!(FIPSCode::with_tract(1, 0, 1_000_000).is_err());
488 assert!(FIPSCode::with_category(1, 0, 0, 0).is_ok());
489 assert!(FIPSCode::with_category(1, 0, 0, FOUR_BIT_MASK + 1).is_err());
490 }
491
492 #[test]
493 fn fields_round_trip() {
494 let fips_code = FIPSCode::new(
495 USState::TX.into(),
496 123,
497 990101,
498 SettingCategory::Home.into(),
499 14938,
500 123,
501 )
502 .unwrap();
503 assert_eq!(fips_code.state().unwrap(), USState::TX);
504 assert_eq!(fips_code.county_code(), 123);
505 assert_eq!(fips_code.census_tract_code(), 990101);
506 assert_eq!(fips_code.category_code(), SettingCategory::Home.into());
507 assert_eq!(fips_code.id(), 14938);
508 assert_eq!(fips_code.data(), 123);
509 }
510
511 #[test]
512 fn expanded_round_trip() {
513 let fips_code = FIPSCode::new(
514 USState::TX.into(),
515 123,
516 990101,
517 SettingCategory::Home.into(),
518 14938,
519 0x01ff,
520 )
521 .unwrap();
522 let expanded = ExpandedFIPSCode::from_fips_code(fips_code);
523 let result = expanded.to_fips_code().unwrap();
524 assert_eq!(result, fips_code);
525 }
526
527 #[test]
528 fn test_compare_non_data() {
529 let fips_code_a = FIPSCode::new(
530 USState::TX.into(),
531 123,
532 990101,
533 SettingCategory::Home.into(),
534 14938,
535 0x01ff,
536 )
537 .unwrap();
538 let fips_code_b = FIPSCode::new(
539 USState::TX.into(),
540 123,
541 990101,
542 SettingCategory::Home.into(),
543 14938,
544 0x00ff,
545 )
546 .unwrap();
547
548 assert_eq!(fips_code_a.compare_non_data(fips_code_b), Ordering::Equal);
549 assert_eq!(fips_code_a.cmp(&fips_code_b), Ordering::Greater);
550 }
551
552 #[test]
553 fn test_set_id() {
554 // Exercises case that triggered a bug that causes a panic.
555 let fips_code = FIPSCode::with_category(
556 USState::AK.into(),
557 0,
558 0,
559 SettingCategory::CensusTract.into(),
560 )
561 .unwrap();
562
563 let other_fips_code = fips_code.set_id(0).unwrap();
564 assert_eq!(fips_code, other_fips_code);
565 }
566
567 #[test]
568 fn test_fips_error() {
569 let err = FIPSError::new("foo", 1, 2, 3);
570 assert_eq!(
571 format!("{}", err),
572 "value 1 provided for foo is outside valid range of 2..3"
573 );
574
575 let e = USState::decode(58).unwrap_err();
576 assert_eq!(
577 e.to_string(),
578 "value 58 provided for USState Code is outside valid range of 1..57"
579 );
580
581 let e = FIPSCode::encode_state(100).unwrap_err();
582 assert_eq!(
583 e.to_string(),
584 "value 100 provided for StateCode is outside valid range of 1..100"
585 );
586
587 let e = FIPSCode::encode_state(0).unwrap_err();
588 assert_eq!(
589 e.to_string(),
590 "value 0 provided for StateCode is outside valid range of 1..100"
591 );
592
593 let e = FIPSCode::encode_county(1000).unwrap_err();
594 assert_eq!(
595 e.to_string(),
596 "value 1000 provided for CountyCode is outside valid range of 0..1000"
597 );
598
599 let e = FIPSCode::encode_tract(1_000_000).unwrap_err();
600 assert_eq!(
601 e.to_string(),
602 "value 1000000 provided for TractCode is outside valid range of 0..1000000"
603 );
604
605 let e = FIPSCode::encode_category(16).unwrap_err();
606 assert_eq!(
607 e.to_string(),
608 "value 16 provided for SettingCategoryCode is outside valid range of 0..16"
609 );
610
611 let e = FIPSCode::encode_id(16_384).unwrap_err();
612 assert_eq!(
613 e.to_string(),
614 "value 16384 provided for IdCode is outside valid range of 0..16384"
615 );
616
617 let e = FIPSCode::encode_data(512).unwrap_err();
618 assert_eq!(
619 e.to_string(),
620 "value 512 provided for DataCode is outside valid range of 0..512"
621 );
622 }
623}