#Windows clipboard + string pain

1 messages · Page 1 of 1 (latest)

glacial whale
#

I wrote this mostly as a thing to help, and hopefully eventually replace, one of my autohotkey scripts since it doesn't have access to the windows clipboard history, and it is just a very awkward language to write anything advanced in overall.
This is my first time writing a rust program that deals with:

  • async
  • winrt
  • formatting numbers
  • impls other than Default
    So a review would be much appreciated. All the misc unwarps in set_clipboard_value are so I can see if it fails with less effort.
    The specific things I'm mostly wondering about are:
  • Is there a better way to handle the IAsyncOperation from GetHistoryItemsAsync, especially when combined with a time out? Or also adding a retry system? This also applies to GetTextAsync.
  • In impl std::iter::Sum for Decimal, how can anything go from a [&u8] to a [&Decimal]? The library I used before did it, but I always ran into the returned Decimal not having a long enough life time.
  • Is there a more sane way to do the comma separated number formatting?
    Any other miscellaneous advice is very welcome.
    Thanks, here's the link https://paste.pythondiscord.com/AQFA
maiden phoenix
# glacial whale I wrote this mostly as a thing to help, and hopefully eventually replace, one of...

In impl std::iter::Sum for Decimal, how can anything go from a [&u8] to a [&Decimal]? The library I used before did it, but I always ran into the returned Decimal not having a long enough life time.
Is that a thing you need? You shouldn't need that to implement Sum.

Also, there's a more efficient way to implement that.

struct Decimal {
  // these fields should probably not be pub: You rely on them having reasonable values, which free mutable access by any random code could ruin
  whole_part: i64,
  decimal_part: u8, //this is always positive (the sign is in whole_part) and most 99, no need for a bigger type than u8
}
impl Add for Decimal {
  type Output = Decimal;
  fn add(self, other: Self) -> Self::Output {
    let whole = self.whole_part + other.whole_part;
    let decimal = self.decimal_part + other.decimal_part;
    Self {
      whole_part: whole + decimal / 100,
      decimal_part: decimal % 100,
    }
  }
}
impl Sum for Decimal {
  fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
    let zero = Decimal { 
      whole_part: 0,
      decimal_part: 0,
    };
    iter.fold(zero, |acc, elem| acc + elem)
  }
}
#

Is there a more sane way to do the comma separated number formatting?
You mean to_string? Are you aiming to print thousand separators? I'll assume yes.
This turns out to be a bit hard to do Properly. You can take advantage of how int.ilog10() + 1 will return the digit count of any strictly positive integer, then, do a loop in which you repeatedly "extract" the top 3 digits, or you can make an array whose length is "-9223372036854775808".len() (it's big enough to store every possible i64 as a string), write your whole part to that, and then read it in rchunks three bytes wide:

//Don't implement a `to_string` method. There is a trait for that, which you get an impl of for free by implementing `Display`
impl Display for Decimal {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    let mut buffer = [0u8; "-9223372036854775808".len()];
    write!(&mut buffer, "{}", self.whole_part())?;
    if self.whole_part < 0 { buffer = &buffer[1..]; }
    //this is not equivalent to `chunks(3)`, unless the buffer length is an exact multiple of 3 
    let mut chunks = buffer.rchunks(3).rev();
    if let Some(first) = chunks.next() {
      f.write_str(str::from_utf8(first).unwrap())?;
      for chunk in chunks {
        f.write_str(",")?;
        f.write_str(str::from_utf8(chunk).unwrap())?;
      }
    }
  }
}
#

(untested code, it might be slightly wrong)

glacial whale
# maiden phoenix > In `impl std::iter::Sum for Decimal`, how can anything go from a `[&u8]` to a ...

Thanks for the feedback, I didn't think of impling Add. The reason I have the sign stored on both is since with this, adding a negative number doesn't work1.1 + (-1.1) -> 0 (1, 1) + (-1, 1) -> (1 + (-1), 1 + 1) -> (0, 2) -> 0.2
And I think storing it in the decimal_part is the better way at least for convenience. In either case that would have to be accounted for either as an invariant to be maintained at creation, or in all the math sites, and less places to worry about it is probably better. Though I should have it be an i8, the only reason I had it as an i64 was so it could store the values during the sum, but that's not needed if it uses Add + fold.

