Skip to content

Instantly share code, notes, and snippets.

@sebnozzi
Last active December 22, 2015 16:29
Show Gist options
  • Save sebnozzi/6500138 to your computer and use it in GitHub Desktop.
Save sebnozzi/6500138 to your computer and use it in GitHub Desktop.
Exploring code transformation from Ruby to Scala. Taken from http://rosettacode.org/wiki/Calendar#Ruby . Taking Ruby as the original version, I want to show that it's possible to write more or less the same code in Scala. Except for the fact that Ruby comes with more "batteries included" than the JDK/Scala.
require 'date'
# Creates a calendar of _year_. Returns this calendar as a multi-line
# string fit to _columns_.
def cal(year, columns)
# Start at January 1.
#
# Date::ENGLAND marks the switch from Julian calendar to Gregorian
# calendar at 1752 September 14. This removes September 3 to 13 from
# year 1752. (By fortune, it keeps January 1.)
#
date = Date.new(year, 1, 1, Date::ENGLAND)
# Collect calendars of all 12 months.
months = (1..12).collect do |month|
rows = [Date::MONTHNAMES[month].center(20), "Su Mo Tu We Th Fr Sa"]
# Make array of 42 days, starting with Sunday.
days = []
date.wday.times { days.push " " }
while date.month == month
days.push("%2d" % date.mday)
date += 1
end
(42 - days.length).times { days.push " " }
days.each_slice(7) { |week| rows.push(week.join " ") }
next rows
end
# Calculate months per row (mpr).
# 1. Divide columns by 22 columns per month, rounded down. (Pretend
# to have 2 extra columns; last month uses only 20 columns.)
# 2. Decrease mpr if 12 months would fit in the same months per
# column (mpc). For example, if we can fit 5 mpr and 3 mpc, then
# we use 4 mpr and 3 mpc.
mpr = (columns + 2).div 22
mpr = 12.div((12 + mpr - 1).div mpr)
# Use 20 columns per month + 2 spaces between months.
width = mpr * 22 - 2
# Join months into calendar.
rows = ["[Snoopy]".center(width), "#{year}".center(width)]
months.each_slice(mpr) do |slice|
slice[0].each_index do |i|
rows.push(slice.map {|a| a[i]}.join " ")
end
end
return rows.join("\n")
end
ARGV.length == 1 or abort "usage: #{$0} year"
# Guess width of terminal.
# 1. Obey environment variable COLUMNS.
# 2. Try to require 'io/console' from Ruby 1.9.3.
# 3. Try to run `tput co`.
# 4. Assume 80 columns.
columns = begin Integer(ENV["COLUMNS"] || "")
rescue
begin require 'io/console'; IO.console.winsize[1]
rescue LoadError
begin Integer(`tput co`)
rescue
80; end; end; end
puts cal(Integer(ARGV[0]), columns)
package com.sebnozzi.rosettacode
import scala.collection.mutable.Buffer
import java.util.Locale
object MutableYearCalendarApp extends App with MutableExtras {
val yearToDisplay = args.headOption.getOrElse("1969").toInt
cal(year = yearToDisplay, columns = 86).foreach(println)
// Creates a calendar of _year_. Returns this calendar as a multi-line
// string fit to _columns_.
def cal(year: Int, columns: Int): Seq[String] = {
// England switched from Julian calendar to Gregorian calendar
// at 1752 September 14. This removes September 3 to 13 from
// year 1752. (By fortune, it keeps January 1.)
val date = EnglandCalendar.newForYear(year)
// Collect calendars of all 12 months.
val months = (1 to 12).map { month =>
val rows = Buffer(
date.monthName.center(20),
"Su Mo Tu We Th Fr Sa")
// Make array of 42 days, starting with Sunday.
val days = Buffer[String]()
(0 until date.dayOfWeek).foreach { _ => days += " " }
while (date.monthNr == month) {
days += ("%2d".format(date.dayOfMonth))
date.addDays(1)
}
(1 to 42 - days.length).foreach { _ => days += " " }
days.grouped(7).foreach { week => rows += week.mkString(" ") }
rows
}
// Calculate months per row (mpr).
// 1. Divide columns by 22 columns per month, rounded down. (Pretend
// to have 2 extra columns; last month uses only 20 columns.)
// 2. Decrease mpr if 12 months would fit in the same months per
// column (mpc). For example, if we can fit 5 mpr and 3 mpc, then
// we use 4 mpr and 3 mpc.
val mpr = {
val x = (columns + 2) / 22
12 / ((12 + x - 1) / x)
}
// Use 20 columns per month + 2 spaces between months.
val width = mpr * 22 - 2
// Join months into calendar.
val rows = Buffer("[Snoopy]".center(width), s"${year}".center(width))
months.grouped(mpr).foreach { slice =>
slice.head.indices.foreach { i =>
rows += slice.map { a => a(i) }.mkString(" ")
}
}
rows
}
}
trait MutableExtras {
import java.util.Calendar
import java.text.SimpleDateFormat
import java.util.GregorianCalendar
implicit class MyString(str: String) {
def center(width: Int): String = {
val leftSpaces = (width / 2) - (str.length() / 2)
val rightSpaces = width - (leftSpaces + str.length)
(" " * leftSpaces) + str + (" " * rightSpaces)
}
}
// England switched from Julian calendar to Gregorian calendar
// at 1752 September 14. This removes September 3 to 13 from
// year 1752. (By fortune, it keeps January 1.)
// Java's default implementation changes Julian => Gregorian in 1582
// Only England changed later. Need to set this manually.
object EnglandCalendar {
def newForYear(year: Int) = {
val cal = new GregorianCalendar()
val gregorianChangeDate = {
val d = Calendar.getInstance()
d.set(1752, Calendar.SEPTEMBER, 14)
d.getTime()
}
cal.setGregorianChange(gregorianChangeDate)
cal.set(Calendar.YEAR, year)
cal.set(Calendar.DAY_OF_YEAR, 1)
cal
}
}
implicit class EnglandCalendar(javaCalendar: Calendar) {
import EnglandCalendar._
def monthName = javaCalendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.ENGLISH)
def monthNr = javaCalendar.get(Calendar.MONTH) + 1
def dayOfMonth = javaCalendar.get(Calendar.DAY_OF_MONTH)
def dayOfWeek = javaCalendar.get(Calendar.DAY_OF_WEEK) - 1 // Sunday = 0, etc.
def addDays(days: Int) = javaCalendar.add(Calendar.DATE, days)
}
}
package com.sebnozzi.rosettacode
/**
* Loosely based on the Ruby implementation.
*
* Focuses on immutability and Scala idioms,
* while trying to remain readable and clean.
*/
object YearCalendarApp extends App with Extras {
val defaultYear = 1752
val columns = 86
val year = args.headOption.map(_.toInt).getOrElse(defaultYear)
yearCalendarLines(year, columns).foreach(println)
def yearCalendarLines(year: Int, columns: Int): Seq[String] = {
// At least one. Divide rest columns by width + 2 spaces (separator)
val calendarsPerRow = 1 + (columns - 20) / (20 + 2)
// Use 20 columns per month + 2 spaces between months
val width = calendarsPerRow * 22 - 2
List(
"[Snoopy]".center(width),
s"${year}".center(width)) ++
// Get, group, transpose and join calendar lines
allMonthCalendarLines(year).grouped(calendarsPerRow).flatMap { calGroup =>
calGroup.transpose.map(stringsInRow => stringsInRow.mkString(" "))
}
}
def allMonthCalendarLines(year: Int): Seq[Seq[String]] = {
(1 to 12).map { monthNr =>
val date = MonthCalendar(monthInYear = monthNr, year)
// Make array of 42 days (7 * 6 weeks max.) starting with Sunday (which is 0)
val daySlotsInMonth = {
(Seq().padTo(date.dayOfWeek, " ") ++
date.daysInMonth.map { dayNr => "%2d".format(dayNr) }).
padTo(42, " ")
}
List(
date.monthName.center(20),
"Su Mo Tu We Th Fr Sa") ++
daySlotsInMonth.grouped(7).map { weekSlots => weekSlots.mkString(" ") }
}
}
}
/**
* This provides extra classes needed for the main
* algorithm.
*/
trait Extras {
import java.util.Calendar
import java.util.GregorianCalendar
import java.text.SimpleDateFormat
import java.util.Locale
import scala.collection.mutable.Buffer
implicit class MyString(str: String) {
def center(width: Int): String = {
val leftSpaces = (width / 2) - (str.length() / 2)
val rightSpaces = width - (leftSpaces + str.length)
(" " * leftSpaces) + str + (" " * rightSpaces)
}
}
case class MonthCalendar(monthInYear: Int, year: Int) {
private val javaCalendar = makeJavaCalendar(year, monthInYear)
private def makeJavaCalendar(year: Int, monthInYear: Int): Calendar = {
val calendar = new GregorianCalendar()
// Actually, other countries changed already in 1582,
// which is the JDK's default implementation.
val gregorianDateChangeInEngland = {
val d = Calendar.getInstance()
d.set(1752, Calendar.SEPTEMBER, 14)
d.getTime()
}
// For England we need to set this explicitly.
calendar.setGregorianChange(gregorianDateChangeInEngland)
calendar.set(Calendar.DAY_OF_MONTH, 1)
calendar.set(Calendar.YEAR, year)
calendar.set(Calendar.MONTH, monthInYear - 1)
calendar
}
val monthName = javaCalendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.ENGLISH)
val dayOfWeek = javaCalendar.get(Calendar.DAY_OF_WEEK) - 1
val daysInMonth = {
val tempCal = makeJavaCalendar(year, monthInYear)
val dayNumbers = Buffer[Int]()
while (tempCal.get(Calendar.MONTH) == monthInYear - 1) {
dayNumbers += tempCal.get(Calendar.DAY_OF_MONTH)
tempCal.add(Calendar.DATE, 1)
}
dayNumbers
}
}
}
@sebnozzi
Copy link
Author

sebnozzi commented Sep 9, 2013

Initially, Scala & Ruby versions are mostly the same. Except for the additional boilerplate code needed in the Scala version to compensate for:

  • lack of String#center method
  • lack of seamless Date handling on Java (infamous problem)

This code has been put into separate classes for readability.

Also absent from the Scala version is the column-width autodetection.

@sebnozzi
Copy link
Author

sebnozzi commented Sep 9, 2013

Some characteristics of the Ruby coding-culture:

  • pragmatism is apparent
  • at the same time, the solution is very readable and elegant
  • short variable names are "ok"
  • short function names are "ok" (e.g. "cal")
  • re-assignment to variables are ok
  • using arrays (mutability) is ok
  • having functions / methods that do a lot is ok

@sebnozzi
Copy link
Author

sebnozzi commented Sep 9, 2013

With the immutable Scala version I wanted to present some good aspects of the Scala coding culture (while trying to avoid the bad ones). These are:

  • succinct and elegant code
  • longer variables preferred
  • longer method names preferred
  • focus on immutable data structures
  • focus on immutable variables (vals)
  • short functions / classes
  • isolate computations
  • isolate mutability

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment