2

I am attempting to come up with a generic function that will rotate the digits of a given number to the left or the right by n. At this point I've only been able to work out an implementation for left shift:

public static T RotateDigitsLeft<T>(this T value, int count) where T : IBinaryInteger<T> {
    var absoluteValue = T.Abs(value: value);
    var countAsT = T.CreateTruncating(value: count);
    var digitCount = absoluteValue.LogarithmBase10();
    var factor = BinaryIntegerConstants<T>.Ten.Exponentiate(exponent: (digitCount - countAsT));
    var endDigits = (absoluteValue / factor);
    var startDigits = (absoluteValue - (endDigits * factor));

    return T.CopySign(sign: value, value: ((startDigits * BinaryIntegerConstants<T>.Ten.Exponentiate(exponent: countAsT)) + endDigits));
}
  • Is it possible to alter this in a way that works for negative values of count?
  • A subset of rotations result in invalid values, depending on the size of T; is there a decent way to check for these generically so that one can throw?
  • Can one alter the function to support right rotation as well?

Note: Answers that continue to use arithmetic will be preferred because I find them more interesting.

Examples:

  • 54321 -> 43215 (rotate left by 1, rotate right by 4)
  • 54321 -> 32154 (rotate left by 2, rotate right by 3)
  • 54321 -> 21543 (rotate left by 3, rotate right by 2)
  • 54321 -> 15432 (rotate right by 1, rotate left by 4)
  • 54321 -> 21543 (rotate right by 2, rotate left by 3)
  • 54321 -> 32154 (rotate right by 3, rotate left by 2)
  • 1123456789 -> 1234567891 (rotate left by 1)
  • 1123456789 -> EXCEPTION (rotate right by 1)
