2

I'm interested in possible ways that different languages can Join an array, but rather than using a single join string, using a different join string at given intervals.

For example (hypothetical language):

Array.modJoin([mod, char], ...)
e.g. [1,2,3,4,5].modJoin( [1, ","], [2, ":"] )

Where the arguments specify an array or object containing a modulo and a join char, the implementation would check which order of modulo took precedence (the latest one), and apply the join char. (requiring that the [mod,char]'s were provided in ascending mod order)

i.e.

if (index % (nth mod) == 0) 
  append (join char) 
  continue to next index 
else 
  (nth mod)-- repeat

when complete join with ""

For example, I've come up with the following in Ruby, but I suspect better / more elegant methods exist, and that's what I'd like to see.

#Create test Array 
#9472 - 9727 as HTML Entities (unicode box chars)
range       = (9472..9727).to_a.map{|u| "&##{u};" }  

Assuming we have a list of mod's and join chars, we have to stipulate that mods increase in value as the list progresses.

mod_joins   = [{m:1, c:",", m:12, c:"<br/>"]

Now process the range with mod_joins

processed = range.each_with_index.map { |e, i| 
  m = nil
  mods.reverse_each {|j|
    m = j and break if i % j[:m] == 0
  }
  # use the lowest mod for index 0
  m = mods[0] if i == 0 
  m = nil ? e : "#{e}#{m[:c]}"
}

#output the result string
puts processed.join ""

From this we have a list of htmlEntities, separated by , unless it's index is a 12th modulo in which case it's a <br/>

So, I'm interested for ways this can be done more elegantly, primarily in functional languages like Haskell, F#, Common Lisp (Scheme, Clojure) etc. but also cool ways to achieve this in general purpose languages that have list comprehension extensions such as C# with Linq, Ruby and Python or even Perl.

ocodo
  • 29,401
  • 18
  • 105
  • 117

2 Answers2

1

Here's a pure-functional version written in Python. I'm sure it can be adapted easily enough to other languages.

import itertools

def calcsep(num, sepspecs):
  '''
    num: current character position
    sepspecs: dict containing modulus:separator entries
  '''
  mods = reversed(sorted(sepspecs))
  return sepspecs[next(x for x in mods if num % x == 0)]

vector = [str(x) for x in range(12)]

result = [y for ix, el in enumerate(vector)
            for y in (calcsep(ix, {1:'.', 3:',', 5:';'}), el)]

print ''.join(itertools.islice(result, 1, None))
Ignacio Vazquez-Abrams
  • 776,304
  • 153
  • 1,341
  • 1,358
  • I'll go out on a limb here and say that Ruby seems to be able to do it a bit cleaner, also I didn't really mean port my implementation to another language, while losing brevity, I meant use novel features of the language to improve the readability of the routine. – ocodo Dec 21 '12 at 04:03
  • It can do it more concisely and tersely. "Cleaner" is usually a matter of taste. – Ignacio Vazquez-Abrams Dec 21 '12 at 04:05
  • Of course, it's definitely a matter of taste, for example, my `do something if condition` is a bit of a perlism. I meant cleaner as in not having to reverse the mods explicitly and then process them. – ocodo Dec 21 '12 at 04:08
  • I suppose without the usage directly underneath, it's looking better (my initial reaction was at a glance.) Excuse me. – ocodo Dec 21 '12 at 04:09
  • by the way, it's not a separator, it's a concatenator. So the names `joinspecs` and `concatentator` (or similar are more on point) – ocodo Dec 21 '12 at 04:10
  • Equally, `num: array index position` – ocodo Dec 21 '12 at 04:14
  • [Python calls it a separator](http://docs.python.org/2/library/stdtypes.html#str.join), so I maintained that terminology. – Ignacio Vazquez-Abrams Dec 21 '12 at 04:17
  • Ah, I see, the array is passed to the string method. – ocodo Dec 21 '12 at 04:18
  • As for the "porting" bit, I didn't even look at your code before writing this; call it "homogenous convergence". – Ignacio Vazquez-Abrams Dec 21 '12 at 04:22
  • If you've got it in you, I'd love to see a more concise / terse implementation. – ocodo Dec 21 '12 at 04:22
  • I can make it more concise by inlining everything, but writing terse Python is... harder than it looks. – Ignacio Vazquez-Abrams Dec 21 '12 at 04:23
  • I don't doubt it ("homogenous convergence") what I'm really interested in is if a language like Haskell has this sort of thing licked, or if it's still really a `.map` job. – ocodo Dec 21 '12 at 04:24
  • If it's essentially the same implementation, not to worry. (definitely didn't mean to offend with the porting remark, just trying to clarify what I'm seeking.) – ocodo Dec 21 '12 at 04:24
  • One could be clever and write code that uses stepped slice assignment to replace the appropriate separators, but I would be hard-pressed to call that an improvement over the functional version. – Ignacio Vazquez-Abrams Dec 21 '12 at 04:32
  • I see, the main "clean" part is about semantics when it comes to list comprehension, `for` is appearing a lot, where Ruby (lisp, linq, etc.) have a lot special purpose of enumerators. They all boil down to the same thing of course, although each one method can be optimised for their specific case in the compiler/interpreter. This is the interesting bit, for me at least. – ocodo Dec 21 '12 at 04:34
  • Yes, that doesn't really sound like an improvement. The functional version is the way to go. – ocodo Dec 21 '12 at 04:35
  • Hope you see @akuhn's answer, it's things like this that make Ruby very nice. – ocodo Dec 21 '12 at 05:33
  • It's nothing that couldn't be done in Python, except `each_slice()` becomes [zip-iter](http://stackoverflow.com/q/2233204/20862). – Ignacio Vazquez-Abrams Dec 21 '12 at 05:43
  • Oh, very nice. Is this the stepped slice you referred to? – ocodo Dec 21 '12 at 06:00
  • Have to say, the Ruby is eminently more readable, and also module imports, just core library. – ocodo Dec 21 '12 at 06:07
  • No, stepped slice assignment is `foo[n::m] = someseq`. `zip()` will take one from whatever is there; 3 will result in 3 taken, 5 will result in 5, etc. – Ignacio Vazquez-Abrams Dec 21 '12 at 07:33
1

Here’s a simpler and more readable solution in Ruby

array = (9472..9727).map{|u|"&##{u};"}
array.each_slice(12).collect{|each|each.join(",")}.join("<br/>")

or for the general case

module Enumerable   
  def fancy_join(instructions)
    return self.join if instructions.empty?
    separator = instructions.delete(mod = instructions.keys.max)
    self.each_slice(mod).collect{|each|each.fancy_join(instructions.dup)}.join(separator)
  end
end

range = (9472..9727).map{|u|"&##{u};"}
instructions = {1=>",",12=>"<br/>"}

puts array.fancy_join(instructions)    
akuhn
  • 27,477
  • 2
  • 76
  • 91
  • Very nice work, I had no idea `each_slice` existed. This looks like a probably Green tick, I'll wait until tomorrow to see if there's anything better. I think it's unlikely though. And this automatically takes care of the index 0 problem. – ocodo Dec 21 '12 at 05:26
  • @slomojo `each_slice` and its sibling `each_cons` are very powerful indeed. I’ve added code to cover the general case. – akuhn Dec 21 '12 at 05:39
  • Thanks, I was just about to come back and ask for the mulitiple arguments case. – ocodo Dec 21 '12 at 05:41
  • If you were going to add `instruction.empty?` you should thrown an error, rather than doing a regular join. – ocodo Dec 21 '12 at 05:44
  • Yes and yes. You need to anchor the recursion, either by testing for being at the end of your instructions or by testing for slices of length 1 using `return self if self.size == 1` (though in that case your instructions MUST provide a separator for mod 1, so for example `array.fancy_join(4=>",",12=>";")` would not work while it works with above code). – akuhn Dec 21 '12 at 05:48
  • Yep, just noticed the recursion, scuse me. – ocodo Dec 21 '12 at 05:51
  • That's a slight problem with the mod 1, but not difficult to fix. – ocodo Dec 21 '12 at 05:57