Using Groovy to Calculate Volume and Surface Area of a CylinderI have recently been involved in checking a seventh grader's math homework to make sure it was done correctly and to help her learn the new concepts. The recent focus of her class has been on volumes and surface areas of various geometric shapes. These can be tedious to check the arithmetic on. Although I do have a
HP48GX calculator (my HP48SX got stolen about 3/4 of the way through my degree) from the days of my undergraduate degree in electrical engineering, its built-in formulas use a definition of
pi with many more significant digits than the seventh graders use (
3.14). There are other options available (such as Google search's
online calculator function), but the potentially different precision of PI is still an issue.
For a variety of reasons (inner geekiness, long-term laziness, and because I can use it as a blog topic) I decided to write a small application to make these checks easier. I did not want to directly deal with
floating-point arithmetic issues and Java's handy
BigDecimal came to mind. At about the same time, I realized that this was a perfect use for
Groovy: I wanted a Java language feature but wanted to be able to write concise, script code.
The
volume (
V = Î r2h
) and
surface area (
SA = 2Î r2 + 2Î rh
) of a
cylinder are the main topics for the current homework assignment. The Groovy code below calculates the volume and surface area of a cylinder given a radius as the first parameter and a height as the second parameter. It is assumed that the radius and height are in the same (unspecified) units.
-
-
-
-
-
-
- pi = 3.14
-
- radius = new BigDecimal(args[0])
- height = new BigDecimal(args[1])
-
- volume = pi * radius * radius * height
- surfaceArea = 2 * pi * radius * radius + pi * 2 * radius * height
-
- println "Volume of cylinder with radius ${radius} and height ${height} is ${volume}."
- println "Surface area of cylinder with radius ${radius} and height ${height} is ${surfaceArea}"
// cylindrical1.groovy
//
// Calculates geometric characteristics of a cylinder with given radius and height.
//
// Don't want to use Math.PI because want same accuracy as used in homework
pi = 3.14
radius = new BigDecimal(args[0])
height = new BigDecimal(args[1])
volume = pi * radius * radius * height
surfaceArea = 2 * pi * radius * radius + pi * 2 * radius * height
println "Volume of cylinder with radius ${radius} and height ${height} is ${volume}."
println "Surface area of cylinder with radius ${radius} and height ${height} is ${surfaceArea}"
Running the script on several different values for radius and height of different cylinders is shown in the next screen snapshot.

The script above does the job, but it can be improved even more. In the second version of this script, I add a conditional check that prints out usage information if the caller does not provide two numeric values for radius and height. I also use Groovy's
GDK-extended
String method
toBigDecimal() rather than the more traditional approach of passing the String to the
BigDecimal constructor.
-
-
-
-
-
- if (args.length < 2)
- {
- println """You must enter at least two numeric parameters for radius and height.
- Usage: cylindrical2 <<radius>> <<height>>"""
- System.exit(args.length)
- }
-
-
- pi = 3.14
-
- radius = args[0].toBigDecimal()
- height = args[1].toBigDecimal()
-
- volume = pi * radius * radius * height
- surfaceArea = 2 * pi * radius * radius + pi * 2 * radius * height
-
- println "Volume of cylinder with radius ${radius} and height ${height} is ${volume}."
- println "Surface area of cylinder with radius ${radius} and height ${height} is ${surfaceArea}"
// cylindrical2.groovy
//
// Calculates geometric characteristics of a cylinder with given radius and height.
//
if (args.length < 2)
{
println """You must enter at least two numeric parameters for radius and height.
Usage: cylindrical2 <<radius>> <<height>>"""
System.exit(args.length)
}
// Don't want to use Math.PI because want same accuracy as used in homework
pi = 3.14
radius = args[0].toBigDecimal()
height = args[1].toBigDecimal()
volume = pi * radius * radius * height
surfaceArea = 2 * pi * radius * radius + pi * 2 * radius * height
println "Volume of cylinder with radius ${radius} and height ${height} is ${volume}."
println "Surface area of cylinder with radius ${radius} and height ${height} is ${surfaceArea}"
The "
happy path" output for this version of the script is the same as for the first version and will not be shown here. However, the usage information provided when an insufficient number of parameters is provided is shown in the next screen snapshot.

