Assuming you took CS5, at this point in CS60 youve worked with (at least) three programming languages: Python, HMMM, and Racket. This week - and in the coming weeks - we will add Java to that list (if it's not already there).
In addition, over the next two weeks you'll build your own task-specific language that will be able to handle arbitrary multiplication-based conversions from one unit to another -- and account for error propagation, too. To do this, your application will be able to navigate within arbitrarily large graphs of unit-conversion data. We're calling this application a unit-calculator, or unicalc.
In the spirit of both Racket and Java, you'll prototype your application in Racket this week. In addition, this week you'll build the fundamental data structure for a Java implementation of unicalc, which you'll complete next week. This data structure will be the OpenList class, in which you'll create Racket-like linked lists that can be naturally handled recursively. Thus, OpenList "shows off" Java's data-structuring capabilities without sacrificing the generality and power of Racket/Scheme.
Problems 1 and 3 this week may be done in pairs -- or individually, if you prefer. Problem 2 should be completed individually. Overall, you will submit 4 files for this week's three problems:
Pair or individual problem You may work on this one either individually or in a pair. If you work in a pair, be sure to follow the CS 60 pair-programming guidelines.
30 Points
This problem asks you to write several Racket functions: together, they will implement a "unit-calculator" or unicalc application. A complete application will be able to convert any unit quantity to any compatible quantity by using an association list as a database of known conversions.
There are two files with which to start:
unicalc.rkt and unicalc-db.rkt
The first you will modify to implement unicalc; the second holds the unicalc
unit-conversion database.
Quantity Lists The fundamental structure used in unicalc is called a Quantity List. As a data structure, a Quantity List is a 4-element Racket list in which
'(9.8 (meter) (second second) 0.01)
Generally speaking, a data structure is rather low-level: it specifies the particular way that language-specific and/or machine-specific constructs are used to organize data, as the above example indicates. An information structure, on the other hand, provides problem-specific capabilities that are independent of a particular application. To reinforce this distinction, we provide a make-QL function in Racket that creates a Quantity List. In addition, we provide the accessor functions get-quant, get-num, get-den, and get-unc, which return the individual pieces of a Quantity List. In order to compare and sort lists of symbols, we provide functions sym<? and sortsym.
In Racket, the difference between data structure and information structure is almost entirely one's point of view. After all, the different ways of organizing data are relatively limited!
The unit database
In the file unicalc-db.rkt is a
unit-conversion database in the form of an association list - here
are a few of its conversions:
Not every unit is defined in terms of others in the
database. Some are intentionally undefined: these are
what we call basic units, e.g.,
second, kg, meter, and ampere.
(define unicalc-db
(list
(list 'A (make-QL 1.0 '(ampere) '() 0.0))
(list 'faraday (make-QL 96490.0 '(coulomb) '() 0.0))
(list 'farad (make-QL 1.0 '(coulomb) '(volt) 0.0))
(list 'fathom (make-QL 6.0 '(foot) '() 0.0))
(list 'hour (make-QL 60.0 '(minute) '() 0.0))
(list 'inch (make-QL 0.02539954113 '(meter) '() 0.0))
... ))
When a quantity is normalized, it is converted entirely into basic units. Also, the database does not contain conversions from everything to everything: that would make it very large and difficult to maintain! Instead, the database can be used multiple times, e.g., to convert furlongs to miles to feet to inches to meters.
A note on uncertainty-propagation: there are many online references that indicate how to propagate uncertainty through various functions. We used this reference from Harvard and this error-propagation calculator as a check for our results. Our uncertainties represent the size of one-standard-deviation within a Gaussian distribution around the quantity in our quantity list - note that we're not tracking relative uncertainties here, though the formulas below do compute them, as needed.
(define (cancel L1 L2)
that takes in two sorted lists of symbols, L1 and L2 and
then returns a new list of all the symbols in L1 after
canceling with L2. The output list should still
be sorted, too. Here, canceling means removing
common symbols as many times as they occur in both lists.
These tests will make that clearer:
(check-expect (cancel '(m m s) '(kg m s s)) '(m))
(check-expect (cancel '(m s) '(kg m s s)) '())
(check-expect (cancel '(m s s s) '(kg m s s)) '(s))
(check-expect (cancel '(kg m s s) '(m m s)) '(kg s))
For this function
use the same element-by-element approach as in the merge
technique we talked about it class: it will thus be O(N),
where N is the length of the smaller of L1 and L2.
The function sym<? is provided to compare
two symbols alphabetically.
The details will be slightly different, but your code will
have the same structure as it did in merge.
(define (simplify QL)
that takes in a quantity list QL and
returns another quantity list of the same value, but
having canceled out any shared elements between the numerator
and denominator. Here are some examples:
(check-expect (equal-QLs? (make-QL 1.0 '(m) '(s) 0.0)
(simplify '(1.0 (m m s) (m s s) 0.0)))
#t)
(check-expect (equal-QLs? (make-QL 1.0 '(m) '(kg) 0.0)
(simplify '(1.0 (kg m m s) (kg kg m s) 0.0)))
#t)
(define (multiply QL1 QL2)
that takes in two quantity lists QL1 and QL2
and returns a quantity list representing their product.
The result returned should be simplified!.
Note that if q1 and q2 are the quantities and u1 and u2 are
the uncertainties in QL1 and QL2,
then the uncertainty in the product is given by
(check-expect (equal-QLs? (make-QL 1764.0 '(m) '(s) 59.39696962)
(multiply (make-QL 42.0 '(kg m m) '(s s) 1.0)
(make-QL 42.0 '(s) '(kg m) 1.0)))
#t)
(check-expect (equal-QLs? (make-QL 1.0 '(kg meter x) '(amp s w) 0.02061553)
(multiply (make-QL 2.0 '(meter) '(amp w) 0.01)
(make-QL 0.5 '(kg x) '(s) 0.01)))
#t)
This was an example from class... . Use the helper function
merge, to ensure O(N) running time, as we did there.
(define (power QL p)
that takes in a quantity lists QL and an
integer p
and returns a quantity list representing QLp.
(check-expect (equal-QLs? (make-QL 1.0 '() '() 0.0)
(power (make-QL 200.0 '(euro) '() 1.0) 0))
#t)
(check-expect (equal-QLs? (make-QL 0.005 '() '(euro) 2.5e-005)
(power (make-QL 200.0 '(euro) '() 1.0) -1))
#t)
(check-expect (equal-QLs? (make-QL 8000000.0 '(euro euro euro) '() 120000.0)
(power (make-QL 200.0 '(euro) '() 1.0) 3))
#t)
Warning Implement power on its own via
recursion - don't use multiply. The reason is that uncertainty
propagates differently when you multiply two independent
variables than when you multiply the same variable, as power
does.
(define (divide QL1 QL2)
that takes in two quantity lists QL1 and QL2
and returns a quantity list representing the quotient of QL1/QL2.
Again, the result returned should be simplified! Note that
the uncertainty is similar to the multiplication case:
(check-expect (equal-QLs? (make-QL 1.0 '(m) '(s) 0.03367175)
(divide (make-QL 42.0 '(kg m m) '(s s) 1.0)
(make-QL 42.0 '(kg m) '(s) 1.0)))
#t)
(check-expect (equal-QLs? (make-QL 4.0 '(meter s) '(amp kg w x) 0.08246211)
(divide (make-QL 2.0 '(meter) '(amp w) 0.01)
(make-QL 0.5 '(kg x) '(s) 0.01)))
#t)
(define (norm-unit unit)
that takes in a Racket symbol unit and
returns the equivalent normalized quantity list.
This will require not only looking up unit in the
provided database, unicalc-db, as we did in class,
but then making sure the final result is normalized,
that is, consists only of basic units. The final result
should be simplified, as well. You should use 0.0
as the uncertainty of a plain unit (the unicalc
database will do this for you). Here are some examples:
(check-expect (equal-QLs? (make-QL 1.0 '(kg meter meter)
'(ampere second second second second) 0.0)
(norm-unit 'weber))
#t)
(check-expect (equal-QLs? (make-QL 1.0 '(ampere) '() 0.0)
(norm-unit 'ampere))
#t)
Hint: read over norm-QL before implementing
norm-unit. Then, implement each one so that it
calls the other: an example of
mutual recursion. If you'd like a bit more
explanation on this idea, see the q/a link at the
end of norm-QL.
(define (norm-QL QL)
that takes in a quantity list QL and
returns the equivalent normalized quantity list.
This will require lots of looking-up in the
provided database, so we strongly suggest a
mutual-recursion approach with norm-unit.
That is, "peel off" one unit, if there is one,
then use norm-unit to find the quantity list
equivalent to that unit. Then use multiply
or divide, as appropriate. Recursion will
be crucial here! The final result
should be simplified. Here are some examples:
(check-expect (equal-QLs? (make-QL 42.0 '() '(ampere second) 1.0)
(norm-QL (make-QL 42.0 '(weber) '(ampere volt) 1.0)))
#t)
(check-expect (equal-QLs? (make-QL 4.3890407 '(meter) '() 0.09143834)
(norm-QL (make-QL 14.4 '(foot) '() 0.3)))
#t)
(check-expect (equal-QLs? (make-QL 0.010159816 '(meter) '(second) 0.0005079908)
(norm-QL (make-QL 2.0 '(foot) '(minute) 0.1)))
#t)
Note: Jane asked a great question about this... to give everyone
access to the question and answers, they're linked here.
(define (add QL1 QL2)
that takes in two quantity lists QL1 and
QL2, and returns their normalized
sum. It's a good idea to normalize first -- that way
you can detect a unit mismatch. If, after normalzing,
the quantity lists' units do not match, return
(list 'error 'add QL1 QL2)
If they can be added, you'll need to use the
error-propagation formula:
(check-expect (equal-QLs? (make-QL 2.13356145 '(meter) '() 0.03592037)
(add (make-QL 42.0 '(inch) '() 1.0)
(make-QL 42.0 '(inch) '() 1.0)))
#t)
(check-expect (equal? (add (make-QL 42.0 '(inch) '() 1.0)
(make-QL 42.0 '(s) '() 1.0))
(list 'error 'add
(make-QL 42.0 '(inch) '() 1.0)
(make-QL 42.0 '(s) '() 1.0)))
#t)
(define (subtract QL1 QL2)
that takes in two quantity lists QL1 and
QL2, and returns their normalized
difference: QL1 - QL2.
This will be almost identical to add,
except that if, after normalzing,
the quantity lists' units do not match, return
(list 'error 'subtract QL1 QL2)
If they can be subtracted, the
error-propagation formula is the same as for addition.
Here are two examples:
(check-expect (equal-QLs? (make-QL 0.0 '(meter) '() 0.03592037)
(subtract (make-QL 42.0 '(inch) '() 1.0)
(make-QL 42.0 '(inch) '() 1.0)))
#t)
(check-expect (equal? (subtract (make-QL 42.0 '(inch) '() 1.0)
(make-QL 42.0 '(s) '() 1.0))
(list 'error 'subtract
(make-QL 42.0 '(inch) '() 1.0)
(make-QL 42.0 '(s) '() 1.0)))
#t)
Note that subtract will be largely copied from add - this is completely OK!
Where's convert?
With this toolkit of functions in place,
unit-conversion turns out to be an application of division:
Feel free to include this and try it out!
(define (convert from to)
(norm-QL (divide from to)))
More tests... Here are
a few more tests you might try. These combine your unit-calculation
functions into composite operations:
(check-expect (equal-QLs? (make-QL 0.38735983 '(second) '() 0.030581039)
(divide (norm-QL (make-QL 3.8 '(m) '(s) 0.3))
(norm-QL (make-QL 9.81 '(m) '(s s) 0.0))))
#t)
(check-expect (equal-QLs? (norm-QL (make-QL 0.48 '(cm) '() 0.6327716))
(let* ((w (make-QL 4.52 '(cm) '() 0.02))
(x (make-QL 2.0 '(cm) '() 0.2))
(y (make-QL 3.0 '(cm) '() 0.6)))
(subtract (add (norm-QL x) (norm-QL y)) (norm-QL w))))
#t)
(check-expect (equal-QLs? (norm-QL (make-QL 18.04 '(cm cm) '() 3.7119827))
(let* ((w (make-QL 4.52 '(cm) '() 0.02))
(x (make-QL 2.0 '(cm) '() 0.2))
(y (make-QL 3.0 '(cm) '() 0.6)))
(norm-QL (add (multiply w x) (power y 2)))))
#t)
(check-expect (equal-QLs? (make-QL 5.1 '(meter) '() 0.360555127)
(subtract (norm-QL (make-QL 14.4 '(meter) '() 0.3))
(norm-QL (make-QL 9.3 '(meter) '() 0.2))))
#t)
(check-expect (equal-QLs? (make-QL 23.7 '(meter) '() 0.360555127)
(add (norm-QL (make-QL 14.4 '(meter) '() 0.3))
(norm-QL (make-QL 9.3 '(meter) '() 0.2))))
#t)
Congratulations! on creating an error-propagating unit calculator! You will extend this prototype implementation into a full-fledged language in the next assignment... (in Java!)
Submitting Be sure to submit unicalc.rkt to the submission site; you do not need to submit unicalc-db.rkt.
Problems 2 and 3 use Java and its unit-testing framework, JUnit. Remember that problem 2, Complex.java is an individual problem and problem 3, OpenList.java can be done in a pair or individually.
Remember, too, that you're welcome to use any Integrated Development Environment, these instructions will focus on Dr. Java, which has a relatively shallow learning curve. Either way, you'll be submitting only your plain-text source code (the .java files).
Because Java has a so strong an industry foothold as a development language, there are many tools available to help test Java code. We will use perhaps the most ubiquitous unit-testing framework, named JUnit. Fortunately, Dr. Java makes using JUnit quite straightforward: the basic framework is included with the IDE.
This week we provide files of unit tests named ComplexTests.java (for problem 2) and OpenListTests.java (for problem 3). The goal for your Complex class will be a correct implementation for a complex-number class that satisfies the provided tets: you won't need to add more tests there. For OpenList, you will add more tests of your own.
Here is what a JUnit test looks like - although its syntax is Java, it's
very much in the same spirit as Racket's check-expect. This
one is from ComplexTests.java:
/**
* test of equals
*/
public void test_equals()
{
Complex actual = new Complex(42.0,60.0); // 42 + 60i
Complex expected = new Complex(2*21.0,2*30.0); // also 42 + 60i
boolean complexesEqual = actual.equals( expected ); // check
boolean exp = true; // expectation
// this next line is JUnit's test, with our own String annotation:
assertEquals( "Complex equals", exp, complexesEqual );
}
Commenting out tests... One way to test
your code one function at a time is to comment out all of the
tests you don't want to compile or run yet. Java uses slash-star:
and star-slash:
/*
in order to comment multiple lines at once. So, for example we could
comment out the entirety of test_equals, above, as follows:
*/
Note that the function-header comment is an example of a multiple-line comment, too.
/**
* test of equals
*/
/*
public void test_equals()
{
Complex actual = new Complex(42.0,60.0); // 42 + 60i
Complex expected = new Complex(2*21.0,2*30.0); // also 42 + 60i
boolean complexesEqual = actual.equals( expected ); // check
boolean exp = true; // expectation
// this next line is JUnit's test, with our own String annotation:
assertEquals( "Complex equals", exp, complexesEqual );
}
*/
Dr. Java has support for JUnit testing: open both the Complex.java file and the ComplexTests.java file. Then hit the compile button (or F5) followed by the test button (or control-t). The testing tab will report on which tests passed and which did not.
Java has eight built-in (primitive) types -- the most common are
such as int, char, boolean,
and double. It has thousands of class types in
its libraries.
Yet one class type that Java does not have is
Complex, representing complex numbers commonly used by
mathematicians, scientists, engineers, and Math 11 students.
This problem asks you to implement a complex number data structure (class).
Complex.java. You should start with the two provided files:
Complex.java and
ComplexTests.java
The Point class example we developed in class will be a good reference. After all, 2d points and Complex numbers are not so different... . The complete Point class is available here in Point.java and here in .txt format.
Complex number notes Recall that a complex number is of the form a + bi where a and b are real numbers and i is an imaginary number. Addition of two complex numbers is defined by (a + bi) + (c + di) = (a+c) + (b+d)i. Similarly, to multiply two complex numbers, we simply compute (a + bi) * (c + di) = ac + adi + bci + bdii = (ac - bd) + (ad + bc) i since ii (i times i) is -1 by definition.
In your implementation, the numbers a and b in the complex number a + bi should be represented by doubles: these will be the two data members of your class. Please use the names real and imag (as already appear in the starter file) for these data members -- this will be far more readable/memorable than a and b!
Below are the public methods (functions) which your Complex class should contain. Be sure to use exactly these names and the specified kinds of arguments and return values. You may use any additional private methods of your own design, though such helper functions are not required.
public Complex( double real_in, double imag_in )
public Complex( )
// method: toString
// in: no inputs
// out: the String version of this Complex object
public String toString()
{
String sign = "+"; // default sign is plus
if (this.imag < 0.0)
sign = "-"; //
return "" + this.real + " " + sign + " " + Math.abs( this.imag ) + "i";
}
// method: equals
// in: any object o
// out: true if o is Complex and its contents
// match this's contents
public boolean equals(Object o)
{
if (o instanceof Complex)
{
Complex c = (Complex)o; // cast o to a Complex
// now we can access c's imaginary and real parts...
return c.imag == this.imag && c.real == this.real;
}
else
{
return false;
}
}
We won't use a non-static version.
public Complex add( Complex c )
public Complex negate( )
public Complex conjugate( )
public Complex multiply( Complex c )
To divide one complex number by another, first multiply both the numerator and denominator by the conjugate of the denominator. That ensures that the denominator is a real value! Then, you can simply divide both the real and imaginary components of the numerator by that value.
Here is the method signature:
public Complex divide( Complex c )
Be sure to test out your Complex class with the unit tests provided before submitting your Complex.java file. You don't need to submit ComplexTests.java.
This problem may be done individually or in a pair. If you work in a pair, follow pair programming practices throughout.
50 points
This problem is meant to introduce the implementation of data structures in Java. In addition, it is a chance to either introduce yourself (or review) what Java looks like and how java code is structured. If you haven't used Java before, you may need to review a bit about the language. In particular, there is a starting point for your OpenList.java and OpenListTests.java files on the top-level assignments page. Also, there are on-line references and tutorials available from the references page.
Write a java class named OpenList that implements an (open) linked list data structure. Since the goal of this problem is to transition from Racket/Scheme to Java, you will be implementing lists in the same way Racket does. In fact, this problem is, essentially, the first step in writing the Racket programming language in Java.
Open lists are characterized by not permitting modifications to existing data. As a result, users of OpenList can re-use lists many times without worrying about one part of their code changing a value that another part of their program might have needed. This means OpenLists do not permit side-effects: the changing of variables in-place.
Write your class in a file called OpenList.java. You should start with this the OpenList.java file. The main method of this OpenList.java contains a couple of informal tests (not JUnit tests). To begin, you might want to try compiling and running the file to start building intuition about how OpenLists work.
Tests Just as with your Racket functions, you should test each of your methods as you write it. A starting set of JUnit tests is provided in OpenListTests.java In addition to running those tests, you should add at least one more test for each method you write in your OpenList class. You don't need to add a test for each of the {static, nonstatic} versions of each method, just one for each distinct name of a method you write.
There are three data members we will use
in this OpenList. Here is how the class
starts (this is in the provided OpenList.java
file:
Notice that because the data member named first is of type Object, this OpenList implementation will be able to create lists of any Java Objects. Everything in Java is an Object except the types int, double, float, boolean, byte, char, and long. We will use only Strings and OpenLists for this assignment. The next assignment, on the other hand, will put your OpenList class to work handling more types.
In Java and other object-oriented languages, functions that are members of a class are often called methods.
So, in your OpenList class, implement the following methods. Note that both static and nonstatic versions of the methods are required in many cases. Feel free to implement one of the pair and then implement the other based on it.
Several methods are already provided in the starter file - please keep those!
This toString method is a nonstatic method that returns a String representation of the invoking list. A static version isn't needed. The toString method is used internally by Java whenever your code adds a String to an OpenList, e.g., in code like this:
OpenList L = OpenList.list("Hello","World");
System.out.println("List L is " + L);
This code should output
List L is ["Hello", "World"]You will also use this method quite a bit when writing your unit tests, so you can assume it's working correctly.
String m1 = "mudd";Basically, if compareTo is less than zero, then this is before the input argument in the dictionary.
String m2 = "mit";
if ( m2.compareTo(m1) < 0 ) ... // this test will evaluate to true
String firstOfL = (String)L.first(); // we cast the Object returned by first()Thus, you'll want to use lines like these two in order to grab the two strings you'll need to implement the merge function.
String firstOfM = (String)M.first(); // into the String we know it to be...
Object s = new String("I'm a string.");
if ( s instanceof String ) ... // this test will evaluate to true
if ( s instanceof OpenList ) ... // this test will evaluate to false
No static version is required. Remarkably, Java's compiler checks if an
instanceof expression can possibly
be true - the code won't compile if it can't! In this case, s
is of type Object, which could be
a String or a OpenList.
(The compiler doesn't actually read the code, it
just checks the types....)
Testing Again, remember that you need at least one test for each of your functions in your OpenListTests.java file!
Submitting Be sure to submit both your OpenList.java file and your OpenListTests.java tests to the submission site.