0.0.1
This commit is contained in:
@@ -0,0 +1,680 @@
|
||||
//! Programmer calculator (Windows-like): integer bases + bitwise ops.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Base {
|
||||
Hex,
|
||||
Dec,
|
||||
Oct,
|
||||
Bin,
|
||||
}
|
||||
|
||||
impl Base {
|
||||
pub fn radix(self) -> u32 {
|
||||
match self {
|
||||
Base::Hex => 16,
|
||||
Base::Dec => 10,
|
||||
Base::Oct => 8,
|
||||
Base::Bin => 2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Base::Hex => "HEX",
|
||||
Base::Dec => "DEC",
|
||||
Base::Oct => "OCT",
|
||||
Base::Bin => "BIN",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WordSize {
|
||||
/// 64-bit (signed DEC).
|
||||
Qword,
|
||||
/// 32-bit unsigned (PLC DWORD).
|
||||
Dword,
|
||||
/// 32-bit signed (PLC DINT).
|
||||
Dint,
|
||||
/// 16-bit unsigned (PLC WORD).
|
||||
Word,
|
||||
/// 16-bit signed (PLC INT).
|
||||
Int,
|
||||
/// 8-bit (signed DEC).
|
||||
Byte,
|
||||
}
|
||||
|
||||
impl WordSize {
|
||||
pub fn bits(self) -> u32 {
|
||||
match self {
|
||||
WordSize::Qword => 64,
|
||||
WordSize::Dword | WordSize::Dint => 32,
|
||||
WordSize::Word | WordSize::Int => 16,
|
||||
WordSize::Byte => 8,
|
||||
}
|
||||
}
|
||||
|
||||
/// Signed two's-complement DEC display (INT / DINT / QWORD / BYTE).
|
||||
pub fn is_signed(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
WordSize::Qword | WordSize::Dint | WordSize::Int | WordSize::Byte
|
||||
)
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
WordSize::Qword => "QWORD",
|
||||
WordSize::Dword => "DWORD",
|
||||
WordSize::Dint => "DINT",
|
||||
WordSize::Word => "WORD",
|
||||
WordSize::Int => "INT",
|
||||
WordSize::Byte => "BYTE",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle(self) -> Self {
|
||||
match self {
|
||||
WordSize::Qword => WordSize::Dword,
|
||||
WordSize::Dword => WordSize::Dint,
|
||||
WordSize::Dint => WordSize::Word,
|
||||
WordSize::Word => WordSize::Int,
|
||||
WordSize::Int => WordSize::Byte,
|
||||
WordSize::Byte => WordSize::Qword,
|
||||
}
|
||||
}
|
||||
|
||||
fn mask(self) -> u64 {
|
||||
match self.bits() {
|
||||
64 => u64::MAX,
|
||||
bits => (1u64 << bits) - 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProgOp {
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
Mod,
|
||||
And,
|
||||
Or,
|
||||
Xor,
|
||||
Nand,
|
||||
Nor,
|
||||
Lsh,
|
||||
Rsh,
|
||||
}
|
||||
|
||||
impl ProgOp {
|
||||
pub fn symbol(self) -> &'static str {
|
||||
match self {
|
||||
ProgOp::Add => "+",
|
||||
ProgOp::Sub => "−",
|
||||
ProgOp::Mul => "×",
|
||||
ProgOp::Div => "÷",
|
||||
ProgOp::Mod => "%",
|
||||
ProgOp::And => "AND",
|
||||
ProgOp::Or => "OR",
|
||||
ProgOp::Xor => "XOR",
|
||||
ProgOp::Nand => "NAND",
|
||||
ProgOp::Nor => "NOR",
|
||||
ProgOp::Lsh => "Lsh",
|
||||
ProgOp::Rsh => "Rsh",
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(self, a: u64, b: u64, size: WordSize) -> Result<u64, ProgError> {
|
||||
let mask = size.mask();
|
||||
let a = a & mask;
|
||||
let b = b & mask;
|
||||
let r = match self {
|
||||
ProgOp::Add => a.wrapping_add(b),
|
||||
ProgOp::Sub => a.wrapping_sub(b),
|
||||
ProgOp::Mul => a.wrapping_mul(b),
|
||||
ProgOp::Div => {
|
||||
if b == 0 {
|
||||
return Err(ProgError::DivByZero);
|
||||
}
|
||||
a / b
|
||||
}
|
||||
ProgOp::Mod => {
|
||||
if b == 0 {
|
||||
return Err(ProgError::DivByZero);
|
||||
}
|
||||
a % b
|
||||
}
|
||||
ProgOp::And => a & b,
|
||||
ProgOp::Or => a | b,
|
||||
ProgOp::Xor => a ^ b,
|
||||
ProgOp::Nand => !(a & b),
|
||||
ProgOp::Nor => !(a | b),
|
||||
ProgOp::Lsh => {
|
||||
let sh = (b as u32) % size.bits().max(1);
|
||||
a << sh
|
||||
}
|
||||
ProgOp::Rsh => {
|
||||
let sh = (b as u32) % size.bits().max(1);
|
||||
a >> sh
|
||||
}
|
||||
};
|
||||
Ok(r & mask)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProgError {
|
||||
DivByZero,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl ProgError {
|
||||
pub fn message(self) -> &'static str {
|
||||
match self {
|
||||
ProgError::DivByZero => "Cannot divide by zero",
|
||||
ProgError::Invalid => "Invalid input",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Programmer {
|
||||
entry: String,
|
||||
typing: bool,
|
||||
value: u64,
|
||||
pending: Option<u64>,
|
||||
op: Option<ProgOp>,
|
||||
expression: String,
|
||||
error: Option<ProgError>,
|
||||
base: Base,
|
||||
word_size: WordSize,
|
||||
}
|
||||
|
||||
impl Default for Programmer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Programmer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entry: "0".into(),
|
||||
typing: false,
|
||||
value: 0,
|
||||
pending: None,
|
||||
op: None,
|
||||
expression: String::new(),
|
||||
error: None,
|
||||
base: Base::Dec,
|
||||
word_size: WordSize::Qword,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base(&self) -> Base {
|
||||
self.base
|
||||
}
|
||||
|
||||
pub fn word_size(&self) -> WordSize {
|
||||
self.word_size
|
||||
}
|
||||
|
||||
pub fn expression(&self) -> &str {
|
||||
&self.expression
|
||||
}
|
||||
|
||||
pub fn display(&self) -> String {
|
||||
if let Some(err) = self.error {
|
||||
return err.message().into();
|
||||
}
|
||||
if self.typing {
|
||||
format_grouped_entry(&self.entry, self.base)
|
||||
} else {
|
||||
format_value(self.value, self.base, self.word_size)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hex_text(&self) -> String {
|
||||
format_value(self.current_bits(), Base::Hex, self.word_size)
|
||||
}
|
||||
|
||||
pub fn dec_text(&self) -> String {
|
||||
format_value(self.current_bits(), Base::Dec, self.word_size)
|
||||
}
|
||||
|
||||
pub fn oct_text(&self) -> String {
|
||||
format_value(self.current_bits(), Base::Oct, self.word_size)
|
||||
}
|
||||
|
||||
pub fn bin_text(&self) -> String {
|
||||
format_value(self.current_bits(), Base::Bin, self.word_size)
|
||||
}
|
||||
|
||||
/// Bit rows for the clickable bit board (MSB-first within each row).
|
||||
/// Each entry is `(is_set, bit_index)` with `bit_index` counted from LSB = 0.
|
||||
pub fn bit_rows(&self) -> Vec<Vec<(bool, u32)>> {
|
||||
let n = self.word_size.bits();
|
||||
let v = self.current_bits();
|
||||
let cols: u32 = if n <= 8 { 8 } else { 16 };
|
||||
let mut rows = Vec::new();
|
||||
let mut row = Vec::new();
|
||||
for i in (0..n).rev() {
|
||||
row.push(((v >> i) & 1 == 1, i));
|
||||
if row.len() == cols as usize {
|
||||
rows.push(std::mem::take(&mut row));
|
||||
}
|
||||
}
|
||||
if !row.is_empty() {
|
||||
rows.push(row);
|
||||
}
|
||||
rows
|
||||
}
|
||||
|
||||
pub fn toggle_bit(&mut self, bit: u32) {
|
||||
if bit >= self.word_size.bits() {
|
||||
return;
|
||||
}
|
||||
if self.error.is_some() {
|
||||
self.clear_entry();
|
||||
}
|
||||
let v = self.current_bits();
|
||||
self.value = (v ^ (1u64 << bit)) & self.word_size.mask();
|
||||
self.typing = false;
|
||||
self.error = None;
|
||||
self.entry = format_raw(self.value, self.base, self.word_size);
|
||||
}
|
||||
|
||||
pub fn set_base(&mut self, base: Base) {
|
||||
if self.error.is_some() {
|
||||
self.clear_entry();
|
||||
}
|
||||
// Commit typed digits into value when switching bases.
|
||||
if self.typing {
|
||||
if let Ok(v) = parse_entry(&self.entry, self.base) {
|
||||
self.value = v & self.word_size.mask();
|
||||
}
|
||||
self.typing = false;
|
||||
}
|
||||
self.base = base;
|
||||
self.entry = format_raw(self.value, base, self.word_size);
|
||||
}
|
||||
|
||||
pub fn cycle_word_size(&mut self) {
|
||||
if self.typing {
|
||||
if let Ok(v) = parse_entry(&self.entry, self.base) {
|
||||
self.value = v;
|
||||
}
|
||||
self.typing = false;
|
||||
}
|
||||
self.word_size = self.word_size.cycle();
|
||||
self.value &= self.word_size.mask();
|
||||
if let Some(p) = self.pending.as_mut() {
|
||||
*p &= self.word_size.mask();
|
||||
}
|
||||
self.entry = format_raw(self.value, self.base, self.word_size);
|
||||
}
|
||||
|
||||
pub fn clear_all(&mut self) {
|
||||
let base = self.base;
|
||||
let word_size = self.word_size;
|
||||
*self = Self::new();
|
||||
self.base = base;
|
||||
self.word_size = word_size;
|
||||
}
|
||||
|
||||
pub fn clear_entry(&mut self) {
|
||||
self.error = None;
|
||||
self.entry = "0".into();
|
||||
self.value = 0;
|
||||
self.typing = false;
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self) {
|
||||
if self.error.is_some() {
|
||||
self.clear_entry();
|
||||
return;
|
||||
}
|
||||
if !self.typing {
|
||||
return;
|
||||
}
|
||||
self.entry.pop();
|
||||
if self.entry.is_empty() {
|
||||
self.entry = "0".into();
|
||||
self.typing = false;
|
||||
self.value = 0;
|
||||
} else if let Ok(v) = parse_entry(&self.entry, self.base) {
|
||||
self.value = v & self.word_size.mask();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input_digit(&mut self, ch: char) {
|
||||
let ch = ch.to_ascii_uppercase();
|
||||
if !is_valid_digit(ch, self.base) {
|
||||
return;
|
||||
}
|
||||
if self.error.is_some() {
|
||||
self.clear_all();
|
||||
}
|
||||
|
||||
if !self.typing {
|
||||
self.entry = ch.to_string();
|
||||
self.typing = true;
|
||||
} else if self.entry == "0" {
|
||||
self.entry = ch.to_string();
|
||||
} else {
|
||||
let max_digits = max_digits(self.base, self.word_size);
|
||||
if digit_len(&self.entry) >= max_digits {
|
||||
return;
|
||||
}
|
||||
self.entry.push(ch);
|
||||
}
|
||||
|
||||
match parse_entry(&self.entry, self.base) {
|
||||
Ok(v) => {
|
||||
let masked = v & self.word_size.mask();
|
||||
// Reject overflow beyond word size (except wrapping interpretation of typed hex).
|
||||
if v != masked && self.base != Base::Bin {
|
||||
self.entry.pop();
|
||||
if self.entry.is_empty() {
|
||||
self.entry = "0".into();
|
||||
self.typing = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.value = masked;
|
||||
}
|
||||
Err(_) => {
|
||||
self.entry.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn negate(&mut self) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
let v = self.current_bits();
|
||||
self.value = (!v).wrapping_add(1) & self.word_size.mask();
|
||||
self.typing = false;
|
||||
self.entry = format_raw(self.value, self.base, self.word_size);
|
||||
}
|
||||
|
||||
pub fn not(&mut self) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
let v = self.current_bits();
|
||||
self.value = (!v) & self.word_size.mask();
|
||||
self.expression.clear();
|
||||
self.typing = false;
|
||||
self.entry = format_raw(self.value, self.base, self.word_size);
|
||||
}
|
||||
|
||||
pub fn set_op(&mut self, op: ProgOp) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = self.commit_pending() {
|
||||
self.fail(e);
|
||||
return;
|
||||
}
|
||||
let v = self.current_bits();
|
||||
self.pending = Some(v);
|
||||
self.op = Some(op);
|
||||
self.expression = format!(
|
||||
"{} {}",
|
||||
format_value(v, self.base, self.word_size),
|
||||
op.symbol()
|
||||
);
|
||||
self.typing = false;
|
||||
self.entry = format_raw(v, self.base, self.word_size);
|
||||
}
|
||||
|
||||
pub fn equals(&mut self) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
let Some(op) = self.op else {
|
||||
self.expression.clear();
|
||||
self.typing = false;
|
||||
return;
|
||||
};
|
||||
let b = self.current_bits();
|
||||
let a = self.pending.unwrap_or(b);
|
||||
match op.apply(a, b, self.word_size) {
|
||||
Ok(r) => {
|
||||
self.expression = format!(
|
||||
"{} {} {} =",
|
||||
format_value(a, self.base, self.word_size),
|
||||
op.symbol(),
|
||||
format_value(b, self.base, self.word_size)
|
||||
);
|
||||
self.value = r;
|
||||
self.pending = None;
|
||||
self.op = None;
|
||||
self.typing = false;
|
||||
self.entry = format_raw(r, self.base, self.word_size);
|
||||
}
|
||||
Err(e) => self.fail(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_bits(&self) -> u64 {
|
||||
if self.typing {
|
||||
parse_entry(&self.entry, self.base)
|
||||
.unwrap_or(self.value)
|
||||
& self.word_size.mask()
|
||||
} else {
|
||||
self.value & self.word_size.mask()
|
||||
}
|
||||
}
|
||||
|
||||
fn commit_pending(&mut self) -> Result<(), ProgError> {
|
||||
let (Some(a), Some(op)) = (self.pending, self.op) else {
|
||||
return Ok(());
|
||||
};
|
||||
if !self.typing {
|
||||
return Ok(());
|
||||
}
|
||||
let b = self.current_bits();
|
||||
let r = op.apply(a, b, self.word_size)?;
|
||||
self.value = r;
|
||||
self.pending = Some(r);
|
||||
self.op = None;
|
||||
self.entry = format_raw(r, self.base, self.word_size);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fail(&mut self, e: ProgError) {
|
||||
self.error = Some(e);
|
||||
self.pending = None;
|
||||
self.op = None;
|
||||
self.expression.clear();
|
||||
self.typing = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_digit(ch: char, base: Base) -> bool {
|
||||
match base {
|
||||
Base::Bin => matches!(ch, '0' | '1'),
|
||||
Base::Oct => matches!(ch, '0'..='7'),
|
||||
Base::Dec => ch.is_ascii_digit(),
|
||||
Base::Hex => ch.is_ascii_hexdigit(),
|
||||
}
|
||||
}
|
||||
|
||||
fn digit_len(s: &str) -> usize {
|
||||
s.chars().filter(|c| c.is_ascii_hexdigit()).count()
|
||||
}
|
||||
|
||||
fn max_digits(base: Base, size: WordSize) -> usize {
|
||||
let bits = size.bits() as f64;
|
||||
match base {
|
||||
Base::Bin => size.bits() as usize,
|
||||
Base::Oct => (bits / 3.0).ceil() as usize,
|
||||
Base::Dec => (bits * std::f64::consts::LN_2 / std::f64::consts::LN_10).ceil() as usize,
|
||||
Base::Hex => (bits / 4.0).ceil() as usize,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_entry(entry: &str, base: Base) -> Result<u64, ProgError> {
|
||||
let cleaned: String = entry
|
||||
.chars()
|
||||
.filter(|c| c.is_ascii_hexdigit())
|
||||
.collect();
|
||||
if cleaned.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
u64::from_str_radix(&cleaned, base.radix()).map_err(|_| ProgError::Invalid)
|
||||
}
|
||||
|
||||
fn format_raw(v: u64, base: Base, size: WordSize) -> String {
|
||||
let v = v & size.mask();
|
||||
match base {
|
||||
Base::Dec => {
|
||||
if size.is_signed() {
|
||||
format!("{}", to_signed(v, size))
|
||||
} else {
|
||||
format!("{v}")
|
||||
}
|
||||
}
|
||||
Base::Hex => format!("{:X}", v),
|
||||
Base::Oct => format!("{:o}", v),
|
||||
Base::Bin => format!("{:b}", v),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_value(v: u64, base: Base, size: WordSize) -> String {
|
||||
format_grouped_entry(&format_raw(v, base, size), base)
|
||||
}
|
||||
|
||||
fn format_grouped_entry(raw: &str, base: Base) -> String {
|
||||
// Keep leading minus for signed decimal.
|
||||
let (sign, digits) = if let Some(rest) = raw.strip_prefix('-') {
|
||||
("-", rest)
|
||||
} else {
|
||||
("", raw)
|
||||
};
|
||||
let digits: String = digits
|
||||
.chars()
|
||||
.filter(|c| c.is_ascii_hexdigit())
|
||||
.collect::<String>()
|
||||
.to_ascii_uppercase();
|
||||
if digits.is_empty() {
|
||||
return format!("{sign}0");
|
||||
}
|
||||
let group = match base {
|
||||
Base::Bin => 4,
|
||||
Base::Hex | Base::Oct => 4,
|
||||
Base::Dec => 3,
|
||||
};
|
||||
let mut out = String::new();
|
||||
for (i, ch) in digits.chars().rev().enumerate() {
|
||||
if i > 0 && i % group == 0 {
|
||||
out.push(' ');
|
||||
}
|
||||
out.push(ch);
|
||||
}
|
||||
let grouped: String = out.chars().rev().collect();
|
||||
let mut result = String::new();
|
||||
let _ = write!(result, "{sign}{grouped}");
|
||||
result
|
||||
}
|
||||
|
||||
fn to_signed(v: u64, size: WordSize) -> i64 {
|
||||
let bits = size.bits();
|
||||
if bits == 64 {
|
||||
return v as i64;
|
||||
}
|
||||
let mask = size.mask();
|
||||
let v = v & mask;
|
||||
let sign_bit = 1u64 << (bits - 1);
|
||||
if v & sign_bit != 0 {
|
||||
(v | !mask) as i64
|
||||
} else {
|
||||
v as i64
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hex_and() {
|
||||
let mut p = Programmer::new();
|
||||
p.set_base(Base::Hex);
|
||||
p.input_digit('F');
|
||||
p.input_digit('F');
|
||||
p.set_op(ProgOp::And);
|
||||
p.input_digit('0');
|
||||
p.input_digit('F');
|
||||
p.equals();
|
||||
assert_eq!(p.current_bits(), 0x0F);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simultaneous_bases() {
|
||||
let mut p = Programmer::new();
|
||||
p.input_digit('2');
|
||||
p.input_digit('5');
|
||||
p.input_digit('5');
|
||||
assert_eq!(p.hex_text().replace(' ', ""), "FF");
|
||||
assert_eq!(p.bin_text().replace(' ', ""), "11111111");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_bit_flips() {
|
||||
let mut p = Programmer::new();
|
||||
// QWORD → DWORD → DINT → WORD → INT → BYTE
|
||||
for _ in 0..5 {
|
||||
p.cycle_word_size();
|
||||
}
|
||||
assert_eq!(p.word_size(), WordSize::Byte);
|
||||
p.toggle_bit(0);
|
||||
p.toggle_bit(7);
|
||||
assert_eq!(p.current_bits(), 0b1000_0001);
|
||||
p.toggle_bit(0);
|
||||
assert_eq!(p.current_bits(), 0b1000_0000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn int_vs_word_dec() {
|
||||
let mut p = Programmer::new();
|
||||
p.set_base(Base::Hex);
|
||||
// → DWORD → DINT → WORD
|
||||
p.cycle_word_size();
|
||||
p.cycle_word_size();
|
||||
p.cycle_word_size();
|
||||
assert_eq!(p.word_size(), WordSize::Word);
|
||||
p.input_digit('F');
|
||||
p.input_digit('F');
|
||||
p.input_digit('F');
|
||||
p.input_digit('F');
|
||||
assert_eq!(p.dec_text().replace(' ', ""), "65535");
|
||||
|
||||
p.cycle_word_size(); // INT
|
||||
assert_eq!(p.word_size(), WordSize::Int);
|
||||
assert_eq!(p.dec_text().replace(' ', ""), "-1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dint_vs_dword_dec() {
|
||||
let mut p = Programmer::new();
|
||||
p.set_base(Base::Hex);
|
||||
p.cycle_word_size(); // DWORD
|
||||
assert_eq!(p.word_size(), WordSize::Dword);
|
||||
for _ in 0..8 {
|
||||
p.input_digit('F');
|
||||
}
|
||||
assert_eq!(p.dec_text().replace(' ', ""), "4294967295");
|
||||
|
||||
p.cycle_word_size(); // DINT
|
||||
assert_eq!(p.word_size(), WordSize::Dint);
|
||||
assert_eq!(p.dec_text().replace(' ', ""), "-1");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user