Kittoes0124
  • 4,930
  • 3
  • 26
  • 47
  • 1
    Have you seen [this](https://stackoverflow.com/questions/812022/c-sharp-bitwise-rotate-left-and-rotate-right) related post? – Axel Kemper Aug 06 '23 at 14:40
  • 1
    @AxelKemper Sorry, that's about rotating *bits*; our goal is to rotate *digits*. – Kittoes0124 Aug 06 '23 at 14:45
  • @Kittoes0124 bits are effectively just base 2 digits. The logic is the same, except bits let you leverage hardware bitops. – Luatic Aug 06 '23 at 14:57
  • @Luatic Great, now draw the rest of the owl. – Kittoes0124 Aug 06 '23 at 15:03
  • 1
    @Kittoes0124 on it right now, but first had to install mono since I normally don't do C# :) – Luatic Aug 06 '23 at 15:04
  • That's the spirit! May I suggest [DotNetFiddle](https://dotnetfiddle.net)? Saves one from defiling their own machine. – Kittoes0124 Aug 06 '23 at 15:07
  • There's LinqPad as well, no need to defile anything. – Charlieface Aug 06 '23 at 15:15
  • @Luatic There seems to be another difference here: OP wants to rotate based on how many digits are actually in the value, not just on how many it could fit. – Charlieface Aug 06 '23 at 15:16
  • @Kittoes0124 added my answer; it's pretty simple in principle, but I'm having trouble fighting C#; where does `LogarithmBase10` come from? Could you provide more context (ideally a working fiddle)? :) – Luatic Aug 06 '23 at 15:26
  • @Luatic That's my fault as it's not built-in; see [here](https://github.com/ByteTerrace/Ouroboros.Maths/blob/main/Project/BinaryIntegerFunctions.cs) for missing classes/functions. – Kittoes0124 Aug 06 '23 at 15:30

3 Answers3

2

Doing a right rotation of n digits is equivalent to doing a left rotation of m - n digits for a number of m digits:

As an example, to rotate 123456 right by two, we rotate left by 6 - 2 = 4:

123456 → 612345 → 561234 → 456123 → 345612

This is equivalent to rotating right twice:

123456 → 234561 → 345612

Thus we only need to make a simple change to support negative rotation values:

if (count < 0) countAsT += digitCount;

right after the digitCount has been computed, and before doing the rest of the rotation. I'm not sure whether this is correct type-wise though; I can't find documentation for the return type of LogarithmBase10. It would be sensible for it to be an int though; if it isn't, you'd have to cast it.

I've created a minimal fiddle based on Dmitry's fiddle for you to test this.

Luatic
  • 8,513
  • 2
  • 13
  • 34
  • If it's easier, feel free to write `T` as `int` or `uint`; one can perform the mechanical translation to generic interfaces from there. – Kittoes0124 Aug 06 '23 at 15:33
  • @Kittoes0124 it's working now. I simply had to use `countAsT` since `digitCount` is a `T`. – Luatic Aug 06 '23 at 15:43
1

I suggest converting integer to string rotate it and then Parse back to integer:

public static T RotateDigitsLeft<T>(this T value, int count) 
  where T : IBinaryInteger<T> {
  
  string text = value.ToString().TrimStart('-');

  count = (count % text.Length + text.Length) % text.Length;

  char[] digits = new char[text.Length];

  for (int i = 0; i < digits.Length; ++i)
    digits[i] = text[(i + count) % digits.Length];

  string result = new string(digits);

  if (value < T.Zero)
    result = "-" + result;

  return T.Parse(result, CultureInfo.InvariantCulture);
}

The only difference between RotateDigitsRight and RotateDigitsLeft is in the count computation, i.e.

public static T RotateDigitsRight<T>(T value, int count) 
  where T : IBinaryInteger<T> {

  string text = value.ToString().TrimStart('-');

  // The only difference is here  
  count = text.Length - (count % text.Length + text.Length) % text.Length;

  char[] digits = new char[text.Length];

  for (int i = 0; i < digits.Length; ++i)
    digits[i] = text[(i + count) % digits.Length];

  string result = new string(digits);

  if (value < T.Zero)
    result = "-" + result;

  return T.Parse(result, CultureInfo.InvariantCulture);
}

Fiddle

Please, note that rotation as it designed can throw exception, e.g. if we rotate byte e.g. 255 by 1 will get 552 or 525 which is beyond 0..255 range.

Another problem (which I've avoided) is with negative numbers: note, that Abs(value) can be beyond range e.g. Abs(int.MinValue) can't be computed as int:

  // -1474836482
  // even if Math.Abs(int.MinValue) not a valid int
  int result = RotateDigitsLeft(int.MinValue, 1);
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
1

I'm still learning C# so I'm not sure about your IBinaryInteger<T>, but I'd go the way of the string:

// careful with zeroes, they may end up at the start.
static int RotateLeft(int value, int count)
{
    string sign = value < 0 ? "-" : "";
    string digits = value.ToString().TrimStart('-');
    // rotation may be > 1 full rotation,
    // let's bring it in the range < 1 full rotation
    count %= digits.Length;

    // count was a multiple of a full rotation, 
    // nothing to rotate here.
    if (count == 0)
        return value;
    
    // negative count == RotateRight
    // let's turn this into the equivalent positive value
    if (count < 0)
        count += digits.Length;
    
    return int.Parse(
        sign + 
        digits.Substring(count, digits.Length - count) + 
        digits.Substring(0, count)
    );
}

static int RotateRight(int value, int count) {
    return RotateLeft(value, -count);
}
Thomas
  • 11,958
  • 1
  • 14
  • 23
  • Be careful with `Math.Abs(value)`: if `value == int.MinValue` you'll get an integer overflow – Dmitry Bychenko Aug 06 '23 at 14:37
  • See [here](https://learn.microsoft.com/en-us/dotnet/standard/generics/math); it's really neat stuff! – Kittoes0124 Aug 06 '23 at 14:39
  • 1
    If you want to support arbitrary `count` (e.g. neagtive) you can use modulo arithmetics, `count = (count % digits.Length + digits.Length) % digits.Length;`, adding `digits.Length` is not enough, e.g. for `count = -101` and `digits.Length = 5` – Dmitry Bychenko Aug 06 '23 at 15:06
  • @DmitryBychenko I cover that part in Line 3 `count %= digits.Length;` after that, `count` is in the range of `-Length < count < Length` so that adding `Length` if count is negative is sufficient. I don't know if the two modulos or the branching is worse/better, but both approaches lead eventually to the same result. – Thomas Aug 06 '23 at 19:16