One problem still remaining with this script is that non-numeric values passed in for radius or height can be problematic as demonstrated in the next screen snapshot in which the second command-line argument (expected height) is a String.

One approach for dealing with this is to use the
GDK-extended String method
isNumber(). This is the approach applied in the next Groovy code listing.
-
-
-
-
-
- if (args.length < 2 || !args[0].isNumber() || !args[1].isNumber())
- {
- println """You must enter at least two numeric parameters for radius and height.
- Usage: cylindrical2 <<radius>> <<height>>"""
- System.exit(args.length)
- }
-
-
- pi = 3.14
-
- radius = args[0].toBigDecimal()
- height = args[1].toBigDecimal()
-
- volume = pi * radius * radius * height
- surfaceArea = 2 * pi * radius * radius + pi * 2 * radius * height
-
- println "Volume of cylinder with radius ${radius} and height ${height} is ${volume}."
- println "Surface area of cylinder with radius ${radius} and height ${height} is ${surfaceArea}"
// cylindrical3.groovy
//
// Calculates geometric characteristics of a cylinder with given radius and height.
//
if (args.length < 2 || !args[0].isNumber() || !args[1].isNumber())
{
println """You must enter at least two numeric parameters for radius and height.
Usage: cylindrical2 <<radius>> <<height>>"""
System.exit(args.length)
}
// Don't want to use Math.PI because want same accuracy as used in homework
pi = 3.14
radius = args[0].toBigDecimal()
height = args[1].toBigDecimal()
volume = pi * radius * radius * height
surfaceArea = 2 * pi * radius * radius + pi * 2 * radius * height
println "Volume of cylinder with radius ${radius} and height ${height} is ${volume}."
println "Surface area of cylinder with radius ${radius} and height ${height} is ${surfaceArea}"
The improved output now resulting from the providing of a non-numeric value for height is shown in the following screen snapshot.

