HiveBrain v1.2.0
Get Started
← Back to all entries
patternrubyMinor

Split string into 160-character chunks while adding text to each part

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
while160eachintochunkstextcharacterpartaddingsplit

Problem

The send_sms_message method takes text as an argument. The length of text is unknown and can be 0 or as large as 1070 (after that we will have no space for the actual text, because multi-part message text will be so long).

The trick is that if text is larger then 160 characters, we need to split it into parts and to each part add text part 1 of 2.

The problem is that we don't know the length of the extra text. It can be 14 characters for part 1 of 2 or 16 for part 11 of 99 and so on.

This code can handle up to 99 parts. How can I make it generic to handle more parts without adding more conditions?

SMS_LENGTH = 160
MPM_SIZE_LONG = 16
MPM_SIZE_SHORT = 14
MPM_SHORT_LIMIT = 1314

def send_sms_message(text, to, from)
  unless text.length > SMS_LENGTH
    deliver_message_via_carrier(text, to, from)
  else
    parts = text.scan(/.{1,#{SMS_LENGTH - (text.length > MPM_SHORT_LIMIT ? MPM_SIZE_LONG : MPM_SIZE_SHORT)}}/)
    parts.to_enum.with_index(1) do |message_part, index|
      deliver_message_via_carrier("#{message_part} - Part #{index} of #{parts.length}", to, from)
    end
  end
end

Solution

Some notes:

-
Is your code inside a module or a class, I guess? You can include it in the question.

-
As a general rule, favour the user of affirmative statements (if instead of unless).

-
If you spend enough time with a pencil and a paper, maybe you'd get to a nice formula get_total_parts(message_size, sms_size, parts_message_min_size), but it's not trivial, that's for sure. A pre-computing of the max_size (along with some extra info you'll need) is a bit tedious to write but fast. For example, what's the maximum size if you have a total count with exactly 3 digits (100-999)? (SMS_SIZE - PARTS_MSG_MIN_SIZE - 2) 9 + (SMS_SIZE - PARTS_MSG_MIN_SIZE - 3) 90 + (SMS_SIZE - PARTS_MSG_MIN_SIZE - 4) * 900. You get the idea.

I'd write:

module SMS
  SMS_LENGTH = 160

  PARTS_MESSAGE = " - Part %{n} of %{total}"

  SPLIT_INFO = (1..70).map do |ndigits|
    base_size = (PARTS_MESSAGE % {n: "1", total: "1"}).size 
    parts_per_digit = (0...ndigits).map do |n|
      (SMS_LENGTH - base_size - (ndigits + n - 1)) * (9 * (10 ** n))
    end
    {
      max_size: parts_per_digit.reduce(0, :+), 
      size_of_first_parts: parts_per_digit[0...-1].reduce(0, :+),
      min_parts: 10**(ndigits - 1) - 1, 
      msgsize_for_last_parts: (SMS_LENGTH - base_size - 2 * (ndigits - 1))
    }
  end

  def self.get_total_parts_for_long_message(text)
    info = SPLIT_INFO.detect { |h| text.size  #{to}: #{text} - #{text.size} bytes")
  end
end

Code Snippets

module SMS
  SMS_LENGTH = 160

  PARTS_MESSAGE = " - Part %{n} of %{total}"

  SPLIT_INFO = (1..70).map do |ndigits|
    base_size = (PARTS_MESSAGE % {n: "1", total: "1"}).size 
    parts_per_digit = (0...ndigits).map do |n|
      (SMS_LENGTH - base_size - (ndigits + n - 1)) * (9 * (10 ** n))
    end
    {
      max_size: parts_per_digit.reduce(0, :+), 
      size_of_first_parts: parts_per_digit[0...-1].reduce(0, :+),
      min_parts: 10**(ndigits - 1) - 1, 
      msgsize_for_last_parts: (SMS_LENGTH - base_size - 2 * (ndigits - 1))
    }
  end

  def self.get_total_parts_for_long_message(text)
    info = SPLIT_INFO.detect { |h| text.size <= h[:max_size] } or 
      raise ValueError("Message text too large")
    info[:size_of_first_parts] + 
      Rational(text.size - info[:min_size], info[:msgsize_for_last_parts]).ceil
  end 

  def self.send_sms_message(text, to, from)
    if text.length <= SMS_LENGTH
      deliver_message_via_carrier(text, to, from)
    else
      total_parts = get_total_parts_for_long_message(text) 
      idx = 0

      (1..total_parts).each do |part_index|
        split_message = PARTS_MESSAGE % {n: part_index, total: total_parts}
        user_message_size = SMS_LENGTH - split_message.size
        message_text = text[idx, SMS_LENGTH - user_message_size]
        deliver_message_via_carrier(message_text + split_message, to, from)
        idx += user_message_size
      end
    end
  end

  def self.deliver_message_via_carrier(text, to, from)
    puts("Sending #{from} -> #{to}: #{text} - #{text.size} bytes")
  end
end

Context

StackExchange Code Review Q#133551, answer score: 2

Revisions (0)

No revisions yet.