\RequirePackage{expl3}[2024/01/25]
\ProvidesExplPackage{non-decimal-units}{2024/01/25}{1.0.1}{Macros for displaying and manipulating historical non-decimal units}

% License: CC-BY-SA 4.0
% Author: Mikkel Eide Eriksen <mikkel.eriksen@gmail.com>

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Key setup
% 

\keys_define:nn { non-decimal-units }
{
	% nil/zero
	replace~nil~with 			.tl_set:N		= \l__ndu_replace_nil_with_tl ,
	treat~zero~as~nil 			.bool_set:N 	= \l__ndu_treat_zero_as_nil_bool ,
	treat~zero~as~nil 			.default:n 		= true ,

	% formatting
	unit~separator 				.tl_set:N		= \l__ndu_unit_separator_tl ,
	unit~separator				.default:n		= { \nobreakspace } ,
	default~format				.tl_set:N		= \l__ndu_default_format_tl ,
	default~format				.default:n		= { \VALUE\nobreakspace\SYMBOL } ,
	display						.choice: ,
	display						.choices:nn		= 
		{ values~only, symbols~only, formatted }
		{ \tl_set_eq:NN \l__ndu_display_choice_tl \l_keys_choice_tl } ,
	display						.default:n		= { formatted } ,
	unit~depth					.tl_set:N		= \l__ndu_unit_depth_tl ,
	use~numprint	 			.bool_set:N 	= \l__ndu_use_numprint_bool ,
	use~numprint	 			.default:n 		= true ,

	% tabular alignment
	aligned						.bool_set:N		= \l__ndu_aligned_bool ,
	aligned						.default:n 		= true ,
	cell~widths					.code:n			= {
			\seq_set_from_clist:Nn \l__ndu_cell_widths_seq {#1}
		} ,
	cell~widths 				.default:n 		= 3em ,

	% math
	normalize 					.bool_set:N		= \l__ndu_normalize_bool ,
	normalize 					.default:n 		= true ,
	current~variable			.tl_set:N		= \l__ndu_current_variable_tl ,
	current~operator			.tl_set:N		= \l__ndu_current_operator_tl ,
	add~to~variable				.meta:n			= {
			current~variable = {#1} ,
			current~operator = {+}
		} ,
	subtract~from~variable		.meta:n			= {
			current~variable = {#1} ,
			current~operator = {-}
		} ,
	
	% environments
	set~aligned~for~environment	.code			= {
			\AtBeginEnvironment{#1}{\nduKeys{aligned}}
		} ,
}

\NewDocumentCommand { \nduKeys } { m } {
	\keys_set:nn { non-decimal-units } { #1 }
}

% set some defaults
\keys_set:nn { non-decimal-units } {
	unit~separator,
	default~format,
	display,
	cell~widths,
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Errors and log messages
% 

\msg_new:nnnn
{ non-decimal-units } { base-unit-exists }
{ A ~ base ~ unit ~ named ~ #1 ~ already ~ exists }
{ You ~ will ~ need ~ to ~ give ~ it ~ another ~ name ~ or ~ load ~ less ~ units. }

\msg_new:nnnn
{ non-decimal-units } { unit-group-exists }
{ A ~ unit ~ group ~ named ~ #1 ~ already ~ exists }
{ You ~ will ~ need ~ to ~ give ~ it ~ another ~ name ~ or ~ load ~ less ~ units. }

\msg_new:nnnn
{ non-decimal-units } { missing-base-unit }
{ No ~ base ~ unit ~ exists ~ named ~ #1 }
{ You ~ will ~ need ~ to ~ add ~ it ~ with ~ nduNewBaseUnit. }

\msg_new:nnnn
{ non-decimal-units } { missing-unit-group }
{ No ~ unit ~ group ~ exists ~ named ~ #1 }
{ You ~ will ~ need ~ to ~ add ~ it ~ with ~ nduNewUnitGroup. }

\msg_new:nnnn
{ non-decimal-units } { missing-factor }
{ No ~ conversion ~ factor ~ exists ~ between ~ #1 ~ and ~ #2 }
{ You ~ will ~ need ~ to ~ add ~ it ~ when ~ creating ~ the ~ base ~ unit ~ or ~ via ~ nduKeys. }

\msg_new:nnn
{ non-decimal-units } { calculating-factor }
{ Calculating ~ conversion ~ factor ~ between ~ #1 ~ and ~ #2 ~ via ~ #3 }

\msg_new:nnn
{ non-decimal-units } { found-factor }
{ Found ~ conversion ~ factor ~ between ~ #1 ~ and ~ #2: ~ #3 }

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Unit setup
% 

\NewDocumentCommand { \nduNewBaseUnit } { m m } {
	\tl_if_exist:cT { l__ndu_factors_#1_prop }
	{
		\msg_error:nnn { non-decimal-units } { base-unit-exists } { #1 }
	}
	\prop_clear_new:c { l__ndu_factors_#1_prop }
	\keys_define:nn { non-decimal-units / units / #1 } {
		factor				.code			= {
			\regex_extract_once:nnN { (\d+)\ (\D+) } { ##1 } \l_tmpa_seq
			\prop_put:cee { l__ndu_factors_#1_prop } { \seq_item:Nn \l_tmpa_seq 3 } { \seq_item:Nn \l_tmpa_seq 2 }
		} ,
		symbol				.tl_set:c		= { l__ndu_symbol_#1_tl } ,
		format				.tl_set:c		= { l__ndu_format_#1_tl } ,
	}
	\keys_set:nn { non-decimal-units / units / #1 } { #2 }
}

\NewDocumentCommand { \nduNewUnitGroup } { m O{} m o }
{
	\tl_if_exist:cT { l__ndu_group_#1_seq }
	{
		\msg_error:nnn { non-decimal-units } { unit-group-exists } { #1 }
	}
	\seq_set_from_clist:cn { l__ndu_group_#1_seq } {#3}

	% set base unit to rightmost item
	\seq_get_right:cN { l__ndu_group_#1_seq } \l_tmpa_tl
	\tl_set_eq:cN { l__ndu_base_unit_#1_tl } \l_tmpa_tl

	% store keys for unit group
	\tl_clear_new:c { l__ndu_options_#1_tl }
	\IfValueT {#2} {
		\tl_set:cn { l__ndu_options_#1_tl } {#2}
	}

	% optionally create macro
	\IfValueT {#4} {
		\NewDocumentCommand { #4 } { O{} m } {
			\group_begin:
				\nduValue{#1}[#2,##1]{##2}
			\group_end:
		}
	}
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Conversion factor initialization and access
% 

\cs_new_protected:Nn \ndu_get_factor:Nnn
{
	% parameters
	% #1: output result (int)
	% #2: from unit (tl)
	% #3: to unit (tl)
	\tl_if_exist:cF { l__ndu_factors_#2_prop }
	{
		\msg_error:nnn { non-decimal-units } { missing-base-unit } { #2 }
	}
	\tl_if_exist:cF { l__ndu_factors_#3_prop }
	{
		\msg_error:nnn { non-decimal-units } { missing-base-unit } { #3 }
	}
	\str_if_eq:nnTF {#2} {#3}
	{
		\int_set:Nn #1 { 1 }
	}
	{
		\prop_if_in:cnTF { l__ndu_factors_#2_prop } { #3 }
		{
			\prop_get:cnN  { l__ndu_factors_#2_prop } { #3 } \l_tmpa_tl
%			\msg_term:nnnee { non-decimal-units } { found-factor } { #2 } { #3 } { \tl_use:N \l_tmpa_tl }
			\int_set:Nn #1 { \l_tmpa_tl }
		}{
			\int_zero_new:N \l__ndu_factor_product_int
			\prop_map_inline:cn { l__ndu_factors_#2_prop } {
				\str_if_eq:nnF {#3} {##1}
				{
%					\msg_term:nnnee { non-decimal-units } { calculating-factor } { #2 } { #3 } { ##1 }
					\int_zero_new:N \l__ndu_sub_factor_int
					\ndu_get_factor:Nnn \l__ndu_sub_factor_int { ##1 } { #3 }
					\int_set:Nn \l__ndu_factor_product_int { ##2 * \l__ndu_sub_factor_int }
					\int_set:Nn #1 { \l__ndu_factor_product_int }
					\prop_put:cnV { l__ndu_factors_#2_prop } { #3 } \l__ndu_factor_product_int
				}
			}
		}
	}
}
\cs_generate_variant:Nn \ndu_get_factor:Nnn {
	NnV,
	NVV
}

% ndu_super_factors (call from nduNewUnitGroup)

\NewDocumentCommand \nduFactor { m m } {
	\ndu_get_factor:Nnn \l_tmpa_int { #1 } { #2 }
	\int_use:N \l_tmpa_int
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Converting from/to representation
% 

\cs_new_protected:Nn \ndu_repr_to_seq:NNNNN
{
	% parameters
	% #1: output result (seq)
	% #2: unit group (seq)
	% #3: base unit (tl)
	% #4: value (int)
	% #5: unit depth (tl)
	\seq_clear:N #1

	\int_zero_new:N \l__ndu_remainder_int

	% initial value * base factor
	\ndu_get_factor:NVV \l_tmpa_int { #5 } { #3 }
	\int_set:Nn \l__ndu_remainder_int { #4 * \l_tmpa_int }

	% for unit in units:
	\seq_map_inline:Nn #2
	{
		\int_zero_new:N \l__ndu_factor_int
		\int_zero_new:N \l__ndu_sum_int

		\ndu_get_factor:NnV \l__ndu_factor_int { ##1 } { #3 }

		% while (remainder >= factor_to_base):
		\int_while_do:nn {\l__ndu_remainder_int >= \l__ndu_factor_int} {
			% remainder -= factor_to_base
			\int_sub:Nn \l__ndu_remainder_int { \l__ndu_factor_int }
			% result[i] += 1
			\int_incr:N \l__ndu_sum_int
		}
		\seq_put_right:NV #1 \l__ndu_sum_int
		
		% stop when we hit the desired unit depth
		\str_if_eq:NNT {##1} {#5} {
			\seq_map_break:
		}
	}
}
\cs_generate_variant:Nn \ndu_repr_to_seq:NNNNN {
	NccNc,
	Ncccc,
	NccNN
}

% convenience function
\cs_new_protected:Nn \ndu_repr_to_seq:NnN
{
	% parameters
	% #1: output result (int)
	% #2: unit group name (tl)
	% #3: value (int)
	\ndu_repr_to_seq:NccNc
		#1
		{ l__ndu_group_#2_seq }
		{ l__ndu_base_unit_#2_tl }
		#3
		{ l__ndu_base_unit_#2_tl }

}
\cs_generate_variant:Nn \ndu_repr_to_seq:NnN {
	Nnc
}


\cs_new_protected:Nn \ndu_seq_to_repr:NNNN
{
	% parameters
	% #1: output result (int)
	% #2: unit group (seq)
	% #3: base unit (tl)
	% #4: input (seq)
	\int_zero:N #1

	\seq_map_indexed_inline:Nn #2
	{
		\tl_set:Ne \l__ndu_value_tl { \seq_item:Nn #4 {##1} }
		\tl_if_blank:VF \l__ndu_value_tl {
			\ndu_get_factor:NnV \l_tmpa_int { ##2 } { #3 }
			\int_add:Nn #1 { \l__ndu_value_tl * \l_tmpa_int }
		}
	}
}
\cs_generate_variant:Nn \ndu_seq_to_repr:NNNN {
	NccN
}

% convenience function
\cs_new_protected:Nn \ndu_seq_to_repr:NnN
{
	% parameters
	% #1: output result (seq)
	% #2: unit group name (tl)
	% #3: values (seq)
	\ndu_seq_to_repr:NccN
		#1
		{ l__ndu_group_#2_seq }
		{ l__ndu_base_unit_#2_tl }
		#3
}
\cs_generate_variant:Nn \ndu_seq_to_repr:NnN {
	Nnc
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Formatting values
% 

\prg_new_protected_conditional:Nnn \ndu_if_not_nil_or_zero:Nn {T,TF}
{
	% parameters
	% #1: output result (tl)
	% #2: value (tl)
	% returns T if value should be output, otherwise F

	\tl_if_blank:nTF {#2}
	{
		% only output something if we are replacing nil
		\tl_if_empty:NTF {\l__ndu_replace_nil_with_tl}
		{
			\prg_return_false:
		}
		{
			\tl_set_eq:NN #1 \l__ndu_replace_nil_with_tl
			\prg_return_true:
		}
	}
	{
		\bool_if:nTF {
			\l__ndu_treat_zero_as_nil_bool &&
			\int_compare_p:n { #2 = 0 }
		} {
			% only output something if we are replacing nil
			\tl_if_empty:NTF {\l__ndu_replace_nil_with_tl}
			{
				\prg_return_false:
			}
			{
				\tl_set_eq:NN #1 \l__ndu_replace_nil_with_tl
				\prg_return_true:
			}
		}
		{
			% else output directly
			\tl_set:Ne #1 { #2 }
			\prg_return_true:
		}
	}
}
\prg_generate_conditional_variant:Nnn \ndu_if_not_nil_or_zero:Nn
{
	Ne
}
{
	T,TF
}

\tl_const:Nn \c__ndu_values_only_tl { values~only }
\tl_const:Nn \c__ndu_symbols_only_tl { symbols~only }
\tl_const:Nn \c__ndu_formatted_tl { formatted }

\cs_new_protected:Nn \ndu__numprint_helper:nn
{
	\numprint[#1]{#2}
}

\cs_new_protected:Nn \ndu_format_display_helper:Nnnn
{
	% parameters
	% #1: output result (tl)
	% #2: format (tl)
	% #3: symbol (tl)
	% #4: value (int)
	\tl_case:Nn \l__ndu_display_choice_tl {
		\c__ndu_symbols_only_tl {
			\tl_set:Nn #1 { #3 }
		}
		\c__ndu_values_only_tl {
			\tl_set:Nn #1 {
				\bool_if:NTF \l__ndu_use_numprint_bool
					{ \ndu__numprint_helper:nn {} {#4} }
					{ #4 }
			}
		}
		\c__ndu_formatted_tl {
			\tl_set:Nn #1 { #2 }
			\tl_replace_all:Nnn #1 { \SYMBOL } { #3 }
			\tl_replace_all:Nnn #1 { \VALUE } {
				\bool_if:NTF \l__ndu_use_numprint_bool
					{ \ndu__numprint_helper:nn {} {#4} }
					{ #4 }
			}
		}
	}
}
\cs_generate_variant:Nn \ndu_format_display_helper:Nnnn {
	NVVV
}

\cs_new_protected:Nn \ndu_format_values:NNNN
{
	% parameters
	% #1: output result (tl)
	% #2: unit group (seq)
	% #3: values (seq)
	% #4: separator (tl)
	\seq_clear:N #1
	\seq_clear_new:N \l__ndu_formatted_segments_seq

	\seq_map_indexed_inline:Nn #3
	{
		\tl_clear_new:N \l__ndu_formatted_segment

		% get symbol
		\tl_clear_new:N \l__ndu_symbol_tl
		\tl_set:Ne \l__ndu_symbol_tl { \tl_use:c { l__ndu_symbol_ \seq_item:Nn #2 {##1} _tl } }

		% get format
		\tl_clear_new:N \l__ndu_format_tl
		\tl_if_empty:cTF { l__ndu_format_ \seq_item:Nn #2 {##1} _tl }
		{
			\tl_set_eq:NN \l__ndu_format_tl \l__ndu_default_format_tl
		}
		{
			\tl_set_eq:Nc \l__ndu_format_tl { l__ndu_format_ \seq_item:Nn #2 {##1} _tl }
		}

		% build formatted segment and append to putput
		\ndu_if_not_nil_or_zero:NnT \l_tmpa_tl { ##2 }
		{
			\ndu_format_display_helper:NVVV
				\l__ndu_formatted_segment
				\l__ndu_format_tl
				\l__ndu_symbol_tl
				\l_tmpa_tl
			\seq_put_right:NV \l__ndu_formatted_segments_seq \l__ndu_formatted_segment
		}

		% stop when we hit the desired unit depth
		\tl_set:Ne \l__ndu_unit_tl { \seq_item:Nn #2 {##1} }
		\tl_if_eq:NNT \l__ndu_unit_tl \l__ndu_unit_depth_tl
		{
			\seq_map_break:
		}
	}
	\tl_set:Nn #1 { \seq_use:Nn \l__ndu_formatted_segments_seq {#4} }
}
\cs_generate_variant:Nn \ndu_format_values:NNNN {
	NcNN
}


\prg_new_protected_conditional:Nnn \ndu_if_valid_dim:n {F}
{
	% adapted from https://tex.stackexchange.com/questions/498976/testing-for-a-length
	\regex_match:nnTF
		{ \A [+\-]? ((\d+(\.\d*)?)|(\.\d+)) \s* (pt|pc|in|bp|cm|mm|dd|cc|sp|ex|em) \Z}
		{ #1 } % test string
		{ \prg_return_true: }
		{ \prg_return_false: }
}
\prg_generate_conditional_variant:Nnn \ndu_if_valid_dim:n
{
	V
}
{
	F
}


\cs_new_protected:Nn \ndu_make_box:Nnn
{
	% parameter
	% #1: output result (box)
	% #2: contents (tl)
	% #3: unit index
	\box_clear_new:N #1

	% find configured width for index
	\dim_zero_new:N \l__ndu_cell_width_dim
	\tl_set:Ne \l_tmpa_tl { \seq_item:Nn \l__ndu_cell_widths_seq { #3 } }
	\ndu_if_valid_dim:VF \l_tmpa_tl
	{
		\seq_get_right:NN \l__ndu_cell_widths_seq \l_tmpa_tl
	}
	\dim_set:Nn \l__ndu_cell_width_dim { \l_tmpa_tl }

	\hbox_set_to_wd:Nnn #1 { \l__ndu_cell_width_dim } { #2 }
}
\cs_generate_variant:Nn \ndu_make_box:Nnn {
	cnn
}


\cs_new_protected:Nn \ndu_format_values_aligned:NNN
{
	% parameters
	% #1: output result (tl)
	% #2: unit group (seq)
	% #3: values (seq)
	\tl_set:Nn #1 { \leavevmode }

	\seq_map_indexed_inline:Nn #2
	{
		\tl_clear_new:N \l__ndu_value_tl
		\ndu_if_not_nil_or_zero:NeTF \l__ndu_value_tl { \seq_item:Nn #3 ##1 }
		{
			\ndu_make_box:cnn
				{ l__ndu_cell_ \int_to_alph:n {##1} _box }
				{ \hfill \tl_use:N \l__ndu_value_tl }
				{ ##1 }
		}
		{
			\ndu_make_box:cnn
				{ l__ndu_cell_ \int_to_alph:n {##1} _box }
				{ \hfill }
				{ ##1 }
		}

		\tl_put_right:Ne #1 { \box_use:c { l__ndu_cell_ \int_to_alph:n {##1} _box } }

		% stop when we hit the desired unit depth
		\tl_if_eq:NnT \l__ndu_unit_depth_tl {##2}
		{
			\seq_map_break:
		}
	}
}
\cs_generate_variant:Nn \ndu_format_values_aligned:NNN {
	NcN
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% User commands
% 


\cs_new_protected:Nn \ndu_set_options:nn
{
	\tl_if_exist:cF { l__ndu_group_#1_seq }
	{
		\msg_error:nnn { non-decimal-units } { missing-unit-group } { #1 }
	}

	% set keys for unit group
	\keys_set:nv { non-decimal-units } { l__ndu_options_#1_tl } 

	% set keys provided by command
	\keys_set:nn { non-decimal-units } { #2 }
}

\NewDocumentCommand \nduValue { m O{} m } {
	\group_begin:
		\ndu_set_options:nn {#1} {#2}

		\tl_clear_new:N \l__ndu_result_tl
		\int_zero_new:N \l__ndu_normalized_int
		\seq_set_split:Nnn \l_tmpa_seq { . } {#3}

		% should we normalize the input?
		\bool_if:nT \l__ndu_normalize_bool
		{
			\ndu_seq_to_repr:NnN
				\l__ndu_normalized_int
				{ #1 }
				\l_tmpa_seq
			\ndu_repr_to_seq:NnN
				\l_tmpa_seq
				{ #1 }
				\l__ndu_normalized_int
		}

		% are we outputting aligned cells?
		\bool_if:NTF \l__ndu_aligned_bool
		{
			\ndu_format_values_aligned:NcN
				\l__ndu_result_tl
				{ l__ndu_group_#1_seq }
				\l_tmpa_seq
		}
		{
			\ndu_format_values:NcNN
				\l__ndu_result_tl
				{ l__ndu_group_#1_seq }
				\l_tmpa_seq
				\l__ndu_unit_separator_tl
		}

		% should we do any math?
		\tl_if_empty:NF \l__ndu_current_variable_tl
		{
			\nduMath{#1}[#2]{\l__ndu_current_variable_tl}{\l__ndu_current_operator_tl}{#3}
		}

		% output
		\tl_use:N \l__ndu_result_tl
	\group_end:
}

\NewDocumentCommand \nduNormalize { m O{} m m } {
	\group_begin:
		\ndu_set_options:nn {#1} {#2}

		\tl_clear_new:N \l__ndu_result_tl

		\int_zero_new:N \l__ndu_amount_int
		\int_set:Nn \l__ndu_amount_int {#3}
		\tl_clear_new:N \l__ndu_unit_tl
		\tl_set:Nn \l__ndu_unit_tl {#4}

		\ndu_repr_to_seq:NccNN
			\l_tmpa_seq
			{ l__ndu_group_#1_seq }
			{ l__ndu_base_unit_#1_tl }
			\l__ndu_amount_int
			\l__ndu_unit_tl

		% are we outputting aligned cells?
		\bool_if:NTF \l__ndu_aligned_bool
		{
			\ndu_format_values_aligned:NcN
				\l__ndu_result_tl
				{ l__ndu_group_#1_seq }
				\l_tmpa_seq
		}
		{
			\ndu_format_values:NcNN
				\l__ndu_result_tl
				{ l__ndu_group_#1_seq }
				\l_tmpa_seq
				\l__ndu_unit_separator_tl
		}

		% should we do any math?
		\tl_if_empty:NF \l__ndu_current_variable_tl
		{
			\nduMath{#1}[#2]{\l__ndu_current_variable_tl}{\l__ndu_current_operator_tl}{#3}
		}

		% output
		\tl_use:N \l__ndu_result_tl
	\group_end:
}

\tl_const:Nn \c__ndu_plus_tl { + }
\tl_const:Nn \c__ndu_minus_tl { - }

\NewDocumentCommand \nduMath { m O{} m m m } {
	\group_begin:
		\ndu_set_options:nn {#1} {#2}

		\int_if_exist:cF { g__ndu_variable_#3_int } {
			\int_zero_new:c { g__ndu_variable_#3_int }
		}

		\int_zero_new:N \l__ndu_operand_int

		\bool_if:nTF
		{
			% TODO hacky, but works
			\str_if_eq_p:nV {#4} \c__ndu_plus_tl ||
			\str_if_eq_p:nV {#4} \c__ndu_minus_tl ||
			\tl_if_eq_p:NN {#4} \c__ndu_plus_tl ||
			\tl_if_eq_p:NN {#4} \c__ndu_minus_tl
		}
		{
			% plus or minus means we use the representation of the provided value
			\seq_set_split:Nnn \l_tmpa_seq { . } { #5 }
			\ndu_seq_to_repr:NnN
				\l__ndu_operand_int
				{ #1 }
				\l_tmpa_seq
		}
		{
			% otherwise (multiplication/division) we use the number provided directly
			\int_set:Nn \l__ndu_operand_int { #5 }
		}

		\int_gset:cn { g__ndu_variable_#3_int } {
			\int_use:c { g__ndu_variable_#3_int }
			#4
			\l__ndu_operand_int
		}
	\group_end:
}

\NewDocumentCommand \nduResult { m O{} m } {
	\group_begin:
		\ndu_set_options:nn {#1} {#2}

		\int_if_exist:cF { g__ndu_variable_#3_int } {
			\int_zero_new:c { g__ndu_variable_#3_int }
		}

		\tl_clear_new:N \l__ndu_result_tl

		\ndu_repr_to_seq:Nnc
			\l_tmpa_seq
			{ #1 }
			{ g__ndu_variable_#3_int }

		% are we outputting aligned cells?
		\bool_if:NTF \l__ndu_aligned_bool
		{
			\ndu_format_values_aligned:NcN
				\l__ndu_result_tl
				{ l__ndu_group_#1_seq }
				\l_tmpa_seq
		}
		{
			\ndu_format_values:NcNN
				\l__ndu_result_tl
				{ l__ndu_group_#1_seq }
				\l_tmpa_seq
				\l__ndu_unit_separator_tl
		}

		% output
		\tl_use:N \l__ndu_result_tl
	\group_end:
}

\NewDocumentCommand \nduHeader { m O{} } {
	\group_begin:
		\ndu_set_options:nn {#1} {#2}
		\tl_set:Nn \l__ndu_result_tl { \leavevmode }

		% build boxes
		\seq_set_eq:Nc \l_tmpa_seq { l__ndu_group_#1_seq }
		\seq_map_indexed_inline:Nn \l_tmpa_seq
		{
			\ndu_make_box:cnn
				{ l__ndu_cell_ \int_to_alph:n {##1} _box }
				{ \hfill \tl_use:c { l__ndu_symbol_ ##2 _tl } }
				{ ##1 }
			\tl_put_right:Nn \l__ndu_result_tl { \box_use:c { l__ndu_cell_ \int_to_alph:n {##1} _box } }

			% stop when we hit the desired unit depth
			\tl_if_eq:NnT \l__ndu_unit_depth_tl {##2}
			{
				\seq_map_break:
			}
		}

		% output
		\tl_use:N \l__ndu_result_tl
	\group_end:
}

\NewDocumentCommand \nduSymbol { m } {
	\tl_use:c { l__ndu_symbol_#1_tl }
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Column type for tabularray
% 

\keys_define:nn { non-decimal-units }
{
	tabularray~column~type		.code			= {
			\NewColumnType {#1}[2] { Q[r, cmd=\ndu_tblr_cell:nnn {##1} {##2}] }
		} ,
}

\tl_clear_new:N \l__ndu_tblr_column_type_tl

\tl_const:Nn \c__ndu_HEADER_tl { HEADER }
\tl_const:Nn \c__ndu_RESULT_tl { RESULT }
\tl_const:Nn \c__ndu_SKIP_tl { SKIP }

\cs_new_protected:Nn \ndu_tblr_cell:nnn
{
	\keys_set:nn { non-decimal-units } {
		aligned,
		#2
	}
	\tl_set:Nn \l_tmpa_tl {#3}
	\tl_case:NnF \l_tmpa_tl {
		\c__ndu_HEADER_tl {
			\nduHeader
			{ #1 }
		}
		\c__ndu_RESULT_tl {
			\nduResult
			{ #1 }
			{ \l__ndu_current_variable_tl }
		}
		\c__ndu_SKIP_tl {
			% nothing
		}
	}
	{
		% format value if no special token was hit
		% do math only once per tblr cell
		% Will https://github.com/lvjr/tabularray/issues/179 introduce
		% elenganter solution?
		\cs_if_exist:cTF { \ndu__tblr_cell_get_csname: }
			% or use a specific bool, e.g., \ndu__do_math_bool
			{ \tl_clear:N \l__ndu_current_variable_tl }
			{ \cs_gset:cpn { \ndu__tblr_cell_get_csname: } {} }
		\nduValue
			{ #1 }
			{ #3 }
	}
}

% a csname distinct per tblr table, per cell
\cs_new:Npn \ndu__tblr_cell_get_csname:
	{
		ndu__tblr_ \int_use:N \g__tblr_table_count_int
		_cell_ \int_use:N \c@rownum _ \int_use:N \c@colnum
		:
	}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Setup
% 

\keys_define:nn { non-decimal-units / options }
{
	british	 					.code:n 		= { \input{non-decimal-units.british} } ,
	danish	 					.code:n 		= { \input{non-decimal-units.danish} } ,
	german	 					.code:n 		= { \input{non-decimal-units.german} } ,
}

\ProcessKeyOptions [ non-decimal-units / options ]
