I'm doing a Ruby kata that asks me to find the sum of the digits of all the numbers from 1 to N (both ends included).
So if I had these inputs, I would get these outputs:
For N = 10 the sum is 1+2+3+4+5+6+7+8+9+(1+0) = 46
For N = 11 the sum is 1+2+3+4+5+6+7+8+9+(1+0)+(1+1) = 48
For N = 12 the sum is 1+2+3+4+5+6+7+8+9+(1+0)+(1+1) +(1+2)= 51
Now I know in my head what needs to be done. Below is the code that I have to solve this problem:
def solution(n)
if n <= 9
return n if n == 1
solution(n-1) + n
elsif n >= 10
45 + (10..n) #How can I grab the ones,tenths, and hundreds?
end
end
Basically everything is fine until I hit over 10.
I'm trying to find some sort of method that could do this. I searched Fixnum and Integer but I haven't found anything that could help me. I want is to find something like "string"[0]
but of course without having to turn the integer back in forth between a string and integer. I know that there is a mathematical relationship there but I'm having a hard time trying to decipher that.
Any help would be appreciated.
You can use modulo and integer division to calculate it recursively:
def sum_digits(n)
return n if n < 10
(n % 10) + sum_digits(n / 10)
end
sum_digits(123)
# => 6
A beginner would probably do this:
123.to_s.chars.map(&:to_i)
# => [1, 2, 3]
but a more thoughtful person would do this:
n, a = 123, []
until n.zero?
n, r = n.divmod(10)
a.unshift(r)
end
a
# => [1, 2, 3]
Rather than computing the sum of the digits for each number in the range, and then summing those subtotals, I have computed the total using combinatorial methods. As such, it is much more efficient than straight enumeration.
Code
SUM_ONES = 10.times.with_object([]) { |i,a| a << i*(i+1)/2 }
S = SUM_ONES[9]
def sum_digits_nbrs_up_to(n)
pwr = n.to_s.size - 1
tot = n.to_s.chars.map(&:to_i).reduce(:+)
sum_leading_digits = 0
pwr.downto(0).each do |p|
pwr_term = 10**p
leading_digit = n/pwr_term
range_size = leading_digit * pwr_term
tot += sum_leading_digits * range_size +
sum_digits_to_pwr(leading_digit, p)
sum_leading_digits += leading_digit
n -= range_size
end
tot
end
def sum_digits_to_pwr(d, p)
case
when d.zero? && p.zero?
0
when d.zero?
10**(p-1) * S * d * p
when p.zero?
10**p * SUM_ONES[d-1]
else
10**p * SUM_ONES[d-1] + 10**(p-1) * S * d * p
end
end
Examples
sum_digits_nbrs_up_to(456) #=> 4809
sum_digits_nbrs_up_to(2345) #=> 32109
sum_digits_nbrs_up_to(43021) #=> 835759
sum_digits_nbrs_up_to(65827359463206357924639357824065821)
#=> 10243650329265398180347270847360769369
These calculations were all essentially instantaneous. I verified the totals for the first three examples by straight enumeration, using @sawa's method for calculating the sum of digits for each number in the range.
Explanation
The algorithm can best be explained with an example. Suppose n
equals 2345
.
We begin by defining the following functions:
t(n)
: sum of all digits of all numbers between 1
and n
, inclusive (the answer)sum(d)
: sum of all digits between 1
and d
, inclusive, (for d=1..9, sum(d) = 0, 1, 3, 6, 10, 15, 21, 28, 36, 45
).g(i)
: sum of digits of the number i
.f(i,j)
: sum of all digits of all integers between i
and j-1
, inclusive.g(m)
: sum of digits of the number m
.h(d,p)
: sum of all digits of all numbers between 0
and d*(10^p)-1
(derived below).Then (I explain the following below):
t(2345) = f(0-1999)+f(2000-2299)+f(2300-2339)+f(2340-2344)+g(2345)
f( 0-1999) = h(2,3) = h(2,3)
f(2000-2299) = 2 * (2299-2000+1) + h(3,2) = 600 + h(3,2)
f(2300-2339) = (2+3) * (2339-2300+1) + h(4,1) = 200 + h(4,1)
f(2340-2344) = (2+3+4) * (2344-2340+1) + h(5,0) = 45 + h(5,0)
g(2345) = 2+3+4+5 = 14
so
t(2345) = 859 + h(2,3) + h(3,2) + h(4,1) + h(5,0)
First consider f(2000-2299)
. The first digit, 2
, appears in every number in the range (2000..2299)
; i.e., 300
times. The remaining three digits contribute (by definition) h(3,2)
to the total:
f(2000-2299) = 2 * 300 + h(3,2)
For f(2300-2339)
the first two digits, 2
and 3
, are present in all 40
numbers in the range (2300..2339)
and the remaining two digits contribute h(4,1)
to the total:
f(2300-2339) = 5 * 40 + h(4,1)
For f(2340-2344)
, the first three digits, '2,
3and
4, are present in all four number in the range ``(2340-2344)
and the last digit contributes h(5,0)
to the total.
It remains to derive an expression for computing h(d,p)
. Again, this is best explained with an example.
Consider h(3,2)
, which is the sum of the all digits of all numbers between 0
and 299
.
First consider the sum of digits for the first digit. 0
, 1
and 2
are each the first digit for 100 numbers in the range 0-299
. Hence, the first digit, summed, contributes
0*100 + 1*100 + 2*100 = sum(2) * 10^2
to the total. We now add the sum of digits for the remaining 2
digits. The 300
numbers each have 2
digits in the last two positions. Each of the digits 0-9
appears in 1/10th
of 2 * 300 = 600
digits; i.e, 60
times. Hence, the sum of all digits in last 2
digit positions, over all 300
numbers, equals:
sum(9) * 2 * 300 / 10 = 45 * 2 * 30 = 2700.
More generally,
h(d,p) = sum(d-1) * 10**p + sum(9) * d * p * 10**(p-1) if d > 0 and p > 0
= sum(d-1) * 10**p if d > 0 and p == 0
= sum(9) * d * p * 10**(p-1) if d == 0 and p > 0
= 0 if d == 0 and p == 0
Applying this to the above example, we have
h(2,3) = sum(1) * 10**3 + (45 * 2 * 3) * 10**2 = 1 * 1000 + 270 * 100 = 28000
h(3,2) = sum(2) * 10**2 + (45 * 3 * 2) * 10**1 = 3 * 100 + 270 * 10 = 3000
h(4,1) = sum(3) * 10**1 + (45 * 4 * 1) * 10**0 = 6 * 10 + 180 * 1 = 240
h(5,0) = sum(4) * 10**0 = 10 * 1 = 10
Therefore
t(2345) = 859 + 28000 + 3000 + 240 + 10 = 32109
The code above implements this algorithm in a straightforward way.
I confirmed the results for the first three examples above by using using @sawa's code to determine the sum of the digits for each number in the range and then summed those totals:
def sum_digits(n)
a = []
until n.zero?
n, r = n.divmod(10)
a.unshift(r)
end
a.reduce(:+)
end
def check_sum_digits_nbrs_up_to(n)
(1..n).reduce(0) {|t,i| t + sum_digits(i) }
end
check_sum_digits_nbrs_up_to(2345) #=> 32109
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With