【问题标题】:No precision loss integer arithmetic无精度损失整数运算
【发布时间】:2015-06-28 07:45:41
【问题描述】:

如果运算导致整数(否则抛出),我将如何创建一个与整数一起使用的类型,至少支持加减除法和乘法以及保证和整数。

例如,我希望能够执行以下操作:

Precise A = 10;
A.Divide(3);
A.GetNumber(); // This would throw an exception as 10/3 isn't an int.
A.Multiply(6);
int result = A.GetNumber; // I want result to be = to 20, not to a floating point type that would round to 2 or be very close like 1.9999999999999999999999998992

我意识到这是一个奇怪的用例,但我确实有这个需求(执行一系列操作,浮点数可能会被错误表示,但保证最终会成为有效的 int)。

【问题讨论】:

  • 为什么结果会是 2?我希望是 18 或 20。但你可以只进行正常除法,然后转换为 int
  • @Sayse 通过我,但 DivideDivideInto 所以它计算 3/10...(命名很难)。
  • Stack Overflow 不做推荐。如果您在找到“大理性”图书馆时遇到困难,我们会为您提供帮助,但我们不会为您找到。
  • 其实这只是一个错字我在写完之后没有改变我的评论就改变了A(A原本是1)。立即更改文本。
  • 我不想存储任意精度的数字 你写问题的方式:在第一次除法之后,你将如何存储中间结果?

标签: c# math floating-point


【解决方案1】:

如果有“.”,我会使用小数作为运算结果,并在 .ToString 上的 GetNumber 检查中使用“。”如果是,我抛出一个异常,如果不是,我将其转换为 int。

