Here's revised script which added code to shuffle the slots of each day in order to randomise the order of lessons in a day if it matters.
Ruby script in shell:
#!/bin/bash
export LC_ALL=en_GB.UTF-8
/usr/bin/ruby -w <<'EOF' - <(pbpaste) | pbcopy
#
# ARGF : TSV file of fields A B C1 [C2 ...]
# A : lesson
# B : number of days of lesson
# C* : excluded day of lesson
#
# output : TSV text representing generated schedule D[i,j,k] where
# D[i,j,k] denotes lesson assigned to slot j of day i in schedule k;
# TSV fields represent tupple (k,j) and TSV record i represents D[i,j,k]
#
# * schedule is built to minimise the number of days with lessons assigned
# * if it fails to generate k'th schedule, D[*,*,k] is left blank
#
# e.g.,
# Given input:
# DAYS = (1..7),
# SLOTS = 2,
# SCHEDULES = 5,
# TSV =
# A B C1 C2
# -------------
# 1 3 5 7
# 2 3 1 3
# 3 3 4
#
# output (e.g.):
# TSV =
# k=1 1 2 2 3 3 4 4 5 5
# j=1 2 1 2 1 2 1 2 1 2
# -------------------------------------
# 1 3 3 3
# 1 2 2 1 3 2 2 3
# 3 1 3 1 3 1 3 1 3
# 1 2 1 2 1 2 2 1
# 2 3 2 3
# 2 3 2 1 1 3 1 2 2 1
# 2 3
#
# version :
# v0.11 - added code to shuffle slots of each day
#
DAYS = (1..30) # days for schedule
SLOTS = 2 # number of slots per day
SCHEDULES = 5 # number of schedules to generate
MAXTRIES = 16 # max number of trials for each schedule
DEBUG = true # debug flag: true to print debug information to $stderr, false otherwise.
def array2text(aa, opts = {})
# array aa : 2d array
# hash opts : {:fs => fs, :rs => rs}
# string fs : field separator
# string rs : record separator
fs, rs = {:fs => %[\t], :rs => %[\n]}.merge(opts).values_at(:fs, :rs)
return aa.map {|a| a.join(fs) }.join(rs) + rs
end
def text2array(t, opts = {})
# string t : text representation of 2d array
# hash opts : {:fs => fs, :rs => rs}
# string fs : field separator
# string rs : record separator
fs, rs = {:fs => %[\t], :rs => %[\n]}.merge(opts).values_at(:fs, :rs)
return t.split(rs).map {|a| a.split(fs, -1)}
end
def schedule(pp)
# array pp : array of [lesson, number of days of lesson, [excluded days of lesson]]
# return hash dd : scheduled table { day => [slots] }
aa, bb, cc = pp.shuffle!.transpose
b_total = bb.inject { |s, b| s + b }
b_threshold = b_total / SLOTS
rk = 0
while (rk += 1) <= MAXTRIES do
dd = DAYS.inject({}) { |h, i| h[i] = []; h }
b_assigned = 0
na = catch :not_assigned do
while b_total > b_assigned do
pp.each do |p|
a, b, c = p
b.times do
ii = dd.keys.select { |d| dd[d].length > 0 } # assinged
jj = ii.select { |d| dd[d].length < SLOTS } # assinged and with slot available
if ii.length < b_threshold || jj.length == 0
kk = dd.keys.select { |d| ! c.include?(d) && dd[d].length < SLOTS && ! dd[d].include?(a) }
else
kk = jj.select { |d| ! c.include?(d) && ! dd[d].include?(a) }
end
throw :not_assigned, a if kk == []
dd[kk[rand(kk.length)]] << a
b_assigned += 1
end
end
end
nil
end
break unless na
end
if DEBUG
$stderr.puts "tries => %d, na => %s\n" % [rk, na.inspect]
if na
$stderr.puts dd.inspect
$stderr.puts array2text(dd.keys.sort.inject([]) { |r, d| r << dd[d].map { |e| '%s*' % e } })
end
end
dd = DAYS.inject({}) { |h, i| h[i] = []; h } if na
dd
end
aa, bb, *cc = text2array(ARGF.read).transpose
bb.map! { |i| i.to_i }
cc = cc.transpose.map { |c| c.map { |e| e == "" ? nil : e.to_i }.compact }
qq = []
SCHEDULES.times { qq << schedule([aa, bb, cc].transpose) }
rr = qq.inject([]) do |q, dd|
q << dd.keys.sort.inject([]) { |r, d| r << (e = dd[d].shuffle).fill('', e.size..SLOTS - 1) }
end
print array2text(rr.transpose)
EOF
And AppleScript wrapper, which now prints debug information in result window whilst it puts generated schedules in the clipboard:
--APPLESCRIPT
do shell script "/bin/bash -s <<'HUM' -
export LC_ALL=en_GB.UTF-8
{
/usr/bin/ruby -w <<'EOF' - <(pbpaste) | pbcopy
#
# ARGF : TSV file of fields A B C1 [C2 ...]
# A : lesson
# B : number of days of lesson
# C* : excluded day of lesson
#
# output : TSV text representing generated schedule D[i,j,k] where
# D[i,j,k] denotes lesson assigned to slot j of day i in schedule k;
# TSV fields represent tupple (k,j) and TSV record i represents D[i,j,k]
#
# * schedule is built to minimise the number of days with lessons assigned
# * if it fails to generate k'th schedule, D[*,*,k] is left blank
#
# e.g.,
# Given input:
# DAYS = (1..7),
# SLOTS = 2,
# SCHEDULES = 5,
# TSV =
# A B C1 C2
# -------------
# 1 3 5 7
# 2 3 1 3
# 3 3 4
#
# output (e.g.):
# TSV =
# k=1 1 2 2 3 3 4 4 5 5
# j=1 2 1 2 1 2 1 2 1 2
# -------------------------------------
# 1 3 3 3
# 1 2 2 1 3 2 2 3
# 3 1 3 1 3 1 3 1 3
# 1 2 1 2 1 2 2 1
# 2 3 2 3
# 2 3 2 1 1 3 1 2 2 1
# 2 3
#
# version :
# v0.11 - added code to shuffle slots of each day
#
DAYS = (1..30) # days for schedule
SLOTS = 2 # number of slots per day
SCHEDULES = 5 # number of schedules to generate
MAXTRIES = 16 # max number of trials for each schedule
DEBUG = true # debug flag: true to print debug information to $stderr, false otherwise.
def array2text(aa, opts = {})
# array aa : 2d array
# hash opts : {:fs => fs, :rs => rs}
# string fs : field separator
# string rs : record separator
fs, rs = {:fs => %[\\t], :rs => %[\\n]}.merge(opts).values_at(:fs, :rs)
return aa.map {|a| a.join(fs) }.join(rs) + rs
end
def text2array(t, opts = {})
# string t : text representation of 2d array
# hash opts : {:fs => fs, :rs => rs}
# string fs : field separator
# string rs : record separator
fs, rs = {:fs => %[\\t], :rs => %[\\n]}.merge(opts).values_at(:fs, :rs)
return t.split(rs).map {|a| a.split(fs, -1)}
end
def schedule(pp)
# array pp : array of [lesson, number of days of lesson, [excluded days of lesson]]
# return hash dd : scheduled table { day => [slots] }
aa, bb, cc = pp.shuffle!.transpose
b_total = bb.inject { |s, b| s + b }
b_threshold = b_total / SLOTS
rk = 0
while (rk += 1) <= MAXTRIES do
dd = DAYS.inject({}) { |h, i| h[i] = []; h }
b_assigned = 0
na = catch :not_assigned do
while b_total > b_assigned do
pp.each do |p|
a, b, c = p
b.times do
ii = dd.keys.select { |d| dd[d].length > 0 } # assinged
jj = ii.select { |d| dd[d].length < SLOTS } # assinged and with slot available
if ii.length < b_threshold || jj.length == 0
kk = dd.keys.select { |d| ! c.include?(d) && dd[d].length < SLOTS && ! dd[d].include?(a) }
else
kk = jj.select { |d| ! c.include?(d) && ! dd[d].include?(a) }
end
throw :not_assigned, a if kk == []
dd[kk[rand(kk.length)]] << a
b_assigned += 1
end
end
end
nil
end
break unless na
end
if DEBUG
$stderr.puts \"tries => %d, na => %s\\n\" % [rk, na.inspect]
if na
$stderr.puts dd.inspect
$stderr.puts array2text(dd.keys.sort.inject([]) { |r, d| r << dd[d].map { |e| '%s*' % e } })
end
end
dd = DAYS.inject({}) { |h, i| h[i] = []; h } if na
dd
end
aa, bb, *cc = text2array(ARGF.read).transpose
bb.map! { |i| i.to_i }
cc = cc.transpose.map { |c| c.map { |e| e == \"\" ? nil : e.to_i }.compact }
qq = []
SCHEDULES.times { qq << schedule([aa, bb, cc].transpose) }
rr = qq.inject([]) do |q, dd|
q << dd.keys.sort.inject([]) { |r, d| r << (e = dd[d].shuffle).fill('', e.size..SLOTS - 1) }
end
print array2text(rr.transpose)
EOF
} 2>&1
HUM"
--END OF APPLESCRIPT
Regards,
H