Last active
December 22, 2015 16:29
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
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
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
Initially, Scala & Ruby versions are mostly the same. Except for the additional boilerplate code needed in the Scala version to compensate for:
This code has been put into separate classes for readability.
Also absent from the Scala version is the column-width autodetection.