I got it wrong, what I actually meant was having a Sum impl do [&Decimal] -> &Decimal. I don't understand how to do this, since [&Decimal] -> Decimal doesn't match the trait if it's impl Sum for &Decimal, but any &Decimal I returned wouldn't have a long enough lifetime.

maiden phoenix
#

For &mut Decimal you could do an extremely cursed hack, assuming there's at least one input, where you mutate it to contain the sum of all inputs and return it, but I'd generally just expect sum to only return owned values

glacial whale
#

Would it work the same to use i64::MIN.to_string().len()? I assume the compiler would optimize that away to 20, but it feels more clear to me.

maiden phoenix
#

(Allocations are extremely hard to optimise out, and so is formatting)

#

?godbolt

#[inline(never)]
pub fn test() -> usize {
  i64::MIN.to_string().len()
}
keen badgerBOT
#
core::fmt::Write::write_fmt:
        mov     rdx, rsi
        lea     rsi, [rip + .L__unnamed_1]
        jmp     qword ptr [rip + _ZN4core3fmt5write17h60a07532f34a2ce8E@GOTPCREL]

core::ptr::drop_in_place<core::fmt::Error>:
        ret

core::ptr::drop_in_place<alloc::string::String>:
        mov     rsi, qword ptr [rdi]
        test    rsi, rsi
        je      .LBB2_1
        mov     rdi, qword ptr [rdi + 8]
        mov     edx, 1
        jmp     qword ptr [rip + __rust_dealloc@GOTPCREL]
.LBB2_1:
        ret

<core::fmt::Error as core::fmt::Debug>::fmt:
        mov     rdi, rsi
        lea     rsi, [rip + .L__unnamed_2]
        mov     edx, 5
        jmp     qword ptr [rip + _ZN4core3fmt9Formatter9write_str17hc8896b702b301ce5E@GOTPCREL]

<alloc::string::String as core::fmt::Write>::write_char:
        push    rbp
        push    r15
        push    r14
        push    rbx
        push    rax
        mov     rbx, rdi
        cmp     esi, 128
        jae     .LBB4_1
        mov     rax, qword ptr [rbx + 16]
        cmp     rax, qword ptr [rbx]
        jne     .LBB4_10
        mov     rdi, rbx
        mov     ebp, esi
        mov     rsi, rax
        call    alloc::raw_vec::RawVec<T,A>::reserve_for_push
        mov     esi, ebp
        mov     rax, qword ptr [rbx + 16]
.LBB4_10:
        mov     rcx, qword ptr [rbx + 8]
```Note: only public functions (`pub fn`) are shown
Output too large. Godbolt link: <https://godbolt.org/z/Yjqdb7Yj4>
maiden phoenix
#

This most definitely didn't optimise out

#

If you want to go for simplicity, just use a String buffer directly. It'll allocate, but so does this

glacial whale
maiden phoenix
#

Annoyingly, this is much easier if you print the number backwards

glacial whale
#

That is what I ended up doing, with the two reverses ```rs
let binding = whole
.bytes()
.rev() // backwards now
.collect::<Vec<>>()
.chunks(3)
.flat_map(|x| [x, b","]) // since this is blind it needs to be trimmed. I did see something that used the length of the original string + take, but couldn't get it working so I opted for the less brain power trim.
.flatten()
.rev() // forwards
.cloned()
.collect::<Vec<
>>();

maiden phoenix
# glacial whale That is what I ended up doing, with the two reverses ```rs let binding = whole ...

You can skip on one collect by using rchunks(3) rather than chunks(3).

As for comma-separation, printing a comma-separated list is equivalent to printing the first element, then a comma-preceded list:

let mut iter = ...
if let Some(first) = iter.next() {
  print(first);
  for elem in iter {
    print(separator);
    print(rest);
  }
}
```nightly also has an `intersperse` method you could use to add commas, as does itertools IIRC