【讨论】:

    【解决方案2】:

    因为我们不知道10 / 3 最终会得到一个精确的整数答案,直到* 6 之后,我们必须通过承诺将其推迟到那时:

    public sealed class Precise
    {
      private interface IOperation
      {
        int Calculate(int value);
        IOperation Combine(IOperation next);
      }
      private sealed class NoOp : IOperation
      {
        public static NoOp Instance = new NoOp();
        public int Calculate(int value)
        {
          return value;
        }
        public IOperation Combine(IOperation next)
        {
          return next;
        }
      }
      private sealed class Combo : IOperation
      {
        private readonly IOperation _first;
        private readonly IOperation _second;
        public Combo(IOperation first, IOperation second)
        {
          _first = first;
          _second = second;
        }
        public int Calculate(int value)
        {
          return _second.Calculate(_first.Calculate(value));
        }
        public IOperation Combine(IOperation next)
        {
          return new Combo(_first, _second.Combine(next));
        }
      }
      private sealed class Mult : IOperation
      {
        private readonly int _multiplicand;
        public Mult(int multiplicand)
        {
          _multiplicand = multiplicand;
        }
        public int Calculate(int value)
        {
          return value * _multiplicand;
        }
        public int Multiplicand
        {
          get { return _multiplicand; }
        }
        public IOperation Combine(IOperation next)
        {
          var nextMult = next as Mult;
          if(nextMult != null)
            return new Mult(_multiplicand * nextMult._multiplicand);
          var nextDiv = next as Div;
          if(nextDiv != null)
          {
            int divisor = nextDiv.Divisor;
            if(divisor == _multiplicand)
              return NoOp.Instance;//multiplcation by 1
            if(divisor > _multiplicand)
            {
              if(divisor % _multiplicand == 0)
                return new Div(divisor / _multiplicand);
            }
            if(_multiplicand % divisor == 0)
              return new Mult(_multiplicand / divisor);
          }
          return new Combo(this, next);
        }
      }
      private sealed class Div : IOperation
      {
        private readonly int _divisor;
        public Div(int divisor)
        {
          _divisor = divisor;
        }
        public int Divisor
        {
          get { return _divisor; }
        }
        public int Calculate(int value)
        {
          int ret = value / _divisor;
          if(value != ret * _divisor)
            throw new InvalidOperationException("Imprecise division");
          return ret;
        }
        public IOperation Combine(IOperation next)
        {
          var nextDiv = next as Div;
          if(nextDiv != null)
            return new Div(_divisor * nextDiv._divisor);
          var nextMult = next as Mult;
          if(nextMult != null)
          {
            var multiplicand = nextMult.Multiplicand;
            if(multiplicand == _divisor)
              return NoOp.Instance;
            if(multiplicand > _divisor)
            {
              if(multiplicand % _divisor == 0)
                return new Mult(multiplicand / _divisor);
            }
            else if(_divisor % multiplicand == 0)
              return new Div(multiplicand / _divisor);
          }
          return new Combo(this, next);
        }
      }
      private sealed class Plus : IOperation
      {
        private readonly int _addend;
        public Plus(int addend)
        {
          _addend = addend;
        }
        public int Calculate(int value)
        {
          return value + _addend;
        }
        public IOperation Combine(IOperation next)
        {
          var nextPlus = next as Plus;
          if(nextPlus != null)
          {
            int newAdd = _addend + nextPlus._addend;
            return newAdd == 0 ? (IOperation)NoOp.Instance : new Plus(newAdd);
          }
          return new Combo(this, next);
        }
      }
      private readonly int _value;
      private readonly IOperation _operation;
      public static readonly Precise Zero = new Precise(0);
      private Precise(int value, IOperation operation)
      {
        _value = value;
        _operation = operation;
      }
      public Precise(int value)
        : this(value, NoOp.Instance)
      {
      }
      public int GetNumber()
      {
        return _operation.Calculate(_value);
      }
      public static explicit operator int(Precise value)
      {
        return value.GetNumber();
      }
      public static implicit operator Precise(int value)
      {
        return new Precise(value);
      }
      public override string ToString()
      {
        return GetNumber().ToString();
      }
      public Precise Multiply(int multiplicand)
      {
        if(multiplicand == 0)
          return Zero;
        return new Precise(_value, _operation.Combine(new Mult(multiplicand)));
      }
      public static Precise operator * (Precise precise, int value)
      {
        return precise.Multiply(value);
      }
      public Precise Divide(int divisor)
      {
        return new Precise(_value, _operation.Combine(new Div(divisor)));
      }
      public static Precise operator / (Precise precise, int value)
      {
        return precise.Divide(value);
      }
      public Precise Add(int addend)
      {
        return new Precise(_value, _operation.Combine(new Plus(addend)));
      }
      public Precise Subtract(int minuend)
      {
        return Add(-minuend);
      }
      public static Precise operator + (Precise precise, int value)
      {
        return precise.Add(value);
      }
      public static Precise operator - (Precise precise, int value)
      {
        return precise.Subtract(value);
      }
    }
    

    这里每个Precise 都有一个整数值和一个将对其执行的操作。进一步的操作会产生一个新的Precise (将这种事情作为一个可变的事情来做是疯狂的)和一个新的操作,但如果可能的话,这些操作会组合成一个更简单的操作。因此,“除以三再乘以六”变成“乘以二”。

    我们可以这样测试:

    public static void Main(string[] args)
    {
      Precise A = 10;
      A /= 3;
      try
      {
        var test = (int)A;
      }
      catch(InvalidOperationException)
      {
        Console.Error.WriteLine("Invalid operation attempted");
      }
      A *= 6;
      int result = (int)A;
      Console.WriteLine(result);
      // Let's do 10 / 5 * 2 = 4 because it works but can't be pre-combined:
      Console.WriteLine(new Precise(10) / 5 * 2);
      // Let's do 10 / 5 * 2 - 6 + 4 == 2 to mix in addition and subtraction:
      Console.WriteLine(new Precise(10) / 5 * 2 - 6 + 4);
      Console.Read();
    }
    

    一个好的解决方案还可以很好地处理 LHS 是整数和 RHS 是 Precise 以及两者都是 Precise; 的操作。留给读者作为练习;)

    遗憾的是,处理(10 / 3 + 1) * 3 的过程变得更加复杂,必须在Combine 实现中进行改进。

    编辑:进一步思考上述问题足以捕捉到至少大多数边缘情况,我认为它应该从只处理两个Precise对象之间的操作开始,因为int -> Precise 是微不足道的,可以很容易地放在首位,但是 Precise -> int 需要调用计算,可能为时过早。我还将操作作为关键的事情(让操作存储一个或两个对象,这些对象又包含一个操作或一个值)。然后,如果您从总和 (10 / 3) + 5 的表示开始并将其乘以 6,则更容易将其转换为 (10 * (6 / 3)) + (5 * 6),在最终计算时可以给出精确的结果 50 而不是失败,因为它达到了不精确的 10 / 3 .

    【讨论】:

    • 正是我想要的,谢谢(比我希望的更详细,感谢编写得非常好的示例代码!)。计时器结束后,我将启动赏金并将其奖励给您
    • "比我希望的要详细得多" 但是要非常小心,因为它仍然比完整的工作简单得多;在将上述内容提升到生产就绪级别时,有很多事情需要注意。进行大量的单元测试!
    • 是的,我只是在寻找一个起点,所以这仍然是一个不错的起点。
    【解决方案3】:

    如果您不允许任意精确的有理数,那么您似乎是在没有更多约束的情况下提出不可能的问题。

    取 1 并除以 65537 两次,然后乘以 65537 两次得到 1:这不适合 32 位整数。

    【讨论】:

    • 是的,我明白这一点,但我并不是要求它适合特定的尺寸。我不介意在类型实现中是否使用了有理数,但我确实需要输出一个异常(如果它不能在没有任何数据丢失的情况下转换为 int)或单个 int。
    • 虽然你说“我不想存储任意精度的数字”。
    【解决方案4】:

    然后使用 Math.Round() 对最终答案进行四舍五入。

    【讨论】:

    • 我根本不想四舍五入。舍入不等于确保没有损失,它希望最好,我希望它永远不会进入浮点,即使作为中间步骤。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-13
    • 1970-01-01
    • 2018-11-26
    • 2020-07-28
    相关资源
    最近更新 更多