Although this basic command-line argument checking is not difficult, it turns out that Groovy has more sophisticated command-line argument support built in. Groovy takes advantage of the
Apache CLI library that I have
previously blogged on and makes it even easier to use via
CliBuilder
. The next code listing demonstrates Groovy code using
CliBuilder
to handle command-line arguments. In addition, it provides several other examples of additional "grooviness."
- #!/usr/bin/env groovy
-
-
-
-
-
- def cli = new CliBuilder(usage: 'cylindrical -r radius_arg -l height_arg [-u arg]')
- cli.with
- {
- h(longOpt: 'help', 'Usage Information')
- r(longOpt: 'radius', required: true, type: Number, 'Radius of Cylinder', args: 1)
- l(longOpt: 'length (height)', required: true, type: Number, 'Height of Cylinder', args: 1)
- u(longOpt: 'units', 'Units of Supplied Radius and Height', args: 1)
- p(longOpt: 'pi', 'Value of pi used in calculations', type: Number, args: 1)
- }
- def opt = cli.parse(args)
- if (!opt || opt.h) return
- if (!opt.r.isNumber()) {println "Radius must be numeric."; cli.usage(); return}
- if (!opt.l.isNumber()) {println "Height must be numeric."; cli.usage(); return}
-
- def radius = opt.r.toBigDecimal()
- def height = opt.l.toBigDecimal()
- def printableUnits = opt.u ?: "units"
- def pi = opt.p ? opt.p.toBigDecimal() : Math.PI
-
-
-
-
- def volume = pi * radius * radius * height
- def surfaceArea = 2 * pi * radius * radius + pi * 2 * radius * height
-
- println "All calculation are using ${pi} as the value of 'pi'"
- println "Volume of cylinder with radius ${radius} ${printableUnits} and height ${height} ${printableUnits} is ${volume}."
- println "Surface area of cylinder with radius ${radius} ${printableUnits} and height ${height} ${printableUnits} is ${surfaceArea}"
#!/usr/bin/env groovy
// cylindrical4.groovy
//
// Calculates geometric characteristics of a cylinder with given radius and height.
//
def cli = new CliBuilder(usage: 'cylindrical -r radius_arg -l height_arg [-u arg]')
cli.with
{
h(longOpt: 'help', 'Usage Information')
r(longOpt: 'radius', required: true, type: Number, 'Radius of Cylinder', args: 1)
l(longOpt: 'length (height)', required: true, type: Number, 'Height of Cylinder', args: 1)
u(longOpt: 'units', 'Units of Supplied Radius and Height', args: 1)
p(longOpt: 'pi', 'Value of pi used in calculations', type: Number, args: 1)
}
def opt = cli.parse(args)
if (!opt || opt.h) return // parsing error occurred or usage was requested
if (!opt.r.isNumber()) {println "Radius must be numeric."; cli.usage(); return}
if (!opt.l.isNumber()) {println "Height must be numeric."; cli.usage(); return}
def radius = opt.r.toBigDecimal()
def height = opt.l.toBigDecimal()
def printableUnits = opt.u ?: "units" // "Elvis" operator
def pi = opt.p ? opt.p.toBigDecimal() : Math.PI
// Don't want to use Math.PI because want same accuracy as used in homework
//def pi = 3.14
def volume = pi * radius * radius * height
def surfaceArea = 2 * pi * radius * radius + pi * 2 * radius * height
println "All calculation are using ${pi} as the value of 'pi'"
println "Volume of cylinder with radius ${radius} ${printableUnits} and height ${height} ${printableUnits} is ${volume}."
println "Surface area of cylinder with radius ${radius} ${printableUnits} and height ${height} ${printableUnits} is ${surfaceArea}"
The use of Groovy's built-in support for command-line argument via Apache CLI and
CliBuilder
facilitates easy development of command line options with short (single hyphen) and long (double hyphen) names. Usage information is readily available via
CliBuilder
as well. In the code above, I used
Groovy's with() to group the calls on my "cli" object.
Because
CliBuilder is so easy to use, I threw in two more options to make the script output potentially more interesting and flexible. I also have support for radius and height to be more specifically specified so there is less change of switching the two numeric values on accident. The new command-line options allow the user to optionally specify the units of measurement and the value of PI used. If the units are not specified, the string "units" is used (thanks to the
Elvis operator) and if the value of PI is not specified, the value from
Math.PI is used by default (thanks to traditional Java's
ternary operator).
The next screen snapshot demonstrates the flexibility of this final version of the script for calculating cylindrical volume and surface area.

As the screen snapshot above demonstrates,
CliBuilder
has allowed me to easily apply great flexibility and usefulness in the my script. Other online posts on Groovy and CliBuilder include
Groovy CLIBuilder in Practice,
Groovy Command Line Options,
Groovy CliBuilder with Multiple Arguments, and
Groovy Script: Get SSL Cipher and CLIBuilder Example.
ObservationsGroovy Uses of Standard Java APIs and Third-Party Java APIs and LibrariesOne of the compelling advantages of Groovy is readily usable Java libraries and APIs and Groovy's built-in use of Apache CLI is a great example of this. For example, if the built-in, Apache CLI-powered command line arguments handling is not exactly what you want, you could consider using another Java-based library such as
Args4j. For a direct Groovy alternative to the built-in support using Apache CLI, consider
Groovy Option Parser. Java's
BigDecimal
(which Groovy uses underneath the covers for floating point types by default) was also particularly useful in this example.
Groovy is Great for ScriptingGroovy brings the advantages of Java's rich feature set while at the same time allowing the scripts to be written more script-like.
Programmer Laziness is a Different Type of LazinessAs many blog posts have already pointed out, the "lazy" programmer is often not "lazy" in the traditional sense of the word. The "lazy" programmer tends to go out of his or her way to reduce the tedious tasks that he or she must perform. He or she may be willing to expend significant effort to generate a script to avoid tedium that might, especially in an isolated case, take less time than writing the script. However, if the situation is one that is repeated, the generated script should earn back its time spent many times over.
ConclusionGroovy is the perfect fit for writing a simple script to perform floating-point arithmetic with precise desired precision. I actually used the first version of the script in my case because it was done in about five minutes, but it was fun to make it more elegant and present that here. Another observation from all of this might be developers' strange sense of "fun." Groovy gives me a scripting language with the language richness and features of Java.