diff --git a/.gitignore b/.gitignore index 5a7890f..25e86e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ - # Created by https://www.toptal.com/developers/gitignore/api/intellij+all # Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all @@ -81,10 +80,12 @@ fabric.properties # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 .idea/ +*.iml +/.idea # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 -*.iml + modules.xml .idea/misc.xml *.ipr @@ -93,6 +94,7 @@ modules.xml .idea/sonarlint # Build output -build/ +!added/to/make/code/build/ +/build/ # End of https://www.toptal.com/developers/gitignore/api/intellij+all diff --git a/README.md b/README.md index 7f679dd..2beb7be 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,31 @@ -# CleanCode -A sample project to work with clean code concepts +# Clean Code, A bottom-up approach +Comprehensive notes for the clean code concepts + +code quality measurement + +## Table of content +- [Introduction*](notes/00_Introduction.md) +- [Chapter 1: Clean code*](notes/01_Chapter_1_Clean_code.md) +- [Chapter 2: Meaningful names*](notes/02_Chapter_2_Meaningful_names.md) +- [Chapter 3: Functions*](notes/03_Chapter_3_Functions.md) +- [Chapter 4: Comments*](notes/04_Chapter_4_Comments.md) +- [Chapter 5: Formatting*](notes/05_Chapter_5_Formatting.md) +- [Chapter 6: Objects and Data structures*](notes/06_Chapter_6_Objects_and_Data_structures.md) +- [Chapter 7: Error handling*](notes/07_Chapter_7_Error_Handling.md) +- [Chapter 8: Boundaries*](notes/08_Chapter_8_Boundaries.md) +- [Chapter 9: Unit tests](notes/09_Chapter_9_Unit_tests.md) +- [Chapter 10: Classes](notes/10_Chapter_10_Classes.md) +- [Chapter 11: Systems](notes/11_Chapter_11_Systems.md) +- [Chapter 12: Emergence](notes/12_Chapter_12_Emergence.md) +- [Chapter 13: Concurrency](notes/13_Chapter_13_Concurrency.md) +- [Chapter 14: Successive Refinement](notes/14_Chapter_14_Successive_Refinement.md) +- [Chapter 15: Junit Internals](notes/15_Chapter_15_Junit_Internals.md) +- [Chapter 16: Refactoring SerialDate](notes/16_Chapter_16_Refactoring_SerialDate.md) +- [Chapter 17: Smells and Heuristics](notes/17_Chapter_17_Smells_and_Heuristics.md) +- [Appendix A: Concurrency ii](notes/18_Appendix_A_Concurrency_ii.md) + +(*) These chapters also have ppt presentation slides + +## Source code examples +- [Snippets](src/main/java/clean/code) +- [Tests](src/test/java) diff --git a/build.gradle b/build.gradle index 7e6c62a..ac9f5b8 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java' } -group 'ir.logicbase.cleancode' +group 'clean.code' version '1.0' repositories { diff --git a/img/book_cover.jpg b/img/book_cover.jpg new file mode 100644 index 0000000..2cb5b92 Binary files /dev/null and b/img/book_cover.jpg differ diff --git a/img/code_quality_measurement.png b/img/code_quality_measurement.png new file mode 100644 index 0000000..383961b Binary files /dev/null and b/img/code_quality_measurement.png differ diff --git a/notes/00_Introduction.md b/notes/00_Introduction.md new file mode 100644 index 0000000..2f42e8d --- /dev/null +++ b/notes/00_Introduction.md @@ -0,0 +1,11 @@ +# Introduction [[Slide](../slides/00_Introduction.pptx)] + +## How to know if code is dirty + +When reading someones code, if you find a portion of code ambigious +you say WTF is this (Use surprise word for public, it's more polite) +how am I supposed to know what's going on here + +**Code quality = WTFs/Minute** + +code quality measurement \ No newline at end of file diff --git a/notes/01_Chapter_1_Clean_code.md b/notes/01_Chapter_1_Clean_code.md new file mode 100644 index 0000000..d35de4f --- /dev/null +++ b/notes/01_Chapter_1_Clean_code.md @@ -0,0 +1,87 @@ +# Chapter 1: Clean code [[Slide](../slides/01_Chapter_1_Clean_code.pptx)] + +## Bad code +we write bad code because: +- trying to go fast and reach irrational deadline +- you're in **rush** +- you feel that you don't have **time** to do a **good job** +- you're boss will be angry if you took the time to clean up your code +- you were just tired of working on this program and wanted it to be over +- you looked at the **backlog** of other stuff that you had promised to get done and realized that you needed to slam this module together so you could move on to the next. + +## Costs of bad code +As the mess piles on top of each other, **productivity** decreases toward zero + +### The Grand Redesign in the Sky + +1. new tiger team selected to redesign system **from scratch** +2. current system must be **maintained** until new one is ready +3. two teams are in race, tiger team must build everything that old system does meanwhile keeping up with changes that are continuously being made to old system +4. it takes long time for both teams to reach focal point +5. when its done, members demand new system to be redesigned because it's such a mess + +### Attitude +- If good code rot so quickly into bad code, it's all **our fault**, we are **unprofessional** +- The managers and marketers look to us for the information they need to make **promises** and **commitments** +- It is unprofessional for programmers to bend to the will of managers who don’t understand the **risks of making messes**. + +### The Primal Conundrum +- You will not make the **deadline** by making the mess. +- Indeed, the mess will slow you down instantly, and will force you to miss the deadline. +- The only way to make the deadline—the only way to go fast—is to `keep the code as clean as possible at all times`. + +## The Art of Clean Code? +- being able to recognize clean code from dirty code does not mean that we know how to write clean code +- A programmer with **code-sense** will look at a messy module and see options and variations + +## What Is Clean Code? +### Bjarne Stroustrup, inventor of C++ +- clean code is pleasing to read. +- Bad code tempts the mess to grow! When others change bad code, they tend to make it worse. + +### Pragmatic Dave Thomas +A building with broken windows looks like nobody cares about it. So other people stop caring. + +### Grady Booch, author of Object Oriented Analysis and Design with Applications +- our code should contain only what is **necessary**. +- clean code should read like well-written prose (can you see images, sounds, humors, … when reading a novel?) + +### “Big” Dave Thomas, founder of OTI, godfather of theEclipse strategy +- clean code makes it easy for **other people** to enhance it +- smaller codebase the better + +### Michael Feathers, author of Working Effectively with Legacy Code +- Clean code always looks like it was written by someone who cares + +### Ron Jeffries, author of Extreme Programming Installed and Extreme ProgrammingAdventures in C# +- Duplicate code is a sign there is an idea in our mind that is not well represented in the code + +### Ward Cunningham, inventor of Wiki +- Clean code is a code that is pretty much what you expect it to do +- Beautiful code makes the language look like it was made for the problem. + +## Schools of Thought +- Often master martial artists will form their own schools of thought and gather students to learn from them +- These recommendations are controversial, you might agree or disagree our point of view + +## We Are Authors +- authors have readers +- the ratio of time spent reading vs. writing is well over 10:1. +- if you want your code to be easy to write, make it easy to read. (you read surrounding code all the times) + +## The Boy Scout Rule +`Leave the campground cleaner than you found it` +Examples: +- Change one variable name for the better +- break up one function that’s a little too large +- eliminate one small bit of duplication +- clean up one composite if statement + +## Prequel and Principles +This book is prequel to the book : +Agile Software Development: Principles, Patterns, and Practices (PPP) +by uncle bob 2002 + +## Conclusion +Books on art don’t promise to make you an artist. All they can do is give you some of the +tools, techniques, and thought processes that other artists have used. \ No newline at end of file diff --git a/notes/02_Chapter_2_Meaningful_names.md b/notes/02_Chapter_2_Meaningful_names.md new file mode 100644 index 0000000..69f2d2d --- /dev/null +++ b/notes/02_Chapter_2_Meaningful_names.md @@ -0,0 +1,72 @@ +# Chapter 2: Meaningful names [[Slide](../slides/02_Chapter_2_Meaningful_names.pptx)] + +## Use Intention-Revealing Names +Using `d` instead of `elapsedTimeInDays`. + +## Avoid Disinformation +- Using names like `hp`, `aix` where it has different meaning in different contexts. +- Using `AccountList` for group of accounts where there is no list + +## Make Meaningful Distinctions +- Using `a1` and `a2` instead of `source` and `destination`. +- what's the difference between `TheCustomer`, `CustomerData`, `CustomerInfo` and `Customer` + +## Use Pronounceable Names +Don't compact names for sake of smaller words, eg. `pref`, `mnger`, `genymdhms`. + +## Use Searchable Names +Using constants and complete names make it easy to find them with search, eg. `WORK_DAYS_PER_WEEK` + +## Avoid Encodings +### Hungarian Notation +In the modern IDE's there is no need to prefix with HN, eg. `frmReserve`, `tblStudent`. + +### Member Prefixes +Don't use stupid m prefix for field names, eg. `mLatitude`, `mContext` + +### Interfaces and Implementations +Prefer to suffix Implementation class, rather than Interface class +eg. `ShapeFactory` for the interface and `ShapeFactoryImpl` for the implementation. + +## Avoid Mental Mapping +Readers shouldn’t have to mentally translate your names into other names they already know. + +## Class Names +noun or noun phrases + +## Method Names +- methods should have verb or phrase names + eg. `deletePage()`, `save()`, `isEven()`, `setPoint()`, `hasElement()` +- prefer factory methods for multiple constructors + +## Don’t Be Cute +Using `whack()` instead of `kill()`, `eatMyShorts()` instead of `abort()`, `nukeTable()` instead of `deleteAll()` + +## Pick One Word per Concept +eg. `fetch`, `retrieve` and `get` + +## Don’t Pun +Avoid using the same word for two purposes. Using the same term for two different ideas +is essentially a pun. +eg. using `add` instead of `insert` for a method which is putting single parameter to a collection is a pun + +## Use Solution Domain Names +use computer science (CS) terms, algorithm names, pattern names, math terms + +## Use Problem Domain Names +When there is no “programmer-eese” for what you’re doing, use the name from the problem +domain. + +## Add Meaningful Context +Separate your concerns using well-named : +- Constant +- Function +- Class +- Namespace +- Package +- Module + +When all else fails, then prefixing the name may be necessary as a last resort. + +## Don’t Add Gratuitous Context +eg. `GSDAccountAddress` diff --git a/notes/03_Chapter_3_Functions.md b/notes/03_Chapter_3_Functions.md new file mode 100644 index 0000000..3ba73eb --- /dev/null +++ b/notes/03_Chapter_3_Functions.md @@ -0,0 +1,126 @@ +# Chapter 3: Functions [[Slide](../slides/03_Chapter_3_Functions.pptx)] + +## Small +- **Line counts** should not be longer than a **screen size** +- **Line width** smaller than `150` characters +- **Line counts** smaller than `100` lines + +## Blocks and Indenting +- blocks within conditional (if/when/while...) statements should be **one line long** +- 1 or 2 indentation level at most + +## Do One Thing +`FUNCTIONS SHOULD DO ONE THING. THEY SHOULD DO IT WELL. +THEY SHOULD DO IT ONLY.` +- another way to know that a function is doing more than **one thing** is if you can + extract another function from it with a name that is not merely a restatement of its implementation + +## Sections within Functions +Functions that do one thing cannot be reasonably divided into sections. + +## One Level of Abstraction per Function +Mixing level's of abstraction make's function confusing + +## Reading Code from Top to Bottom: The Stepdown Rule +We want the code to read like a top-down narrative. +We want every function to be followed by those at the next level of abstraction so that we can read the program, descending one level of abstraction at a time as we read down the list of functions + +## Switch Statements +they can be tolerated if they appear only once, are used to create polymorphic objects, and are hidden behind an inheritance relationship so that the rest of the system can’t see them. + +## Use Descriptive Names +- Don’t be afraid to make a name long. A long descriptive name is better than a short + enigmatic name +- A long descriptive name is better than a long descriptive comment +- Don't be afraid to change names + +## Function Arguments +- The ideal number of arguments for a function is zero (*niadic*). +- Next comes one (*monadic*), followed closely by two (*dyadic*). +- Three arguments (*triadic*) should be avoided where possible. +- More than three (*polyadic*) requires very special justification—and then shouldn’t be used anyway. + +## Common Monadic Forms +- condition arg to decide what to do +- argument to transform into something else +- event + +`Using an output argument instead of a return value for a transformation is confusing` + +## Flag Arguments +should be avoided, it is loudly proclaiming that this function does more than one thing + +## Dyadic Functions +you can simply convert these functions to monadic by moving it to right class as a member function + +## Triads +same applies to this + +## Argument Objects +using objects for group of variables that declare a concept is better than passing every single one of variables + +## Argument Lists +eg. `varargs` +functions with these arguments are simply monadic, dyadic and traids +so same rules applies to them + +## Verbs and Keywords +functions and arguments should form a very nice verb/noun pairs eg. `write(name)` +we can use keywords as well to make them more clear eg. +```kotlin +firstOrNull(predicate) +assertExpectedEqualsActual(expected, actual) +``` + +## Have No Side Effects +### temporal coupling +Temporal coupling is coupling that occurs when there are two or more members of a class that need to be invoked in a particular order. eg: +``` +var calculator = new PriceCalculator(); +calculator.UpdateCurrencyRates(eur: 1.02, gbp: 1.25); +decimal price = calculator.CalculatePrice(myShoppingCart); +``` +[Reference](https://enterprisecraftsmanship.com/posts/temporal-coupling-and-immutability) + +Here, currency rates required to calculate the price of a shopping cart. + +## Output Arguments +should be avoided + +## Command Query Separation +``` +public boolean set(String attribute, String value); +if (set("username", "unclebob"))… +if (attributeExists("username")) { + setAttribute("username", "unclebob"); + ... +} +``` + +## Prefer Exceptions to Returning Error Codes +returning **error code*** will result in **nested conditional** checks + +## Extract Try/Catch Blocks +extract each block to another function + +## Error Handling Is One Thing +a function that handles errors should do nothing else + +## The Error.java Dependency Magnet +having one enum class for all error codes will force compiler to rebuild every time there is a change in this enum, because all other parts of code have imported this +## Don’t Repeat Yourself +Duplication may be the root of all evil in software + +## Structured Programming +### Dijkstra's rule for functions +`every function, and every block within a function, should have one entry and one exit.` + +if you keep your functions small, then the occasional multiple return, break, or +continue statement does no harm and can sometimes even be more expressive than the single- +entry, single-exit rule. + +## How Do You Write Functions Like This? +you cannot reach clean code at the first try! +when you start writing code, just jot down your thoughts into any clumsy code that pop's up into your mind, duplicate code, complicated loops, it's all O.K +then you start iterating through your code and refine them to smaller functions and clean design +while keeping unit tests passing. diff --git a/notes/04_Chapter_4_Comments.md b/notes/04_Chapter_4_Comments.md new file mode 100644 index 0000000..6d01d47 --- /dev/null +++ b/notes/04_Chapter_4_Comments.md @@ -0,0 +1,106 @@ +# Chapter 4: Comments [[Slide](../slides/04_Chapter_4_Comments.pptx)] + +`Don’t comment bad code—rewrite it". Brian W. Kernighan` + +## Comments Do Not Make Up for Bad Code +Clear and expressive code with few comments is far superior to cluttered and complex +code with lots of comments + +## Explain Yourself in Code +prefer using separate function instead of commenting on if statement with multiple conditions + +## Good Comments +### Legal Comments +copyright + +### Informative Comments +- function return intention +- comments to show eligible format patterns + +### Explanation of Intent +important decisions that can be questionable by other programmers + +### Clarification +when you don't have access to api and, you are just consumer of it and there need to be some clarification for it + +### Warning of Consequences +eg. SimpleDateFormat is not thread safe, so we need to create each instance independently. + +Don't run unless you have some time to kill. + +### ToDo +It's okay to put todo comments when you don't have a good solution at the moment + +### Amplification +indicate higher importance of notice + +### Javadocs in Public APIs +public api's must have java docs + +## Bad Comments +### Mumbling +eg. putting in comments for **catch** in try/catch blocks, just to satisfy IDE inspector. + +### Redundant Comments +Don't repeat code in comment, add something that has more value and meaning + +### Misleading Comments +comment should describe code accurately, otherwise it will mislead other programmers + +### Mandated Comments +Not all functions and variables should have java doc, it will just clutter up the code + +### Journal Comments +log of changes to code should be maintained in **version control** system, not in code + +### Noise Comments +commenting something that we **already know** about code is called noise + +eg. // this is default constructor + +### Scary Noise +**copy pasting** variable name in comment is a horrible scray noise! + +### Don’t Use a Comment When You Can Use a Function or a Variable +eg. section comments, if statement comments + +### Position Markers +eg. // Actions ////////////////////////////////// + +### Closing Brace Comments +separate your code into functions instead of commenting closing braces +eg. +``` +for(~) { + while(~) { + if() { + } // if + } // while +} // for +``` + +### Attributions and Bylines +use version control instead of adding author name at top of classes + +### Commented-Out Code +leaving commented out code is not a good practice anymore because we have VCS now. + +### HTML Comments +If you want to put HTML docs, make sure your tool is displaying human readable format + +### Nonlocal Information +referencing global configurations and information in a local function will quickly turn it into a **lie** when those information changes + +### Too Much Information +Don’t put interesting **historical discussions** or irrelevant descriptions of details into your comments. + +### Inobvious Connection +- The connection between a comment and the code it describes should be obvious +- The purpose of a comment is to explain code that does not explain itself. It is a **pity** when a comment needs its own explanation. + +### Function Headers +Short functions don’t need much description. + +### Javadocs in Nonpublic Code +nonpublic code doesn't need javadocs for all single parameters + diff --git a/notes/05_Chapter_5_Formatting.md b/notes/05_Chapter_5_Formatting.md new file mode 100644 index 0000000..f79a7a3 --- /dev/null +++ b/notes/05_Chapter_5_Formatting.md @@ -0,0 +1,57 @@ +# Chapter 5: Formatting [[Slide](../slides/05_Chapter_5_Formatting.pptx)] + +Formatting your code show's your professional attitude and care to details + +## The Purpose of Formatting +- The functionality that you create today has a good chance of changing in the next release, but the **readability** of your code will have a profound effect on all the changes that will ever be made +- Your style and discipline survives, even though your code does not + +## Vertical Formatting +Small files are usually easier to understand than large files are. + +## The Newspaper Metaphor +The topmost parts of the source file should provide the high-level concepts and algorithms. Detail should increase as we move downward, until at the end we find the lowest level functions and details in the source file + +## Vertical Openness Between Concepts +Use blank lines to separate different concepts in your code. Eg. Properties, functions, companion object, … + +## Vertical density +Lines of code that are tightly related should appear vertically dense. Eg. Related properties like two way data bindings + +## Vertical distance +Concepts that are closely related should be kept vertically close to each other. + +### Variable Declarations +Variables should be declared close to their usage. + +### Instance variables +Put all instance variables of a class at the top of class. + +### Dependent Functions +Functions that are dependent on each other should be close Caller above the callee. + +### Conceptual affinity +Certain bits of code want to be near other bits. + +## Vertical Ordering +Source code dependencies must be downward, details goes down. +Just like newspaper that we see most important subjects abstracted out at top of page. + +## Horizontal Formatting +Use 120 characters for line width in your IDE for better readability + +### Horizontal openness and density +Put white space between separate concepts, eg. Function arguments, math statements + +### Horizontal alignment +There is no need to use horizontal alignment for statements, because code automatic reformating will change it + +## Indentation +Every scope of code structure, eg. File, class, method, statement block, … is indented one level to the right. +Don't break indentation rule in single line lambda or anonymous blocks, I know this is tempting for make code smaller, but it makes it less readable instead. + +### Dummy scopes +In one line statements like `while` with captured resource loop use indentation and braces to make it more visible + +## Team rules +When you write code in a team, agree upon a coding style and follow team rules for it diff --git a/notes/06_Chapter_6_Objects_and_Data_structures.md b/notes/06_Chapter_6_Objects_and_Data_structures.md new file mode 100644 index 0000000..05bbd1b --- /dev/null +++ b/notes/06_Chapter_6_Objects_and_Data_structures.md @@ -0,0 +1,52 @@ +# Chapter 6: Objects and Data structures [[Slide](../slides/06_Chapter_6_Objects_and_Data_structures.pptx)] + +## Data abstraction +- We keep our variables private to prevent unintended dependency on them, so that we can change implementation. +- If you expose your implementation detail to user, the chance of mistake and bug will raise + +## Data/Object Anti-Symmetry +- `Procedural code is easy to add new functions without changing existing data structures + Object Oriented code is easy to add new classes without changing existing functions` +- Procedural code is hard to add new data structures + OO code is hard to add new functions (because all classes must change) +- Procedural code is complementory of OO code + +## The Law of Demeter +`a module should not know about the innards of the objects it manipulates` + +Below code is violating the law of demeter: +```java +final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath(); +``` + +### Train wrecks +Calling multiple functions after returning object, one after another is called Train Wrecks. + +This would violate the law of demeter. + +If above code was returning only data structure instead of object, it wouldn't violate the law of demeter. +```java +final String outputDir = ctxt.options.scratchDir.absolutePath; +``` + +### Hybrids +having functions, mutators, accessors for public and private variables makes a Hybrid code. + +It will makes it hard to add new functions and data structures. + +### Hiding Structure +result of law of demeter is: +``` +BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName); +``` + +## Data Transfer Objects +## DTO +a data structure with public variables and no functions. + +### Bean +private variables manipulated by getters and setters. + +### Active Record +Special form of DTO. data structure with public (or bean-accessed) variables with extra navigational methods like save and find. (example: Model class in Laravel) +- Putting business rules into these classes makes it a Hybrid structure and hard to change. diff --git a/notes/07_Chapter_7_Error_Handling.md b/notes/07_Chapter_7_Error_Handling.md new file mode 100644 index 0000000..d02479e --- /dev/null +++ b/notes/07_Chapter_7_Error_Handling.md @@ -0,0 +1,35 @@ +# Chapter 7: Error Handling [[Slide](../slides/07_Chapter_7_Error_Handling.pptx)] + +Error handling is important, but if it obscures logic, it’s wrong. + +## Use Exceptions Rather Than Return Codes +Using exception for error handling helps separate business logic from error code + +## Write Your Try-Catch-Finally Statement First +Your catch has to leave your program in a consistent state, no matter what happens in the try. + +So writing try-catch statement first ensures your code is not jeopardizing consistency, and it will make structure of code simpler. + +## Use Unchecked Exceptions +Checked exceptions force any change in low level code to affect higher levels. + +This will clearly violate Open/Closed principle. + +## Provide Context with Exceptions +write a good and self-explanatory message with enough context to be shown in exception stack trace console + +## Define Exception Classes in Terms of a Caller’s Needs +- Use wrapper class to compound different exceptions thrown from a function into a common exception type we care about +- Use different classes only if there are times when you want to catch one exception and allow the other one to pass through. + +## Define the Normal Flow +Encapsulate **special cases** inside your code so that the caller don't have to deal with default values when there is an exception + +## Don’t Return Null +Instead of returning null you can return a special object like **Failure** +or if you're returning a list of object you can simple return an **empty list** +if you really need to return null, mention it in your function name like: getFirstOrNull() + +## Don’t Pass Null +- avoid passing null when your argument does not support null +- If you really want to pass null at least use a **nullable** type or **annotation** or any other proper means to declare your parameter as nullable diff --git a/notes/08_Chapter_8_Boundaries.md b/notes/08_Chapter_8_Boundaries.md new file mode 100644 index 0000000..08dede5 --- /dev/null +++ b/notes/08_Chapter_8_Boundaries.md @@ -0,0 +1,24 @@ +# Chapter 8: Boundaries [[Slide](../slides/08_Chapter_8_Boundaries.pptx)] + +We seldom control all the software in our systems. Sometimes we buy third-party packages +or use open source. Other times we depend on teams in our own company to produce +components or subsystems for us. + +## Using Third-Party Code +- Interface providers strive for broad applicability, while interface users want it to be focused on their needs +- Don’t pass map outside of system (eg. Public API) boundaries + +## Exploring and learning boundaries +Learning test: +We call third-party API, as we expect to use it in our application + +## Learning log4j +We write test to see how it works + +## Learning tests are better than free +Learning tests helps us to find boundary tests and ease our migrations if there is any breaking change in new releases + +## Clean boundaries +Change is inevitable in software + +Code at the boundaries needs clear separation and tests that define expectations diff --git a/notes/09_Chapter_9_Unit_tests.md b/notes/09_Chapter_9_Unit_tests.md new file mode 100644 index 0000000..77d5125 --- /dev/null +++ b/notes/09_Chapter_9_Unit_tests.md @@ -0,0 +1,48 @@ +# Chapter 9: Unit tests + +## The three laws of TDD +## First law +You may not write production code until you have written a failing unit test. + +### Second law +You may not write more of a unit test than is sufficient to fail, and not compliling is failing. + +### Third law +You may not write more production code than is sufficient to pass the currently failing test. + +## Keeping Tests Clean +- Tests must change as the production code evolves. The dirtier the tests, the harder they are to change. + +This section is important (all of it) + +## Tests enable the -ilities +- If you have tests, you do not fear making changes to the code! +- Tests enable all the -ilities because it enables CHANGE + +## Clean Tests +Readability makes a test clean. +clarity, simplicity, and density of expression makes test readable. +Use BUILD-OPERATE-CHECK pattern for your tests. + +## Domain-Specific Testing Language +Building functions and utilities become a specialized API used by the tests. + +## A Dual Standard +There are things that you might never do in a production environment that are perfectly fine in a test environment. Usually they involve issues of memory or CPU efficiency. But they never involve issues of cleanliness + +## One Assert per Test +the number of asserts in a test ought to be minimized (ideally one assert per test). + +## Single Concept per Test +the best rule is that you should minimize the number of asserts per concept and test just one concept per test function. + +## F.I.R.S.T rule +**Fast**: Tests should be fast, so that we could run them frequently. + +**Independent**: Tests should not depend on each other. + +**Repeatable**: Tests should be repeatable in any environment. + +**Self-Validating**: The tests should have a boolean output. Either they pass or fail. + +**Timely**: Unit tests should be written just before the production code that makes them pass. diff --git a/notes/10_Chapter_10_Classes.md b/notes/10_Chapter_10_Classes.md new file mode 100644 index 0000000..f918723 --- /dev/null +++ b/notes/10_Chapter_10_Classes.md @@ -0,0 +1,33 @@ +# Chapter 10: Classes + +## Class organization +Put most important parts of code at top of class, eg. Public properties and functions + +## Encapsulation +Keep variables and utility functions, private. Unless you need it in tests or other good reason. + +## Classes should be small! +With functions we measured size by counting physical lines, with classes we use a different measure, we count responsibilities. + +## The single responsibility principle +- A class or module should have one and only one reason to change +- We can reuse codes with **SRP** priciple +- Programmers have fear of breaking problems into too many small parts because they are afraid it would look complex +- Larger portions of code hampers us by insisting we wade through lots of stuff we don't need to know right now. + +## Cohesion +When Instance variables of a class are used in most of it's methods. We say this class has HIGH cohesion. +Separating related variables and methods into more classes makes them more cohesive. + +## Maintaining cohesion results in many small classes +- When classes lose cohesion, split them. +- Breaking a large function into many smaller functions often gives us the opportunity to split several smaller classes out as well. + +## Organizing for change +In clean system we organize our classes so as to reduce the risk of change. + +## Isolating from change +Needs will change, therefore code will change. +Instead of depending upon implementation details, create more abstractions and inject them with DIP (Dependency inversion principle) +Code must be les coupled and more cohesive. +Talk about **Cohesion** VS **Coupling** diff --git a/notes/11_Chapter_11_Systems.md b/notes/11_Chapter_11_Systems.md new file mode 100644 index 0000000..c72677a --- /dev/null +++ b/notes/11_Chapter_11_Systems.md @@ -0,0 +1,71 @@ +# Chapter 11: Systems + +`Complexity kills. It sucks the life out of developers, +it makes products difficult to plan, build, and test.` + +—Ray Ozzie, CTO, Microsoft Corporation + +## How would you build a city? +- cities have teams of people who manage particular parts of the city, the water systems, power systems, traffic, law enforcement, building codes, and so forth +- Although software teams are often organized like that too, the systems they work on + often don’t have the same separation of concerns and levels of abstraction. + +## Separate constructing a System from Using it +Software systems should separate the startup process, when the application objects are +constructed and the dependencies are “wired” together, from the runtime logic that takes +over after startup. +- Lazy initialization +- Separation of concerns +- Test double +- Mock object + +## Separation of Main +move all aspects of construction to main, or modules called by main, and to design the rest of the system assuming that all objects have been constructed and wired up appropriately + +## Factories +To create instances of models use ABSTRACT FACTORY pattern . In this case you can easily swap factories in your test. + +## Dependency injection +- Separating construction from use +- Move responsibility of construction to another authoritative mechanism + +### Lazy initialization +Don't create instance of object unless it's used + +## Scaling up +It is a myth that we can get systems "right the first time." + +We implement only today's stories. Then refactor and expand the system to implement new stories tomorrow. +This is the essence of iterative and incremental agility. + +TDD + Refactoring + Clean code + +## Cross-cutting concerns +Research about AOP (aspect-oriented programming) +Laravel environment might be using this approach + +## Java Proxies +Annotation processors might be using this approach, take a look at mockfit + +## Pure Java AOP Framework +- Android room library might be using this approach check it out +- Decorator pattern + +## AspectJ aspects +An extension language of Java to provide aspect programming + +## Test drive the system architecture +- It is not necessary to do a Big Design Up Front. You can start your architecture so simple to meet user stories and evolve gradually +- Even well-designed apis can be over-kill when they aren't really needed + +## Optimize decision making +- **Modularity** and **Separation of concerns** make decentralized management and decision making possible. +- You always have to make a choice, even if you’re not 100% sure if it’s the right one. It’s always better to take your best guess, and see how things work out, rather than spend endless hours debating one choice or another. (Analysis paralysis - Head first OOAD) + +## Use standards wisely, when they add demonstratble value +You might have different frameworks or standards to do a job. In web development eg. Laravel, Django, Flask, Ktor, Asp.net, … + +If one minimal framework suffices your customer needs, don't obsessed with big luxiory tools that will only delay delivery + +## Systems need domain-specific languages +Good DSL minimizes the "communication gap" between a domain concept and the code. Eg. Gradle, SQL, Html, … diff --git a/notes/12_Chapter_12_Emergence.md b/notes/12_Chapter_12_Emergence.md new file mode 100644 index 0000000..4f7256d --- /dev/null +++ b/notes/12_Chapter_12_Emergence.md @@ -0,0 +1,26 @@ +# Chapter 12: Emergence + +Kent Beck's four rules of Simple Design in XPE book +A design is simple if it follows these rules: +1. Runs all the tests +2. Contains no duplication +3. Expresses the intent of the programmer +4. Minimizes the number of classes and methods + +## Simple design rule 1: Runs all the tests +If there is no simple way to verify that the system actually works as intended, then all the paper effort on design is questionable. +Writing tests leads to better designs, because we will have to use dependency injection, interfaces, abstraction and more to make it simpler and minimize coupling. + +## Simple design rule 2-4: Refactoring +The fact that we have tests, eliminates the fear that cleaning up the code will break it. + +## No duplication +Duplication is the primary enemy of a well-designed system. +Understanding how to achieve reuse in the small is essential to achieving reuse in large +Template method pattern is a common technique for removing higher-level duplication + +## Expressive +It's easy to write code that we understand, because at the time we write it we're deep in an understanding of the problem we're trying to solve. + +## Minimal classes and methods +We must keep our functions and class counts low as well as making classes and methods small diff --git a/notes/13_Chapter_13_Concurrency.md b/notes/13_Chapter_13_Concurrency.md new file mode 100644 index 0000000..d68a237 --- /dev/null +++ b/notes/13_Chapter_13_Concurrency.md @@ -0,0 +1,112 @@ +# Chapter 13: Concurrency + +## Why Concurrency? +- Decoupling what gets done from when it gets done. +- Request constraints in web apps (eg. crawler, download manager, …) +- IO, UI, CPU threads +- Provide more efficient response time + +## Myths and Misconceptions +- Concurrency always improves performance. +- Design does not change when writing concurrent programs +- Understanding concurrency issues is not important when working with a container + such as a Web or EJB container. + +### Truth +- Concurrency incurs some overhead +- Correct concurrency is complex +- Concurrency bugs aren’t usually repeatable +- Concurrency often requires a fundamental change in design strategy + +## Challenges +**Race** condition + +## Concurrency Defense Principles +### Single Responsibility Principle +The SRP states that a given method/class/component should have a **single reason to change**. +Recommendation: Keep your concurrency-related code separate from other code. + +### Corollary: Limit the Scope of Data +Use **synchronized** keyword to protect shared objects. +Restrict the number of places using shared objects. +Recommendation: Take data encapsulation to heart; severely limit the access of any +data that may be shared. + +### Corollary: Use Copies of Data +Immutability matters + +if using copies of objects allows the code to avoid synchronizing, the savings in avoiding the intrinsic lock will likely make up for the additional creation and garbage collection overhead. + +### Corollary: Threads Should Be as Independent as Possible +Recommendation: Attempt to partition data into independent subsets than can be +operated on by independent threads, possibly in different processors. + +## Know Your Library +- Thread safety +- Executor framework +- Nonblocking + +## Know Your Execution Models +- Bound Resources +- Mutual Exclusion +- Starvation +- Deadlock +- Livelock + +### Producer-Consumer +`One or more producer threads create some work and place it in a buffer or queue. One or +more consumer threads acquire that work from the queue and complete it.` + +### Readers-Writers +When you have a shared resource that primarily serves as a source of information for readers, +but which is occasionally updated by writers + +### Dining Philosophers +Each philosopher eat with two forks, and put down both forks when finished. + +## Beware Dependencies Between Synchronized Methods +Avoid using more than one method on a shared object + +## Keep Synchronized Sections Small +extending synchronization beyond the minimal critical section increases +contention and degrades performance + +## Writing Correct Shut-Down Code Is Hard +Think about shut-down early and get it working early. + +## Testing Threaded Code +Write tests that have the potential to expose problems and then run them frequently + +### Treat Spurious Failures as Candidate Threading Issues +Do not ignore system failures as one-offs. + +### Get Your Nonthreaded Code Working First +Do not try to chase down nonthreading bugs and threading bugs +at the same time. Make sure your code works outside of threads. + +### Make Your Threaded Code Pluggable +Make your thread-based code especially pluggable so that you can run it in various configurations. + +### Make Your Threaded Code Tunable +Getting the right balance of threads typically requires trial an error. + +### Run with More Threads Than Processors +The more frequently your tasks swap, the more +likely you’ll encounter code that is missing a critical section or causes deadlock + +### Run on Different Platforms +Run your threaded code on all target platforms early and often. + +### Instrument Your Code to Try and Force Failures +only a very few pathways out of the many thousands of possible pathways through a vulnerable +section actually fail. + +## Code instrumentation: Hand-coded +There are many problems with this approach: +- You have to manually find appropriate places to do this. +- How do you know where to put the call and what kind of call to use? +- Leaving such code in a production environment unnecessarily slows the code down. +- It’s a shotgun approach. You may or may not find flaws. Indeed, the odds aren’t with you. + +## Code instrumentation: Automated +Use jiggling strategies to ferret out errors. diff --git a/notes/14_Chapter_14_Successive_Refinement.md b/notes/14_Chapter_14_Successive_Refinement.md new file mode 100644 index 0000000..52da100 --- /dev/null +++ b/notes/14_Chapter_14_Successive_Refinement.md @@ -0,0 +1,11 @@ +# Chapter 14: Successive Refinement + +This chapter is a case study in successive refinement. You will see a module that started +well but did not scale. Then you will see how the module was refactored and cleaned. + +## Notes +- To write clean code, you must first write dirty code and then clean it. +- One of the best ways to ruin a program is to make massive changes to its structure in the name of improvement +- using TDD, I am not allowed to make a change to the system that breaks that system +- It is not enough for code to work. Code that works is often badly broken. + Programmers who satisfy themselves with merely working code are behaving unprofessionally. diff --git a/notes/15_Chapter_15_Junit_Internals.md b/notes/15_Chapter_15_Junit_Internals.md new file mode 100644 index 0000000..8d00e35 --- /dev/null +++ b/notes/15_Chapter_15_Junit_Internals.md @@ -0,0 +1,3 @@ +# Chapter 15: Junit Internals + +This chapter is about a ComparisonCompactor class and **refactoring** to a better version of it \ No newline at end of file diff --git a/notes/16_Chapter_16_Refactoring_SerialDate.md b/notes/16_Chapter_16_Refactoring_SerialDate.md new file mode 100644 index 0000000..e647bdb --- /dev/null +++ b/notes/16_Chapter_16_Refactoring_SerialDate.md @@ -0,0 +1,11 @@ +# Chapter 16: Refactoring SerialDate + +We have a **SerialDate** class from `org.jfree.date` and we want to refactor it. + +## First make it work +we added some unit tests to cover %92 of code which was covered only %50 before. + +found some bugs and fixed along the way + +## Then make it right +We refactored and splitted code into more pieces to make it more readable. diff --git a/notes/17_Chapter_17_Smells_and_Heuristics.md b/notes/17_Chapter_17_Smells_and_Heuristics.md new file mode 100644 index 0000000..fed0f26 --- /dev/null +++ b/notes/17_Chapter_17_Smells_and_Heuristics.md @@ -0,0 +1,218 @@ +# Chapter 17: Smells and Heuristics + +## Comments +### C1: Inappropriate Information +Comments should be reserved for technical notes about the code and design + +### C2: Obsolete Comment +If you find an obsolete comment, it is best to update it or get rid of it as quickly as possible + +### C3: Redundant Comment +A comment is redundant if it describes something that adequately describes itself + +### C4: Poorly Written Comment +If you are going to write a comment, take the time to make sure it is the best comment you can write + +### C5: Commented-Out Code +When you see commented-out code, delete it! Don’t worry, the source code control system still remembers it. + +## Environment +### E1: Build Requires More Than One Step +Building a project should be a single trivial operation. + +### E2: Tests Require More Than One Step +You should be able to run all the unit tests with just one command. + +## Functions +### F1: Too Many Arguments +More than three is very questionable and should be avoided with prejudice. + +### F2: Output Arguments +Readers expect arguments to be inputs, not outputs. + +### F3: Flag Arguments +Boolean arguments loudly declare that the function does more than one thing. + +### F4: Dead Function +Methods that are never called should be discarded. + +## General +### G1: Multiple Languages in One Source File +We should take pains to minimize both the number and extent of extra languages in our source files. + +### G2: Obvious Behavior Is Unimplemented +The Principle of Least Surprise +If the code surprisingly do something else than expected, users of code lose their trust in the original author and must fall back on reading the details of the code + +### G3: Incorrect Behavior at the Boundaries +Don’t rely on your intuition. Look for every boundary condition and write a test for it. + +### G4: Overridden Safeties +Turning off safety inspections may be soothing at first. but will result endless debugging sessions + +### G5: Duplication +Don't repeat yourself (DRY) + +### G6: Code at Wrong Level of Abstraction +Don't mix lower level and higher level of abstractions. separate them into good components. + +### G7: Base Classes Depending on Their Derivatives +base classes should know nothing about their derivatives. + +### G8: Too Much Information +Expose as little information as possible. + +### G9: Dead Code +Dead code is code that isn’t executed. + +### G10: Vertical Separation +Related codes must have least vertical distance from each other. + +### G11: Inconsistency +Stick to your conventions across codebase. + +### G12: Clutter +Redundant and unused codes makes clutter + +### G13: Artificial Coupling +artificial coupling is a coupling between two modules that serves no direct purpose. + +### G14: Feature Envy +Reaching other objects internals in a function. + +### G15: Selector Arguments +Passing argument to a function in order to select the behavior of the function. + +### G16: Obscured Intent +Using notations and symbols to shorten code would obscure our intent to reader. + +### G17: Misplaced Responsibility +Code should be placed where a reader would naturally expect it to be. + +### G18: Inappropriate Static +Prefer using nonstatic methods instead of static methods. + +### G19: Use Explanatory Variables +Intermediate variables make it obvious what the outputs are. + +### G20: Function Names Should Say What They Do +If you have to look at the implementation (or documentation) of the function to know +what it does, then you should work to find a better name or rearrange the functionality. + +### G21: Understand the Algorithm +Writing function to just work is not enough, make sure you understand how it works. + +### G22: Make Logical Dependencies Physical +If one module depends upon another, that dependency should be physical, not just logical. +The dependent module should not make assumptions (in other words, logical dependencies) +about the module it depends upon. Rather it should explicitly ask that module for all +the information it depends upon. + +### G23: Prefer Polymorphism to If/Else or Switch/Case +There may be no more than one switch statement for a given type of selection. The cases in that switch statement must create polymorphic objects that take the place of other such switch statements in the rest of the system. + +### G24: Follow Standard Conventions +Every team should follow a coding standard based on common industry norms. + +### G25: Replace Magic Numbers with Named Constants +it is a bad idea to have raw numbers in your code. + +### G26: Be Precise +Ambiguities and imprecision in code are either a result of disagreements or laziness. + +### G27: Structure over Convention +Enforce design decisions with structure over convention. + +### G28: Encapsulate Conditionals +Extract functions that explain the intent of the conditional. + +### G29: Avoid Negative Conditionals +Negatives are just a bit harder to understand than positives. + +### G30: Functions Should Do One Thing +It is often tempting to create functions that have multiple sections that perform a series of +operations. + +### G31: Hidden Temporal Couplings +Make the temporal coupling explicit by returning result from functions and putting as argument for next operation. + +### G32: Don’t Be Arbitrary +Have a reason for the way you structure your code, and make sure that reason is communicated +by the structure of the code. + +### G33: Encapsulate Boundary Conditions +Boundary conditions are hard to keep track of. Put the processing for them in one place. + +### G34: Functions Should Descend Only One Level of Abstraction +Mixing multiple levels of abstraction make's code harder to understand and change. + +### G35: Keep Configurable Data at High Levels +If you have a constant such as a default or configuration value that is known and expected +at a high level of abstraction, do not bury it in a low-level function. + +### G36: Avoid Transitive Navigation +In general, we don’t want a single module to know much about its collaborators. More specifically, if A collaborates with B, and B collaborates with C, we don’t want modules that use A to know about C. + +## Java +### J1: Avoid Long Import Lists by Using Wildcards +If you use two or more classes from a package, then import the whole package with wildcards. + +### J2: Don’t Inherit Constants +Instead of inheriting constants from interfaces, you can use static import + +### J3: Constants versus Enums +Prefer using enums instead of constants. + +## Names +### N1: Choose Descriptive Names +Don’t be too quick to choose a name. Make sure the name is descriptive. your name must be pretty much what reader expect it to do. + +### N2: Choose Names at the Appropriate Level of Abstraction +Don’t pick names that communicate implementation; choose names the reflect the level of +abstraction of the class or function you are working in. + +### N3: Use Standard Nomenclature Where Possible +Names are easier to understand if they are based on existing convention or usage. + +### N4: Unambiguous Names +Choose names that make the workings of a function or variable unambiguous. + +### N5: Use Long Names for Long Scopes +The longer the scope of the name, the longer and more precise the name should be. + +### N6: Avoid Encodings +Keep your names free of Hungarian pollution. + +### N7: Names Should Describe Side-Effects +Don’t hide side effects with a name. Don’t use a simple verb to describe a function that does more than just that simple action. + +## Tests +### T1: Insufficient Tests +The tests are insufficient so long as there are conditions that have not been explored by the +tests or calculations that have not been validated. + +### T2: Use a Coverage Tool! +Coverage tools reports gaps in your testing strategy. + +### T3: Don’t Skip Trivial Tests +They are easy to write and have high documentary value. + +### T4: An Ignored Test Is a Question about an Ambiguity +Sometimes we are uncertain about a behavioral detail because the requirements are +unclear. + +### T5: Test Boundary Conditions +often get the middle of an algorithm right but misjudge the boundaries. + +### T6: Exhaustively Test Near Bugs +You’ll probably find that the bug was not alone. + +### T7: Patterns of Failure Are Revealing +Sometimes just seeing the pattern of red and green on the test report is enough to spark the “Aha!” that leads to the solution. + +### T8: Test Coverage Patterns Can Be Revealing +Looking at the code that is or is not executed by the passing tests gives clues to why the +failing tests fail. + +### T9: Tests Should Be Fast +do what you must, to keep your tests fast. diff --git a/notes/18_Appendix_A_Concurrency_ii.md b/notes/18_Appendix_A_Concurrency_ii.md new file mode 100644 index 0000000..017ee37 --- /dev/null +++ b/notes/18_Appendix_A_Concurrency_ii.md @@ -0,0 +1,187 @@ +# Appendix A: Concurrency ii + +## Client/Server Example +We have a client/server socket connecting to each other to get data. +We test performance of this code in a 10 seconds timeout. +What factors affect performance ? +- I/O—using a socket, connecting to a database, waiting for virtual memory swapping, + and so on. + - Threading will be benefitial +- Processor—numerical calculations, regular expression processing, garbage collection, + and so on. + - Threading will not be benefitial that much + +### Adding Threading +Applying concurrency and threads comes with some limitations in platform. +we fixed this using Executor framework to handle it for us. +There are different responsibilities to handle: +- Socket connection management +- Client processing +- Threading policy +- Server shutdown policy + +## Possible Paths of Execution +```java +public class IdGenerator { + int lastIdUsed; + + public int incrementValue() { + return ++lastIdUsed; + } +} +``` +How many path's of execution might be when 2 threads call incrementValue() method? +- Thread 1 gets the value of 94, thread 2 gets the value of 95, and lastIdUsed is now 95. +- Thread 1 gets the value of 95, thread 2 gets the value of 94, and lastIdUsed is now 95. +- Thread 1 gets the value of 94, thread 2 gets the value of 94, and lastIdUsed is now 94. + +### Number of Paths +For this simple case of N instructions in a sequence, no looping or conditionals, and T +threads, the total number of possible execution paths is equal to: + +`(NT)!/(N!)^T` + +for above example we have 8 lines of java bytecode so result is: + +`(8*2)!/(8!)^2 = 12,870` + +if we change method signature to : +```java +public synchronized void incrementValue() { + ++lastIdUsed; +} +``` +The number of possible execution pathways becomes two for two threads and **N!** in the +general case. + +### Digging Deeper +In Jvm specification assignment to a 32-bit value (eg. int) is uninterruptable. so this operation is **Atomic**. but 64-bit value like long is 2 atomic operations. + +## Knowing Your Library +### Executor Framework +Executor framework works with classes that implement Runnable or Callable and also supports Future + +**Runnable**: an interface to execute instructions without return value. + +**Callable**: an interface to execute instructions with a return value. + +**Future**: when code needs to execute multiple, independent operations and wait for both to finish. + +### Nonblocking Solutions +In modern processors there is an operation called Compare and Swap (CAS) which allows us to use one single atomic operation for any assignment. +see. AtomicBoolean, AtomicInteger and AtomicReference in Java. + +### Nonthread-Safe Classes +- SimpleDateFormat +- Database Connections +- Containers in java.util +- Servlets + +There are thread-safe classes in java.util.concurrent eg. `ConcurrentHashMap` + +## Dependencies Between Methods Can Break Concurrent Code +Having multiple synchronized methods have a subtle side effect. +if client use these methods and have some conditionals to choose between method calls, there will be boundary problems and inconsistencies. + +### Tolerate the Failure +Sometimes you can set things up such that the failure causes no harm. + +### Client-Based Locking +We can use synchronized lock for all of the clients using server code. +This brings the risk of forgetting a client to lock and blowing system. and also violates DRY. + +### Server-Based Locking +create a single method containing all concurrent operations in the server. +In general, you should prefer server-based locking for these reasons: +- It reduces repeated code +- It allows for better performance +- It reduces the possibility of error +- It enforces a single policy +- It reduces the scope of the shared variables + +If you don’t own server code, use **ADAPTER** pattern to wrap server call in a single place. + +## Increasing Throughput +Consider a page reader program, which downloads some pages and prints statistics + +### Single-Thread Calculation of Throughput +Lets assume following facts: +- I/O time to retrieve a page (average): 1 second +- Processing time to parse page (average): 0.5 seconds + +Total execution time = 1.5 seconds * N (pages) + +### Multithread Calculation of Throughput +We aquire T times faster throughput where T is thread count. + +## Deadlock +When some processes are waiting for each other to release shared resources. +To really solve the problem of deadlock, we need to understand what causes it. +There are four conditions required for deadlock to occur: +- Mutual exclusion +- Lock & wait +- No preemption +- Circular wait + +### Mutual exclusion +hen multiple threads need to use the same resources and those resources : +- Cannot be used by multiple threads at the same time. +- Are limited in number. + +eg. Database connection, a file open for write + +**How to break?** +- Using resources that allow simultaneous use, for example, AtomicInteger. +- Increasing the number of resources such that it equals or exceeds the number of competing threads. +- Checking that all your resources are free before seizing any. + +### Lock & Wait +Once a thread acquires a resource, it will not release the resource until it has acquired all +of the other resources it requires and has completed its work. + +**How to break?** +refuse to wait, Check each resource before you seize it, and release all resources and start over if you run into one that’s busy. + +Problems: +- Starvation +- Livelock + +### No Preemption +One thread cannot take resources away from another thread. Once a thread holds a +resource, the only way for another thread to get it is for the holding thread to release it. + +**How to break?** +When a thread discovers that a resource is busy, it asks the owner to release it. If the owner is also waiting for some other resource, it releases them all and starts over. + +### Circular Wait +This is also referred to as the **deadly embrace**. Imagine two threads, T1 and T2, and two +resources, R1 and R2. T1 has R1, T2 has R2. T1 also requires R2, and T2 also requires R1. + +**How to break?** +if all threads can agree on a global ordering of resources and if they all allocate resources in that order, then deadlock is impossible. + +## Testing Multithreaded Code +```java +public class ClassWithThreadingProblem { + int nextId; + + public int takeNextId() { + return nextId++; + } +} +``` +Here’s a description of a test that will prove the code is broken: +- Remember the current value of nextId. +- Create two threads, both of which call takeNextId() once. +- Verify that nextId is two more than what we started with. +- Run this until we demonstrate that nextId was only incremented by one instead of two. + +Using this approach, the chance that we pass this test is so low (once in million). +- We can use a continuous integration server to run these tests for us with different configurations. +- Run the test on every one of the target deployment platforms. +- Run the tests on a machine with varying loads. + +`Make sure to carefully log the conditions under which the test failed.` + +## Tool Support for Testing Thread-Based Code +Eg. ConTest from IBM \ No newline at end of file diff --git a/slides/00 - Introduction.pptx b/slides/00_Introduction.pptx similarity index 100% rename from slides/00 - Introduction.pptx rename to slides/00_Introduction.pptx diff --git a/slides/01 - Chapter 1 - Clean code.pptx b/slides/01_Chapter_1_Clean_code.pptx similarity index 95% rename from slides/01 - Chapter 1 - Clean code.pptx rename to slides/01_Chapter_1_Clean_code.pptx index 1c0ad51..695a561 100644 Binary files a/slides/01 - Chapter 1 - Clean code.pptx and b/slides/01_Chapter_1_Clean_code.pptx differ diff --git a/slides/02 - Chapter 2 - Meaningful names.pptx b/slides/02_Chapter_2_Meaningful_names.pptx similarity index 100% rename from slides/02 - Chapter 2 - Meaningful names.pptx rename to slides/02_Chapter_2_Meaningful_names.pptx diff --git a/slides/03 - Chapter 3 - Functions.pptx b/slides/03_Chapter_3_Functions.pptx similarity index 100% rename from slides/03 - Chapter 3 - Functions.pptx rename to slides/03_Chapter_3_Functions.pptx diff --git a/slides/04 - Chapter 4 - Comments.pptx b/slides/04_Chapter_4_Comments.pptx similarity index 96% rename from slides/04 - Chapter 4 - Comments.pptx rename to slides/04_Chapter_4_Comments.pptx index b0704a9..a197691 100644 Binary files a/slides/04 - Chapter 4 - Comments.pptx and b/slides/04_Chapter_4_Comments.pptx differ diff --git a/slides/05_Chapter_5_Formatting.pptx b/slides/05_Chapter_5_Formatting.pptx new file mode 100644 index 0000000..9f960e3 Binary files /dev/null and b/slides/05_Chapter_5_Formatting.pptx differ diff --git a/slides/06_Chapter_6_Objects_and_Data_structures.pptx b/slides/06_Chapter_6_Objects_and_Data_structures.pptx new file mode 100644 index 0000000..d1528ad Binary files /dev/null and b/slides/06_Chapter_6_Objects_and_Data_structures.pptx differ diff --git a/slides/07_Chapter_7_Error_Handling.pptx b/slides/07_Chapter_7_Error_Handling.pptx new file mode 100644 index 0000000..037153f Binary files /dev/null and b/slides/07_Chapter_7_Error_Handling.pptx differ diff --git a/slides/08_Chapter_8_Boundaries.pptx b/slides/08_Chapter_8_Boundaries.pptx new file mode 100644 index 0000000..55d0133 Binary files /dev/null and b/slides/08_Chapter_8_Boundaries.pptx differ diff --git a/src/main/java/MainEntry.java b/src/main/java/MainEntry.java deleted file mode 100644 index a97f400..0000000 --- a/src/main/java/MainEntry.java +++ /dev/null @@ -1,6 +0,0 @@ -public class MainEntry { - - public static void main(String[] args) { - System.out.println("Hello uncle bob!"); - } -} diff --git a/src/main/java/clean/code/added/to/make/code/build/Account.java b/src/main/java/clean/code/added/to/make/code/build/Account.java new file mode 100644 index 0000000..81ce938 --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/Account.java @@ -0,0 +1,4 @@ +package clean.code.added.to.make.code.build; + +public class Account { +} diff --git a/src/main/java/clean/code/added/to/make/code/build/AnnualDateRule.java b/src/main/java/clean/code/added/to/make/code/build/AnnualDateRule.java new file mode 100644 index 0000000..ad7f05d --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/AnnualDateRule.java @@ -0,0 +1,12 @@ +package clean.code.added.to.make.code.build; + +public class AnnualDateRule { + public SerialDate getDate(int year) { + return null; //TODO: Auto-generated + } + + @Override + public Object clone() throws CloneNotSupportedException { + return new Object(); + } +} diff --git a/src/main/java/clean/code/added/to/make/code/build/Blah.java b/src/main/java/clean/code/added/to/make/code/build/Blah.java new file mode 100644 index 0000000..d186e72 --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/Blah.java @@ -0,0 +1,7 @@ +package clean.code.added.to.make.code.build; + +public class Blah { + public PageCrawler getPageCrawler() { + return null; //TODO: Auto-generated + } +} diff --git a/src/main/java/clean/code/added/to/make/code/build/Column.java b/src/main/java/clean/code/added/to/make/code/build/Column.java new file mode 100644 index 0000000..ae27187 --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/Column.java @@ -0,0 +1,4 @@ +package clean.code.added.to.make.code.build; + +public class Column { +} \ No newline at end of file diff --git a/src/main/java/clean/code/added/to/make/code/build/Criteria.java b/src/main/java/clean/code/added/to/make/code/build/Criteria.java new file mode 100644 index 0000000..8c63c37 --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/Criteria.java @@ -0,0 +1,4 @@ +package clean.code.added.to.make.code.build; + +public class Criteria { +} diff --git a/src/main/java/clean/code/added/to/make/code/build/DayAndMonthRule.java b/src/main/java/clean/code/added/to/make/code/build/DayAndMonthRule.java new file mode 100644 index 0000000..4b95ab7 --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/DayAndMonthRule.java @@ -0,0 +1,4 @@ +package clean.code.added.to.make.code.build; + +public class DayAndMonthRule extends AnnualDateRule { +} diff --git a/src/main/java/clean/code/added/to/make/code/build/PageCrawler.java b/src/main/java/clean/code/added/to/make/code/build/PageCrawler.java new file mode 100644 index 0000000..04e92cb --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/PageCrawler.java @@ -0,0 +1,15 @@ +package clean.code.added.to.make.code.build; + +public class PageCrawler { + public void setDeadEndStrategy(VirtualEnabledPageCrawler virtualEnabledPageCrawler) { + //TODO: Auto-generated + } + + public WikiPage getPage(Blah root, WikiPagePath path) { + return null; //TODO: Auto-generated + } + + public WikiPagePath getFullPath(WikiPage page) { + return null; //TODO: Auto-generated + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/added/to/make/code/build/PageCrawlerImpl.java b/src/main/java/clean/code/added/to/make/code/build/PageCrawlerImpl.java new file mode 100644 index 0000000..65a44a1 --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/PageCrawlerImpl.java @@ -0,0 +1,7 @@ +package clean.code.added.to.make.code.build; + +public class PageCrawlerImpl { + public static WikiPage getInheritedPage(Object suite_setup_name, WikiPage wikiPage) { + return null; //TODO: Auto-generated + } +} diff --git a/src/main/java/clean/code/added/to/make/code/build/PageData.java b/src/main/java/clean/code/added/to/make/code/build/PageData.java new file mode 100644 index 0000000..e8bcf04 --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/PageData.java @@ -0,0 +1,23 @@ +package clean.code.added.to.make.code.build; + +public class PageData { + public WikiPage getWikiPage() { + return null; //TODO: Auto-generated + } + + public boolean hasAttribute(String test) { + return false; //TODO: Auto-generated + } + + public Object getContent() { + return null; //TODO: Auto-generated + } + + public void setContent(String s) { + //TODO: Auto-generated + } + + public String getHtml() { + return null; //TODO: Auto-generated + } +} diff --git a/src/main/java/clean/code/added/to/make/code/build/PathParser.java b/src/main/java/clean/code/added/to/make/code/build/PathParser.java new file mode 100644 index 0000000..47f6f14 --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/PathParser.java @@ -0,0 +1,11 @@ +package clean.code.added.to.make.code.build; + +public class PathParser { + public static WikiPagePath parse(String resource) { + return null; //TODO: Auto-generated + } + + public static String render(WikiPagePath fullPath) { + return null; //TODO: Auto-generated + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/added/to/make/code/build/SerialDate.java b/src/main/java/clean/code/added/to/make/code/build/SerialDate.java new file mode 100644 index 0000000..4d0c218 --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/SerialDate.java @@ -0,0 +1,22 @@ +package clean.code.added.to.make.code.build; + +public class SerialDate { + public static final int PRECEDING = 0; + public static final int NEAREST = 1; + public static final int FOLLOWING = 2; + public static int MINIMUM_YEAR_SUPPORTED; + public static int MAXIMUM_YEAR_SUPPORTED; + public static int MONDAY; + + public static SerialDate getPreviousDayOfWeek(int dayOfWeek, SerialDate base) { + return null; //TODO: Auto-generated + } + + public static SerialDate getNearestDayOfWeek(int dayOfWeek, SerialDate base) { + return null; //TODO: Auto-generated + } + + public static SerialDate getFollowingDayOfWeek(int dayOfWeek, SerialDate base) { + return null; //TODO: Auto-generated + } +} diff --git a/src/main/java/clean/code/added/to/make/code/build/SuiteResponder.java b/src/main/java/clean/code/added/to/make/code/build/SuiteResponder.java new file mode 100644 index 0000000..bb67d76 --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/SuiteResponder.java @@ -0,0 +1,6 @@ +package clean.code.added.to.make.code.build; + +public class SuiteResponder { + public static String SUITE_SETUP_NAME; + public static String SUITE_TEARDOWN_NAME; +} diff --git a/src/main/java/clean/code/added/to/make/code/build/VirtualEnabledPageCrawler.java b/src/main/java/clean/code/added/to/make/code/build/VirtualEnabledPageCrawler.java new file mode 100644 index 0000000..52234d2 --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/VirtualEnabledPageCrawler.java @@ -0,0 +1,4 @@ +package clean.code.added.to.make.code.build; + +public class VirtualEnabledPageCrawler { +} diff --git a/src/main/java/clean/code/added/to/make/code/build/WikiPage.java b/src/main/java/clean/code/added/to/make/code/build/WikiPage.java new file mode 100644 index 0000000..9cb45cb --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/WikiPage.java @@ -0,0 +1,15 @@ +package clean.code.added.to.make.code.build; + +public class WikiPage { + public PageData getData() { + return null; //TODO: Auto-generated + } + + public PageCrawler getPageCrawler() { + return null; //TODO: Auto-generated + } + + public WikiPagePath getFullPath(WikiPage page) { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/added/to/make/code/build/WikiPagePath.java b/src/main/java/clean/code/added/to/make/code/build/WikiPagePath.java new file mode 100644 index 0000000..c65f4c0 --- /dev/null +++ b/src/main/java/clean/code/added/to/make/code/build/WikiPagePath.java @@ -0,0 +1,4 @@ +package clean.code.added.to.make.code.build; + +public class WikiPagePath { +} diff --git a/src/main/java/clean/code/appendixA/ClassWithThreadingProblem.java b/src/main/java/clean/code/appendixA/ClassWithThreadingProblem.java new file mode 100644 index 0000000..9187ca5 --- /dev/null +++ b/src/main/java/clean/code/appendixA/ClassWithThreadingProblem.java @@ -0,0 +1,9 @@ +package clean.code.appendixA; + +public class ClassWithThreadingProblem { + public int nextId; + + public int takeNextId() { + return nextId++; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/appendixA/Example.java b/src/main/java/clean/code/appendixA/Example.java new file mode 100644 index 0000000..af855c9 --- /dev/null +++ b/src/main/java/clean/code/appendixA/Example.java @@ -0,0 +1,17 @@ +package clean.code.appendixA; + +/** + * Example class to demonstrate bytecode + * and concurrency issue with non atomic operations + */ +public class Example { + int lastId; + + public void resetId() { + lastId = 0; + } + + public int getNextId() { + return ++lastId; + } +} diff --git a/src/main/java/clean/code/appendixA/MessageUtils.java b/src/main/java/clean/code/appendixA/MessageUtils.java new file mode 100644 index 0000000..a771efb --- /dev/null +++ b/src/main/java/clean/code/appendixA/MessageUtils.java @@ -0,0 +1,24 @@ +package clean.code.appendixA; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.net.Socket; + +public class MessageUtils { + public static void sendMessage(Socket socket, String message) + throws IOException { + OutputStream stream = socket.getOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(stream); + oos.writeUTF(message); + oos.flush(); + } + + public static String getMessage(Socket socket) throws IOException { + InputStream stream = socket.getInputStream(); + ObjectInputStream ois = new ObjectInputStream(stream); + return ois.readUTF(); + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/appendixA/nonthreaded/Server.java b/src/main/java/clean/code/appendixA/nonthreaded/Server.java new file mode 100644 index 0000000..9cede32 --- /dev/null +++ b/src/main/java/clean/code/appendixA/nonthreaded/Server.java @@ -0,0 +1,82 @@ +package clean.code.appendixA.nonthreaded; + +import clean.code.appendixA.MessageUtils; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; + +public class Server implements Runnable { + ServerSocket serverSocket; + volatile boolean keepProcessing = true; + + public Server(int port, int millisecondsTimeout) throws IOException { + serverSocket = new ServerSocket(port); + serverSocket.setSoTimeout(millisecondsTimeout); + + } + + public void run() { + System.out.printf("Server Starting\n"); + + while (keepProcessing) { + try { + System.out.printf("accepting client\n"); + Socket socket = serverSocket.accept(); + System.out.printf("got client\n"); + process(socket); + } catch (Exception e) { + handle(e); + } + } + + } + + private void handle(Exception e) { + if (!(e instanceof SocketException)) { + e.printStackTrace(); + } + } + + public void stopProcessing() { + keepProcessing = false; + closeIgnoringException(serverSocket); + } + void process(Socket socket) { + if (socket == null) + return; + + try { + System.out.printf("Server: getting message\n"); + String message = MessageUtils.getMessage(socket); + System.out.printf("Server: got message: %s\n", message); + Thread.sleep(1000); + System.out.printf("Server: sending reply: %s\n", message); + MessageUtils.sendMessage(socket, "Processed: " + message); + System.out.printf("Server: sent\n"); + closeIgnoringException(socket); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + private void closeIgnoringException(Socket socket) { + if (socket != null) + try { + socket.close(); + } catch (IOException ignore) { + } + + } + + private void closeIgnoringException(ServerSocket serverSocket) { + if (serverSocket != null) + try { + serverSocket.close(); + } catch (IOException ignore) { + } + + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/appendixA/threaded/Server.java b/src/main/java/clean/code/appendixA/threaded/Server.java new file mode 100644 index 0000000..6c05b36 --- /dev/null +++ b/src/main/java/clean/code/appendixA/threaded/Server.java @@ -0,0 +1,91 @@ +package clean.code.appendixA.threaded; + +import clean.code.appendixA.MessageUtils; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; + +public class Server implements Runnable { + ServerSocket serverSocket; + volatile boolean keepProcessing = true; + + public Server(int port, int millisecondsTimeout) throws IOException { + serverSocket = new ServerSocket(port); + serverSocket.setSoTimeout(millisecondsTimeout); + + } + + public void run() { + System.out.printf("Server Starting\n"); + + while (keepProcessing) { + try { + System.out.printf("accepting client\n"); + Socket socket = serverSocket.accept(); + System.out.printf("got client\n"); + process(socket); + } catch (Exception e) { + handle(e); + } + } + + } + + private void handle(Exception e) { + if (!(e instanceof SocketException)) { + e.printStackTrace(); + } + } + + public void stopProcessing() { + keepProcessing = false; + closeIgnoringException(serverSocket); + } + + void process(final Socket socket) { + if (socket == null) + return; + + Runnable clientHandler = new Runnable() { + @Override + public void run() { + try { + System.out.printf("Server: getting message\n"); + String message = MessageUtils.getMessage(socket); + System.out.printf("Server: got message: %s\n", message); + Thread.sleep(1000); + System.out.printf("Server: sending reply: %s\n", message); + MessageUtils.sendMessage(socket, "Processed: " + message); + System.out.printf("Server: sent\n"); + closeIgnoringException(socket); + } catch (Exception e) { + e.printStackTrace(); + } + } + }; + + Thread clientConnection = new Thread(clientHandler); + clientConnection.start(); + + } + + private void closeIgnoringException(Socket socket) { + if (socket != null) + try { + socket.close(); + } catch (IOException ignore) { + } + + } + + private void closeIgnoringException(ServerSocket serverSocket) { + if (serverSocket != null) + try { + serverSocket.close(); + } catch (IOException ignore) { + } + + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter02/GuessStatisticsMessage.java b/src/main/java/clean/code/chapter02/GuessStatisticsMessage.java new file mode 100644 index 0000000..7725185 --- /dev/null +++ b/src/main/java/clean/code/chapter02/GuessStatisticsMessage.java @@ -0,0 +1,42 @@ +package clean.code.chapter02; + +public class GuessStatisticsMessage { + private String number; + private String verb; + private String pluralModifier; + + public String make(char candidate, int count) { + createPluralDependentMessageParts(count); + return String.format( + "There %s %s %s%s", + verb, number, candidate, pluralModifier ); + } + + private void createPluralDependentMessageParts(int count) { + if (count == 0) { + thereAreNoLetters(); + } else if (count == 1) { + thereIsOneLetter(); + } else { + thereAreManyLetters(count); + } + } + + private void thereAreManyLetters(int count) { + number = Integer.toString(count); + verb = "are"; + pluralModifier = "s"; + } + + private void thereIsOneLetter() { + number = "1"; + verb = "is"; + pluralModifier = ""; + } + + private void thereAreNoLetters() { + number = "no"; + verb = "are"; + pluralModifier = "s"; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter03/HtmlUnit.java b/src/main/java/clean/code/chapter03/HtmlUnit.java new file mode 100644 index 0000000..fd2c5f0 --- /dev/null +++ b/src/main/java/clean/code/chapter03/HtmlUnit.java @@ -0,0 +1,70 @@ +package clean.code.chapter03; + +import clean.code.added.to.make.code.build.*; + +public class HtmlUnit { + public static String testableHtml( + PageData pageData, + boolean includeSuiteSetup + ) throws Exception { + WikiPage wikiPage = pageData.getWikiPage(); + StringBuffer buffer = new StringBuffer(); + if (pageData.hasAttribute("Test")) { + if (includeSuiteSetup) { + WikiPage suiteSetup = + PageCrawlerImpl.getInheritedPage( + SuiteResponder.SUITE_SETUP_NAME, wikiPage + ); + if (suiteSetup != null) { + WikiPagePath pagePath = + suiteSetup.getPageCrawler().getFullPath(suiteSetup); + String pagePathName = PathParser.render(pagePath); + buffer.append("!include -setup .") + .append(pagePathName) + .append("\n"); + } + } + WikiPage setup = + PageCrawlerImpl.getInheritedPage("SetUp", wikiPage); + if (setup != null) { + WikiPagePath setupPath = + wikiPage.getPageCrawler().getFullPath(setup); + String setupPathName = PathParser.render(setupPath); + buffer.append("!include -setup .") + .append(setupPathName) + .append("\n"); + } + } + buffer.append(pageData.getContent()); + if (pageData.hasAttribute("Test")) { + WikiPage teardown = + PageCrawlerImpl.getInheritedPage("TearDown", wikiPage); + if (teardown != null) { + WikiPagePath tearDownPath = + wikiPage.getPageCrawler().getFullPath(teardown); + String tearDownPathName = PathParser.render(tearDownPath); + buffer.append("\n") + .append("!include -teardown .") + .append(tearDownPathName) + .append("\n"); + } + if (includeSuiteSetup) { + WikiPage suiteTeardown = + PageCrawlerImpl.getInheritedPage( + SuiteResponder.SUITE_TEARDOWN_NAME, + wikiPage + ); + if (suiteTeardown != null) { + WikiPagePath pagePath = + suiteTeardown.getPageCrawler().getFullPath (suiteTeardown); + String pagePathName = PathParser.render(pagePath); + buffer.append("!include -teardown .") + .append(pagePathName) + .append("\n"); + } + } + } + pageData.setContent(buffer.toString()); + return pageData.getHtml(); + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter03/SetupTeardownIncluder.java b/src/main/java/clean/code/chapter03/SetupTeardownIncluder.java new file mode 100644 index 0000000..f59c33a --- /dev/null +++ b/src/main/java/clean/code/chapter03/SetupTeardownIncluder.java @@ -0,0 +1,106 @@ +package clean.code.chapter03; + +import clean.code.added.to.make.code.build.*; + +public class SetupTeardownIncluder { + private PageData pageData; + private boolean isSuite; + private WikiPage testPage; + private StringBuffer newPageContent; + private PageCrawler pageCrawler; + + public static String render(PageData pageData) throws Exception { + return render(pageData, false); + } + + public static String render(PageData pageData, boolean isSuite) + throws Exception { + return new SetupTeardownIncluder(pageData).render(isSuite); + } + + private SetupTeardownIncluder(PageData pageData) { + this.pageData = pageData; + testPage = pageData.getWikiPage(); + pageCrawler = testPage.getPageCrawler(); + newPageContent = new StringBuffer(); + } + + private String render(boolean isSuite) throws Exception { + this.isSuite = isSuite; + if (isTestPage()) + includeSetupAndTeardownPages(); + return pageData.getHtml(); + } + + private boolean isTestPage() throws Exception { + return pageData.hasAttribute("Test"); + } + + private void includeSetupAndTeardownPages() throws Exception { + includeSetupPages(); + includePageContent(); + includeTeardownPages(); + updatePageContent(); + } + private void includeSetupPages() throws Exception { + if (isSuite) + includeSuiteSetupPage(); + includeSetupPage(); + } + + private void includeSuiteSetupPage() throws Exception { + include(SuiteResponder.SUITE_SETUP_NAME, "-setup"); + } + + private void includeSetupPage() throws Exception { + include("SetUp", "-setup"); + } + + private void includePageContent() throws Exception { + newPageContent.append(pageData.getContent()); + } + + private void includeTeardownPages() throws Exception { + includeTeardownPage(); + if (isSuite) + includeSuiteTeardownPage(); + } + + private void includeTeardownPage() throws Exception { + include("TearDown", "-teardown"); + } + + private void includeSuiteTeardownPage() throws Exception { + include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown"); + } + + private void updatePageContent() throws Exception { + pageData.setContent(newPageContent.toString()); + } + + private void include(String pageName, String arg) throws Exception { + WikiPage inheritedPage = findInheritedPage(pageName); + if (inheritedPage != null) { + String pagePathName = getPathNameForPage(inheritedPage); + buildIncludeDirective(pagePathName, arg); + } + } + + private WikiPage findInheritedPage(String pageName) throws Exception { + return PageCrawlerImpl.getInheritedPage(pageName, testPage); + } + + private String getPathNameForPage(WikiPage page) throws Exception { + WikiPagePath pagePath = pageCrawler.getFullPath(page); + return PathParser.render(pagePath); + } + + private void buildIncludeDirective(String pagePathName, String arg) { + newPageContent + .append("\n!include ") + .append(arg) + .append(" .") + .append(pagePathName) + .append("\n"); + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter04/GeneratePrimes.java b/src/main/java/clean/code/chapter04/GeneratePrimes.java new file mode 100644 index 0000000..3369c1d --- /dev/null +++ b/src/main/java/clean/code/chapter04/GeneratePrimes.java @@ -0,0 +1,81 @@ +package clean.code.chapter04; + +/** + * This class Generates prime numbers up to a user specified + * maximum. The algorithm used is the Sieve of Eratosthenes. + *

+ * Eratosthenes of Cyrene, b. c. 276 BC, Cyrene, Libya -- + * d. c. 194, Alexandria. The first man to calculate the + * circumference of the Earth. Also known for working on + * calendars with leap years and ran the library at Alexandria. + *

+ * The algorithm is quite simple. Given an array of integers + * starting at 2. Cross out all multiples of 2. Find the next + * uncrossed integer, and cross out all of its multiples. + * Repeat untilyou have passed the square root of the maximum + * value. + * + * @author Alphonse + * @version 13 Feb 2002 atp + */ + +public class GeneratePrimes +{ + /** + * @param maxValue is the generation limit. + */ + public static int[] generatePrimes(int maxValue) + { + if (maxValue >= 2) // the only valid case + { + // declarations + int s = maxValue + 1; // size of array + boolean[] f = new boolean[s]; + int i; + // initialize array to true. + for (i = 0; i < s; i++) + f[i] = true; + + // get rid of known non-primes + f[0] = f[1] = false; + + // sieve + int j; + for (i = 2; i < Math.sqrt(s) + 1; i++) + { + if (f[i]) // if i is uncrossed, cross its multiples. + { + for (j = 2 * i; j < s; j += i) + f[j] = false; // multiple is not prime + } + } + + // how many primes are there? + int count = 0; + for (i = 0; i < s; i++) + { + if (f[i]) + count++; // bump count. + } + + int[] primes = new int[count]; + + // move the primes into the result + for (i = 0, j = 0; i < s; i++) + { + if (f[i]) // if prime + primes[j++] = i; + } + + return primes; // return the primes + } + else // maxValue < 2 + return new int[0]; // return null array if bad input. + } + + public static void main(String[] args) { + for (int i: generatePrimes(50)) { + System.out.print(i + " "); + } + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter04/PrimeGenerator.java b/src/main/java/clean/code/chapter04/PrimeGenerator.java new file mode 100644 index 0000000..20d6f25 --- /dev/null +++ b/src/main/java/clean/code/chapter04/PrimeGenerator.java @@ -0,0 +1,81 @@ +package clean.code.chapter04; + +/** + * This class Generates prime numbers up to a user specified + * maximum. The algorithm used is the Sieve of Eratosthenes. + * Given an array of integers starting at 2: + * Find the first uncrossed integer, and cross out all its + * multiples. Repeat until there are no more multiples + * in the array. + */ + +public class PrimeGenerator { + private static boolean[] crossedOut; + private static int[] result; + + public static int[] generatePrimes(int maxValue) { + if (maxValue < 2) + return new int[0]; + else { + uncrossIntegersUpTo(maxValue); + crossOutMultiples(); + putUncrossedIntegersIntoResult(); + return result; + } + } + + private static void uncrossIntegersUpTo(int maxValue) { + crossedOut = new boolean[maxValue + 1]; + for (int i = 2; i < crossedOut.length; i++) + crossedOut[i] = false; + } + + private static void crossOutMultiples() { + int limit = determineIterationLimit(); + for (int i = 2; i <= limit; i++) + if (notCrossed(i)) + crossOutMultiplesOf(i); + } + + private static int determineIterationLimit() { + // Every multiple in the array has a prime factor that + // is less than or equal to the root of the array size, + // so we don't have to cross out multiples of numbers + // larger than that root. + double iterationLimit = Math.sqrt(crossedOut.length); + return (int) iterationLimit; + } + + private static void crossOutMultiplesOf(int i) { + for (int multiple = 2 * i; + multiple < crossedOut.length; + multiple += i) + crossedOut[multiple] = true; + } + + private static boolean notCrossed(int i) { + return crossedOut[i] == false; + } + + private static void putUncrossedIntegersIntoResult() { + result = new int[numberOfUncrossedIntegers()]; + for (int j = 0, i = 2; i < crossedOut.length; i++) + if (notCrossed(i)) + result[j++] = i; + } + + private static int numberOfUncrossedIntegers() { + int count = 0; + for (int i = 2; i < crossedOut.length; i++) + if (notCrossed(i)) + count++; + + return count; + } + + public static void main(String[] args) { + for (int i : generatePrimes(50)) { + System.out.print(i + " "); + } + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter04/wc.java b/src/main/java/clean/code/chapter04/wc.java new file mode 100644 index 0000000..e874f02 --- /dev/null +++ b/src/main/java/clean/code/chapter04/wc.java @@ -0,0 +1,29 @@ +package clean.code.chapter04; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +public class wc { + public static void main(String[] args) { + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + String line; + int lineCount = 0; + int charCount = 0; + int wordCount = 0; + try { + while ((line = in.readLine()) != null) { + lineCount++; + charCount += line.length(); + String words[] = line.split("\\W"); + wordCount += words.length; + } //while + System.out.println("wordCount = " + wordCount); + System.out.println("lineCount = " + lineCount); + System.out.println("charCount = " + charCount); + } // try + catch (IOException e) { + System.err.println("Error:" + e.getMessage()); + } //catch + } //main +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter05/CodeAnalyzer.java b/src/main/java/clean/code/chapter05/CodeAnalyzer.java new file mode 100644 index 0000000..ab87e45 --- /dev/null +++ b/src/main/java/clean/code/chapter05/CodeAnalyzer.java @@ -0,0 +1,117 @@ +package clean.code.chapter05; + +import javax.naming.directory.Attributes; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +public class CodeAnalyzer implements JavaFileAnalysis { + private int lineCount; + private int maxLineWidth; + private int widestLineNumber; + private LineWidthHistogram lineWidthHistogram; + private int totalChars; + + public CodeAnalyzer() { + lineWidthHistogram = new LineWidthHistogram(); + } + + public static List findJavaFiles(File parentDirectory) { + List files = new ArrayList(); + findJavaFiles(parentDirectory, files); + return files; + } + + private static void findJavaFiles(File parentDirectory, List files) { + for (File file : parentDirectory.listFiles()) { + if (file.getName().endsWith(".java")) + files.add(file); + else if (file.isDirectory()) + findJavaFiles(file, files); + } + } + + public void analyzeFile(File javaFile) throws Exception { + BufferedReader br = new BufferedReader(new FileReader(javaFile)); + String line; + while ((line = br.readLine()) != null) + measureLine(line); + } + + private void measureLine(String line) { + lineCount++; + int lineSize = line.length(); + totalChars += lineSize; + lineWidthHistogram.addLine(lineSize, lineCount); + recordWidestLine(lineSize); + } + + private void recordWidestLine(int lineSize) { + if (lineSize > maxLineWidth) { + maxLineWidth = lineSize; + widestLineNumber = lineCount; + } + } + + public int getLineCount() { + return lineCount; + } + + public int getMaxLineWidth() { + return maxLineWidth; + } + + public int getWidestLineNumber() { + return widestLineNumber; + } + + public LineWidthHistogram getLineWidthHistogram() { + return lineWidthHistogram; + } + + public double getMeanLineWidth() { + return (double)totalChars/lineCount; + } + + public int getMedianLineWidth() { + Integer[] sortedWidths = getSortedWidths(); + int cumulativeLineCount = 0; + for (int width : sortedWidths) { + cumulativeLineCount += lineCountForWidth(width); + if (cumulativeLineCount > lineCount/2) + return width; + } + throw new Error("Cannot get here"); + } + + private int lineCountForWidth(int width) { + return lineWidthHistogram.getLinesforWidth(width).size(); + } + + private Integer[] getSortedWidths() { + Set widths = lineWidthHistogram.getWidths(); + Integer[] sortedWidths = (widths.toArray(new Integer[0])); + Arrays.sort(sortedWidths); + return sortedWidths; + } +} + +// Added to get code to build +interface JavaFileAnalysis {} +class LineWidthHistogram { + public void addLine(int lineSize, int lineCount) { + //TODO: Auto-generated + } + + public Attributes getLinesforWidth(int width) { + return null; //TODO: Auto-generated + } + + public Set getWidths() { + return null; //TODO: Auto-generated + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter05/WikiPageResponder.java b/src/main/java/clean/code/chapter05/WikiPageResponder.java new file mode 100644 index 0000000..f2512dc --- /dev/null +++ b/src/main/java/clean/code/chapter05/WikiPageResponder.java @@ -0,0 +1,96 @@ +package clean.code.chapter05; + +import clean.code.added.to.make.code.build.*; + +public class WikiPageResponder implements SecureResponder { + protected WikiPage page; + protected PageData pageData; + protected String pageTitle; + protected Request request; + protected PageCrawler crawler; + + public Response makeResponse(FitNesseContext context, Request request) + throws Exception { + String pageName = getPageNameOrDefault(request, "FrontPage"); + loadPage(pageName, context); + if (page == null) + return notFoundResponse(context, request); + else + return makePageResponse(context); + } + + private String getPageNameOrDefault(Request request, String defaultPageName) + { + String pageName = request.getResource(); + if (StringUtil.isBlank(pageName)) + pageName = defaultPageName; + + return pageName; + } + + protected void loadPage(String resource, FitNesseContext context) + throws Exception { + WikiPagePath path = PathParser.parse(resource); + crawler = context.root.getPageCrawler(); + crawler.setDeadEndStrategy(new VirtualEnabledPageCrawler()); + page = crawler.getPage(context.root, path); + if (page != null) + pageData = page.getData(); + } + + private Response notFoundResponse(FitNesseContext context, Request request) + throws Exception { + return new NotFoundResponder().makeResponse(context, request); + } + + private SimpleResponse makePageResponse(FitNesseContext context) + throws Exception { + pageTitle = PathParser.render(crawler.getFullPath(page)); + String html = makeHtml(context); + + SimpleResponse response = new SimpleResponse(); + response.setMaxAge(0); + response.setContent(html); + return response; + } + + // added to make code build + private String makeHtml(FitNesseContext context) { + return null; //TODO: Auto-generated + } +} + +// Following added to make code build +interface SecureResponder {} + + +class Request { + public String getResource() { + return null; //TODO: Auto-generated + } +} + +class Response {} +class FitNesseContext { + public Blah root; +} +class SimpleResponse extends Response { + public void setMaxAge(int i) { + //TODO: Auto-generated + } + + public void setContent(String html) { + //TODO: Auto-generated + } +} +class StringUtil { + public static boolean isBlank(String pageName) { + return false; //TODO: Auto-generated + } +} + +class NotFoundResponder { + public Response makeResponse(FitNesseContext context, Request request) { + return null; //TODO: Auto-generated + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter10/original/PrintPrimes.java b/src/main/java/clean/code/chapter10/original/PrintPrimes.java new file mode 100644 index 0000000..d0dfd47 --- /dev/null +++ b/src/main/java/clean/code/chapter10/original/PrintPrimes.java @@ -0,0 +1,69 @@ +package clean.code.chapter10.original; + +public class PrintPrimes { + public static void main(String[] args) { + final int M = 1000; + final int RR = 50; + final int CC = 4; + final int WW = 10; + final int ORDMAX = 30; + int P[] = new int[M + 1]; + int PAGENUMBER; + int PAGEOFFSET; + int ROWOFFSET; + int C; + int J; + int K; + boolean JPRIME; + int ORD; + int SQUARE; + int N; + int MULT[] = new int[ORDMAX + 1]; + + J = 1; + K = 1; + P[1] = 2; + ORD = 2; + SQUARE = 9; + + while (K < M) { + do { + J = J + 2; + if (J == SQUARE) { + ORD = ORD + 1; + SQUARE = P[ORD] * P[ORD]; + MULT[ORD - 1] = J; + } + N = 2; + JPRIME = true; + while (N < ORD && JPRIME) { + while (MULT[N] < J) + MULT[N] = MULT[N] + P[N] + P[N]; + if (MULT[N] == J) + JPRIME = false; + N = N + 1; + } + } while (!JPRIME); + K = K + 1; + P[K] = J; + } + { + PAGENUMBER = 1; + PAGEOFFSET = 1; + while (PAGEOFFSET <= M) { + System.out.println("The First " + M + + " Prime Numbers --- Page " + PAGENUMBER); + System.out.println(""); + for (ROWOFFSET = PAGEOFFSET; ROWOFFSET < PAGEOFFSET + RR; ROWOFFSET++){ + for (C = 0; C < CC;C++) + if (ROWOFFSET + C * RR <= M) + System.out.format("%10d", P[ROWOFFSET + C * RR]); + System.out.println(""); + } + System.out.println("\f"); + PAGENUMBER = PAGENUMBER + 1; + PAGEOFFSET = PAGEOFFSET + RR * CC; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter10/original/Sql.java b/src/main/java/clean/code/chapter10/original/Sql.java new file mode 100644 index 0000000..422559c --- /dev/null +++ b/src/main/java/clean/code/chapter10/original/Sql.java @@ -0,0 +1,37 @@ +package clean.code.chapter10.original; + +import clean.code.added.to.make.code.build.Column; +import clean.code.added.to.make.code.build.Criteria; + +public abstract class Sql { + // public Sql(String table, Column[] columns); + public abstract String create(); + + public abstract String insert(Object[] fields); + + public abstract String selectAll(); + + public abstract String findByKey(String keyColumn, String keyValue); + + public abstract String select(Column column, String pattern); + + public abstract String select(Criteria criteria); + + public abstract String preparedInsert(); + + private String columnList(Column[] columns) { + return ""; + } + + private String valuesList(Object[] fields, final Column[] columns) { + return ""; + } + + private String selectWithCriteria(String criteria) { + return ""; + } + + private String placeholderList(Column[] columns) { + return ""; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter10/refactored/PrimeGenerator.java b/src/main/java/clean/code/chapter10/refactored/PrimeGenerator.java new file mode 100644 index 0000000..37c967a --- /dev/null +++ b/src/main/java/clean/code/chapter10/refactored/PrimeGenerator.java @@ -0,0 +1,70 @@ +package clean.code.chapter10.refactored; + +import java.util.ArrayList; + +public class PrimeGenerator { + private static int[] primes; + private static ArrayList multiplesOfPrimeFactors; + + protected static int[] generate(int n) { + primes = new int[n]; + multiplesOfPrimeFactors = new ArrayList(); + set2AsFirstPrime(); + checkOddNumbersForSubsequentPrimes(); + return primes; + } + + private static void set2AsFirstPrime() { + primes[0] = 2; + multiplesOfPrimeFactors.add(2); + } + + private static void checkOddNumbersForSubsequentPrimes() { + int primeIndex = 1; + for (int candidate = 3; + primeIndex < primes.length; + candidate += 2) { + if (isPrime(candidate)) + primes[primeIndex++] = candidate; + } + } + + private static boolean isPrime(int candidate) { + if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) { + multiplesOfPrimeFactors.add(candidate); + return false; + } + return isNotMultipleOfAnyPreviousPrimeFactor(candidate); + } + + private static boolean + isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) { + int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()]; + int leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor; + return candidate == leastRelevantMultiple; + } + + private static boolean + isNotMultipleOfAnyPreviousPrimeFactor(int candidate) { + for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) { + if (isMultipleOfNthPrimeFactor(candidate, n)) + return false; + } + return true; + } + + private static boolean + isMultipleOfNthPrimeFactor(int candidate, int n) { + return + candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n); + } + + private static int + smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) { + int multiple = multiplesOfPrimeFactors.get(n); + while (multiple < candidate) + multiple += 2 * primes[n]; + multiplesOfPrimeFactors.set(n, multiple); + return multiple; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter10/refactored/PrimePrinter.java b/src/main/java/clean/code/chapter10/refactored/PrimePrinter.java new file mode 100644 index 0000000..dd1d24f --- /dev/null +++ b/src/main/java/clean/code/chapter10/refactored/PrimePrinter.java @@ -0,0 +1,19 @@ +package clean.code.chapter10.refactored; + +public class PrimePrinter { + public static void main(String[] args) { + final int NUMBER_OF_PRIMES = 1000; + int[] primes = PrimeGenerator.generate(NUMBER_OF_PRIMES); + + final int ROWS_PER_PAGE = 50; + final int COLUMNS_PER_PAGE = 4; + RowColumnPagePrinter tablePrinter = + new RowColumnPagePrinter(ROWS_PER_PAGE, + COLUMNS_PER_PAGE, + "The First " + NUMBER_OF_PRIMES + + " Prime Numbers"); + + tablePrinter.print(primes); + } + +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter10/refactored/RowColumnPagePrinter.java b/src/main/java/clean/code/chapter10/refactored/RowColumnPagePrinter.java new file mode 100644 index 0000000..2007f12 --- /dev/null +++ b/src/main/java/clean/code/chapter10/refactored/RowColumnPagePrinter.java @@ -0,0 +1,68 @@ +package clean.code.chapter10.refactored; + +import java.io.PrintStream; + +public class RowColumnPagePrinter { + private int rowsPerPage; + private int columnsPerPage; + private int numbersPerPage; + private String pageHeader; + private PrintStream printStream; + + public RowColumnPagePrinter(int rowsPerPage, + int columnsPerPage, + String pageHeader) { + this.rowsPerPage = rowsPerPage; + this.columnsPerPage = columnsPerPage; + this.pageHeader = pageHeader; + numbersPerPage = rowsPerPage * columnsPerPage; + printStream = System.out; + } + public void print(int data[]) { + int pageNumber = 1; + for (int firstIndexOnPage = 0; + firstIndexOnPage < data.length; + firstIndexOnPage += numbersPerPage) { + int lastIndexOnPage = + Math.min(firstIndexOnPage + numbersPerPage - 1, + data.length - 1); + printPageHeader(pageHeader, pageNumber); + printPage(firstIndexOnPage, lastIndexOnPage, data); + printStream.println("\f"); + pageNumber++; + } + } + + private void printPage(int firstIndexOnPage, + int lastIndexOnPage, + int[] data) { + int firstIndexOfLastRowOnPage = + firstIndexOnPage + rowsPerPage - 1; + for (int firstIndexInRow = firstIndexOnPage; + firstIndexInRow <= firstIndexOfLastRowOnPage; + firstIndexInRow++) { + printRow(firstIndexInRow, lastIndexOnPage, data); + printStream.println(""); + } + } + + private void printRow(int firstIndexInRow, + int lastIndexOnPage, + int[] data) { + for (int column = 0; column < columnsPerPage; column++) { + int index = firstIndexInRow + column * rowsPerPage; + if (index <= lastIndexOnPage) + printStream.format("%10d", data[index]); + } + } + + private void printPageHeader(String pageHeader, + int pageNumber) { + printStream.println(pageHeader + " --- Page " + pageNumber); + printStream.println(""); + } + + public void setOutput(PrintStream printStream) { + this.printStream = printStream; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter10/refactored/Sql.java b/src/main/java/clean/code/chapter10/refactored/Sql.java new file mode 100644 index 0000000..4318e88 --- /dev/null +++ b/src/main/java/clean/code/chapter10/refactored/Sql.java @@ -0,0 +1,59 @@ +package clean.code.chapter10.refactored; + +import clean.code.added.to.make.code.build.Column; +import clean.code.added.to.make.code.build.Criteria; + +abstract public class Sql { + public Sql(String table, Column[] columns) {}; + abstract public String generate(); +} + +class CreateSql extends Sql { + public CreateSql(String table, Column[] columns) { super(table, columns); } + @Override public String generate() { return ""; } +} + +class SelectSql extends Sql { + public SelectSql(String table, Column[] columns) { super(table, columns); } + @Override public String generate() { return ""; } +} + +class InsertSql extends Sql { + public InsertSql(String table, Column[] columns, Object[] fields) { super(table, columns); } + @Override public String generate() { return ""; } + private String valuesList(Object[] fields, final Column[] columns) { return ""; } +} + +class SelectWithCriteriaSql extends Sql { + public SelectWithCriteriaSql( + String table, Column[] columns, Criteria criteria) { super(table, columns); } + @Override public String generate() { return ""; } +} + +class SelectWithMatchSql extends Sql { + public SelectWithMatchSql( + String table, Column[] columns, Column column, String pattern) { super(table, columns); } + @Override public String generate() { return ""; } +} + +class FindByKeySql extends Sql { + public FindByKeySql( + String table, Column[] columns, String keyColumn, String keyValue) { super(table, columns); } + @Override public String generate() { return ""; } +} + +class PreparedInsertSql extends Sql { + public PreparedInsertSql(String table, Column[] columns) { super(table, columns); } + @Override public String generate() {return ""; } + private String placeholderList(Column[] columns) { return ""; } +} + +class Where { + public Where(String criteria) {} + public String generate() { return ""; } +} + +class ColumnList { + public ColumnList(Column[] columns) {} + public String generate() { return ""; } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter11/BankImpl.java b/src/main/java/clean/code/chapter11/BankImpl.java new file mode 100644 index 0000000..63cda38 --- /dev/null +++ b/src/main/java/clean/code/chapter11/BankImpl.java @@ -0,0 +1,84 @@ +package clean.code.chapter11; + +// Bank.java (suppressing package names...) + +import clean.code.added.to.make.code.build.Account; + +import java.lang.reflect.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +// The abstraction of a bank. +interface Bank { + Collection getAccounts(); + + void setAccounts(Collection accounts); +} + +// BankImpl.java + +// The "Plain Old Java Object" (POJO) implementing the abstraction. +public class BankImpl implements Bank { + private List accounts; + + public Collection getAccounts() { + return accounts; + } + + public void setAccounts(Collection accounts) { + this.accounts = new ArrayList(); + for (Account account : accounts) { + this.accounts.add(account); + } + } +} + +// BankProxyHandler.java + +// "InvocationHandler" required by the proxy API. +class BankProxyHandler implements InvocationHandler { + private Bank bank; + + public BankProxyHandler(Bank bank) { + this.bank = bank; + } + + // Method defined in InvocationHandler + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + String methodName = method.getName(); + if (methodName.equals("getAccounts")) { + bank.setAccounts(getAccountsFromDatabase()); + return bank.getAccounts(); + } else if (methodName.equals("setAccounts")) { + bank.setAccounts((Collection) args[0]); + setAccountsToDatabase(bank.getAccounts()); + return null; + } else { + // ... + } + // Dummy to get it to compile + return null; + } + + // Lots of details here: + protected Collection getAccountsFromDatabase() { + // ... + // Dummy to get it to compile + return null; + } + + protected void setAccountsToDatabase(Collection accounts) { + // ... + } + + // Somewhere else... + public static void main(String[] args) { + Bank bank = (Bank) Proxy.newProxyInstance( + Bank.class.getClassLoader(), + new Class[]{Bank.class}, + new BankProxyHandler(new BankImpl()) + ); + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/draft/bool/and/str/Args.java b/src/main/java/clean/code/chapter14/draft/bool/and/str/Args.java new file mode 100644 index 0000000..1ed826b --- /dev/null +++ b/src/main/java/clean/code/chapter14/draft/bool/and/str/Args.java @@ -0,0 +1,201 @@ +package clean.code.chapter14.draft.bool.and.str; + +import java.text.ParseException; +import java.util.*; + +public class Args { + private String schema; + private String[] args; + private boolean valid = true; + private Set unexpectedArguments = new TreeSet(); + private Map booleanArgs = + new HashMap(); + private Map stringArgs = + new HashMap(); + private Set argsFound = new HashSet(); + private int currentArgument; + private char errorArgument = '\0'; + + enum ErrorCode { + OK, MISSING_STRING} + + private ErrorCode errorCode = ErrorCode.OK; + + public Args(String schema, String[] args) throws ParseException { + this.schema = schema; + this.args = args; + valid = parse(); + } + + private boolean parse() throws ParseException { + if (schema.length() == 0 && args.length == 0) + return true; + parseSchema(); + parseArguments(); + return valid; + } + + private boolean parseSchema() throws ParseException { + for (String element : schema.split(",")) { + if (element.length() > 0) { + String trimmedElement = element.trim(); + parseSchemaElement(trimmedElement); + } + } + return true; + } + + private void parseSchemaElement(String element) throws ParseException { + char elementId = element.charAt(0); + String elementTail = element.substring(1); + validateSchemaElementId(elementId); + if (isBooleanSchemaElement(elementTail)) + parseBooleanSchemaElement(elementId); + else if (isStringSchemaElement(elementTail)) + parseStringSchemaElement(elementId); + } + + private void validateSchemaElementId(char elementId) throws ParseException { + if (!Character.isLetter(elementId)) { + throw new ParseException( + "Bad character:" + elementId + "in Args format: " + schema, 0); + } + + } + + private void parseStringSchemaElement(char elementId) { + stringArgs.put(elementId, ""); + } + private boolean isStringSchemaElement(String elementTail) { + return elementTail.equals("*"); + } + + private boolean isBooleanSchemaElement(String elementTail) { + return elementTail.length() == 0; + } + + private void parseBooleanSchemaElement(char elementId) { + booleanArgs.put(elementId, false); + } + + private boolean parseArguments() { + for (currentArgument = 0; currentArgument < args.length; currentArgument++) + { + String arg = args[currentArgument]; + parseArgument(arg); + } + return true; + } + + private void parseArgument(String arg) { + if (arg.startsWith("-")) + parseElements(arg); + } + + private void parseElements(String arg) { + for (int i = 1; i < arg.length(); i++) + parseElement(arg.charAt(i)); + } + + private void parseElement(char argChar) { + if (setArgument(argChar)) + argsFound.add(argChar); + else { + unexpectedArguments.add(argChar); + valid = false; + } + } + + private boolean setArgument(char argChar) { + boolean set = true; + if (isBoolean(argChar)) + setBooleanArg(argChar, true); + else if (isString(argChar)) + setStringArg(argChar, ""); + else + set = false; + + return set; + } + + private void setStringArg(char argChar, String s) { + currentArgument++; + try { + stringArgs.put(argChar, args[currentArgument]); + } catch (ArrayIndexOutOfBoundsException e) { + valid = false; + errorArgument = argChar; + errorCode = ErrorCode.MISSING_STRING; + } + } + + private boolean isString(char argChar) { + return stringArgs.containsKey(argChar); + } + + private void setBooleanArg(char argChar, boolean value) { + booleanArgs.put(argChar, value); + } + + private boolean isBoolean(char argChar) { + return booleanArgs.containsKey(argChar); + } + + public int cardinality() { + return argsFound.size(); + } + + public String usage() { + if (schema.length() > 0) + return "-[" + schema + "]"; + else + return ""; + } + + public String errorMessage() throws Exception { + if (unexpectedArguments.size() > 0) { + return unexpectedArgumentMessage(); + } else + switch (errorCode) { + case MISSING_STRING: + return String.format("Could not find string parameter for -%c.", + errorArgument); + case OK: + throw new Exception("TILT: Should not get here."); + } + return ""; + } + + private String unexpectedArgumentMessage() { + StringBuffer message = new StringBuffer("Argument(s) -"); + for (char c : unexpectedArguments) { + message.append(c); + } + message.append(" unexpected."); + + return message.toString(); + } + public boolean getBoolean(char arg) { + return falseIfNull(booleanArgs.get(arg)); + } + + private boolean falseIfNull(Boolean b) { + return b == null ? false : b; + } + + public String getString(char arg) { + return blankIfNull(stringArgs.get(arg)); + } + + private String blankIfNull(String s) { + return s == null ? "" : s; + } + + public boolean has(char arg) { + return argsFound.contains(arg); + } + + public boolean isValid() { + return valid; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/draft/bool/only/Args.java b/src/main/java/clean/code/chapter14/draft/bool/only/Args.java new file mode 100644 index 0000000..7ac6692 --- /dev/null +++ b/src/main/java/clean/code/chapter14/draft/bool/only/Args.java @@ -0,0 +1,115 @@ +package clean.code.chapter14.draft.bool.only; + +import java.util.*; + +public class Args { + private String schema; + private String[] args; + private boolean valid; + private Set unexpectedArguments = new TreeSet(); + private Map booleanArgs = + new HashMap(); + private int numberOfArguments = 0; + + public Args(String schema, String[] args) { + this.schema = schema; + this.args = args; + valid = parse(); + } + + public boolean isValid() { + return valid; + } + + private boolean parse() { + if (schema.length() == 0 && args.length == 0) + return true; + parseSchema(); + parseArguments(); + return unexpectedArguments.size() == 0; + } + + private boolean parseSchema() { + for (String element : schema.split(",")) { + parseSchemaElement(element); + } + return true; + } + + private void parseSchemaElement(String element) { + if (element.length() == 1) { + parseBooleanSchemaElement(element); + } + } + + private void parseBooleanSchemaElement(String element) { + char c = element.charAt(0); + if (Character.isLetter(c)) { + booleanArgs.put(c, false); + } + } + + private boolean parseArguments() { + for (String arg : args) + parseArgument(arg); + return true; + } + + private void parseArgument(String arg) { + if (arg.startsWith("-")) + parseElements(arg); + } + + private void parseElements(String arg) { + for (int i = 1; i < arg.length(); i++) + parseElement(arg.charAt(i)); + } + + private void parseElement(char argChar) { + if (isBoolean(argChar)) { + numberOfArguments++; + setBooleanArg(argChar, true); + } else + unexpectedArguments.add(argChar); + } + + private void setBooleanArg(char argChar, boolean value) { + booleanArgs.put(argChar, value); + } + + private boolean isBoolean(char argChar) { + return booleanArgs.containsKey(argChar); + } + + public int cardinality() { + return numberOfArguments; + } + + public String usage() { + if (schema.length() > 0) + return "-["+schema+"]"; + else + return ""; + } + + public String errorMessage() { + if (unexpectedArguments.size() > 0) { + return unexpectedArgumentMessage(); + } else + return ""; + } + + private String unexpectedArgumentMessage() { + StringBuffer message = new StringBuffer("Argument(s) -"); + for (char c : unexpectedArguments) { + message.append(c); + } + message.append(" unexpected."); + + return message.toString(); + } + + public boolean getBoolean(char arg) { + return booleanArgs.get(arg); + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/draft/needs/refactoring/Args.java b/src/main/java/clean/code/chapter14/draft/needs/refactoring/Args.java new file mode 100644 index 0000000..11d25ec --- /dev/null +++ b/src/main/java/clean/code/chapter14/draft/needs/refactoring/Args.java @@ -0,0 +1,260 @@ +package clean.code.chapter14.draft.needs.refactoring; + +import java.text.ParseException; +import java.util.*; + +public class Args { + private String schema; + private String[] args; + private boolean valid = true; + private Set unexpectedArguments = new TreeSet(); + private Map booleanArgs = + new HashMap(); + private Map stringArgs = new HashMap(); + private Map intArgs = new HashMap(); + private Set argsFound = new HashSet(); + private int currentArgument; + private char errorArgumentId = '\0'; + private String errorParameter = "TILT"; + private ErrorCode errorCode = ErrorCode.OK; + + private enum ErrorCode { + OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT} + + public Args(String schema, String[] args) throws ParseException { + this.schema = schema; + this.args = args; + valid = parse(); + } + + private boolean parse() throws ParseException { + if (schema.length() == 0 && args.length == 0) + return true; + parseSchema(); + try { + parseArguments(); + } catch (ArgsException e) { + } + return valid; + } + + private boolean parseSchema() throws ParseException { + for (String element : schema.split(",")) { + if (element.length() > 0) { + String trimmedElement = element.trim(); + parseSchemaElement(trimmedElement); + } + } + return true; + } + + private void parseSchemaElement(String element) throws ParseException { + char elementId = element.charAt(0); + String elementTail = element.substring(1); + validateSchemaElementId(elementId); + if (isBooleanSchemaElement(elementTail)) + parseBooleanSchemaElement(elementId); + else if (isStringSchemaElement(elementTail)) + parseStringSchemaElement(elementId); + else if (isIntegerSchemaElement(elementTail)) { + parseIntegerSchemaElement(elementId); + } else { + throw new ParseException( + String.format("Argument: %c has invalid format: %s.", + elementId, elementTail), 0); + } + } + + private void validateSchemaElementId(char elementId) throws ParseException { + if (!Character.isLetter(elementId)) { + throw new ParseException( + "Bad character:" + elementId + "in Args format: " + schema, 0); + } + } + + private void parseBooleanSchemaElement(char elementId) { + booleanArgs.put(elementId, false); + } + + private void parseIntegerSchemaElement(char elementId) { + intArgs.put(elementId, 0); + } + + private void parseStringSchemaElement(char elementId) { + stringArgs.put(elementId, ""); + } + + private boolean isStringSchemaElement(String elementTail) { + return elementTail.equals("*"); + } + + private boolean isBooleanSchemaElement(String elementTail) { + return elementTail.length() == 0; + } + + private boolean isIntegerSchemaElement(String elementTail) { + return elementTail.equals("#"); + } + private boolean parseArguments() throws ArgsException { + for (currentArgument = 0; currentArgument < args.length; currentArgument++) + { + String arg = args[currentArgument]; + parseArgument(arg); + } + return true; + } + + private void parseArgument(String arg) throws ArgsException { + if (arg.startsWith("-")) + parseElements(arg); + } + + private void parseElements(String arg) throws ArgsException { + for (int i = 1; i < arg.length(); i++) + parseElement(arg.charAt(i)); + } + + private void parseElement(char argChar) throws ArgsException { + if (setArgument(argChar)) + argsFound.add(argChar); + else { + unexpectedArguments.add(argChar); + errorCode = ErrorCode.UNEXPECTED_ARGUMENT; + valid = false; + } + } + + private boolean setArgument(char argChar) throws ArgsException { + if (isBooleanArg(argChar)) + setBooleanArg(argChar, true); + else if (isStringArg(argChar)) + setStringArg(argChar); + else if (isIntArg(argChar)) + setIntArg(argChar); + else + return false; + + return true; + } + + private boolean isIntArg(char argChar) {return intArgs.containsKey(argChar);} + + private void setIntArg(char argChar) throws ArgsException { + currentArgument++; + String parameter = null; + try { + parameter = args[currentArgument]; + intArgs.put(argChar, new Integer(parameter)); + } catch (ArrayIndexOutOfBoundsException e) { + valid = false; + errorArgumentId = argChar; + errorCode = ErrorCode.MISSING_INTEGER; + throw new ArgsException(); + } catch (NumberFormatException e) { + valid = false; + errorArgumentId = argChar; + errorParameter = parameter; + errorCode = ErrorCode.INVALID_INTEGER; + throw new ArgsException(); + } + } + + private void setStringArg(char argChar) throws ArgsException { + currentArgument++; + try { + stringArgs.put(argChar, args[currentArgument]); + } catch (ArrayIndexOutOfBoundsException e) { + valid = false; + errorArgumentId = argChar; + errorCode = ErrorCode.MISSING_STRING; + throw new ArgsException(); + } + } + + private boolean isStringArg(char argChar) { + return stringArgs.containsKey(argChar); + } + + private void setBooleanArg(char argChar, boolean value) { + booleanArgs.put(argChar, value); + } + + private boolean isBooleanArg(char argChar) { + return booleanArgs.containsKey(argChar); + } + + public int cardinality() { + return argsFound.size(); + } + + public String usage() { + if (schema.length() > 0) + return "-[" + schema + "]"; + else + return ""; + } + + public String errorMessage() throws Exception { + switch (errorCode) { + case OK: + throw new Exception("TILT: Should not get here."); + case UNEXPECTED_ARGUMENT: + return unexpectedArgumentMessage(); + case MISSING_STRING: + return String.format("Could not find string parameter for -%c.", + errorArgumentId); + case INVALID_INTEGER: + return String.format("Argument -%c expects an integer but was '%s'.", + errorArgumentId, errorParameter); + case MISSING_INTEGER: + return String.format("Could not find integer parameter for -%c.", + errorArgumentId); + } + return ""; + } + + private String unexpectedArgumentMessage() { + StringBuffer message = new StringBuffer("Argument(s) -"); + for (char c : unexpectedArguments) { + message.append(c); + } + message.append(" unexpected."); + + return message.toString(); + } + + private boolean falseIfNull(Boolean b) { + return b != null && b; + } + + private int zeroIfNull(Integer i) { + return i == null ? 0 : i; + } + + private String blankIfNull(String s) { + return s == null ? "" : s; + } + + public String getString(char arg) { + return blankIfNull(stringArgs.get(arg)); + } + + public int getInt(char arg) { + return zeroIfNull(intArgs.get(arg)); + } + + public boolean getBoolean(char arg) { + return falseIfNull(booleanArgs.get(arg)); + } + + public boolean has(char arg) { + return argsFound.contains(arg); + } + + public boolean isValid() { + return valid; + } + + private class ArgsException extends Exception { + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/refactored/first/Args.java b/src/main/java/clean/code/chapter14/refactored/first/Args.java new file mode 100644 index 0000000..b404a61 --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/first/Args.java @@ -0,0 +1,285 @@ +package clean.code.chapter14.refactored.first; + +import java.text.ParseException; +import java.util.*; + +public class Args { + private String schema; + private String[] args; + private boolean valid = true; + private Set unexpectedArguments = new TreeSet(); + private Map marshalers = + new HashMap(); + private Set argsFound = new HashSet(); + private int currentArgument; + private char errorArgumentId = '\0'; + private String errorParameter = "TILT"; + private ErrorCode errorCode = ErrorCode.OK; + + private enum ErrorCode { + OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT} + + public Args(String schema, String[] args) throws ParseException { + this.schema = schema; + this.args = args; + valid = parse(); + } + + private boolean parse() throws ParseException { + if (schema.length() == 0 && args.length == 0) + return true; + parseSchema(); + try { + parseArguments(); + } catch (ArgsException e) { + } + return valid; + } + + private boolean parseSchema() throws ParseException { + for (String element : schema.split(",")) { + if (element.length() > 0) { + String trimmedElement = element.trim(); + parseSchemaElement(trimmedElement); + } + } + return true; + } + + private void parseSchemaElement(String element) throws ParseException { + char elementId = element.charAt(0); + String elementTail = element.substring(1); + validateSchemaElementId(elementId); + if (isBooleanSchemaElement(elementTail)) + marshalers.put(elementId, new BooleanArgumentMarshaler()); + else if (isStringSchemaElement(elementTail)) + marshalers.put(elementId, new StringArgumentMarshaler()); + else if (isIntegerSchemaElement(elementTail)) { + marshalers.put(elementId, new IntegerArgumentMarshaler()); + } else { + throw new ParseException(String.format( + "Argument: %c has invalid format: %s.", elementId, elementTail), 0); + } + } + + private void validateSchemaElementId(char elementId) throws ParseException { + if (!Character.isLetter(elementId)) { + throw new ParseException( + "Bad character:" + elementId + "in Args format: " + schema, 0); + } + } + + private boolean isStringSchemaElement(String elementTail) { + return elementTail.equals("*"); + } + + private boolean isBooleanSchemaElement(String elementTail) { + return elementTail.length() == 0; + } + + private boolean isIntegerSchemaElement(String elementTail) { + return elementTail.equals("#"); + } + + private boolean parseArguments() throws ArgsException { + for (currentArgument=0; currentArgument 0) + return "-[" + schema + "]"; + else + return ""; + } + + public String errorMessage() throws Exception { + switch (errorCode) { + case OK: + throw new Exception("TILT: Should not get here."); + case UNEXPECTED_ARGUMENT: + return unexpectedArgumentMessage(); + case MISSING_STRING: + return String.format("Could not find string parameter for -%c.", + errorArgumentId); + case INVALID_INTEGER: + return String.format("Argument -%c expects an integer but was '%s'.", + errorArgumentId, errorParameter); + case MISSING_INTEGER: + return String.format("Could not find integer parameter for -%c.", + errorArgumentId); + } + return ""; + } + + private String unexpectedArgumentMessage() { + StringBuffer message = new StringBuffer("Argument(s) -"); + for (char c : unexpectedArguments) { + message.append(c); + } + message.append(" unexpected."); + + return message.toString(); + } + + public boolean getBoolean(char arg) { + Args.ArgumentMarshaler am = marshalers.get(arg); + boolean b = false; + try { + b = am != null && (Boolean) am.get(); + } catch (ClassCastException e) { + b = false; + } + return b; + } + + public String getString(char arg) { + Args.ArgumentMarshaler am = marshalers.get(arg); + try { + return am == null ? "" : (String) am.get(); + } catch (ClassCastException e) { + return ""; + } + } + public int getInt(char arg) { + Args.ArgumentMarshaler am = marshalers.get(arg); + try { + return am == null ? 0 : (Integer) am.get(); + } catch (Exception e) { + return 0; + } + } + + public boolean has(char arg) { + return argsFound.contains(arg); + } + + public boolean isValid() { + return valid; + } + + private class ArgsException extends Exception { + } + + private abstract class ArgumentMarshaler { + public abstract void set(String s) throws ArgsException; + public abstract Object get(); + } + + private class BooleanArgumentMarshaler extends ArgumentMarshaler { + private boolean booleanValue = false; + + public void set(String s) { + booleanValue = true; + } + + public Object get() { + return booleanValue; + } + } + + private class StringArgumentMarshaler extends ArgumentMarshaler { + private String stringValue = ""; + + public void set(String s) { + stringValue = s; + } + + public Object get() { + return stringValue; + } + } + + private class IntegerArgumentMarshaler extends ArgumentMarshaler { + private int intValue = 0; + + public void set(String s) throws ArgsException { + try { + intValue = Integer.parseInt(s); + } catch (NumberFormatException e) { + throw new ArgsException(); + } + } + + public Object get() { + return intValue; + } + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/refactored/second/Args.java b/src/main/java/clean/code/chapter14/refactored/second/Args.java new file mode 100644 index 0000000..c3e2d82 --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/second/Args.java @@ -0,0 +1,148 @@ +package clean.code.chapter14.refactored.second; + +import java.util.*; + +public class Args { + private String schema; + private Map marshalers = + new HashMap(); + private Set argsFound = new HashSet(); + private Iterator currentArgument; + private List argsList; + + public Args(String schema, String[] args) throws ArgsException { + this.schema = schema; + argsList = Arrays.asList(args); + parse(); + } + + private void parse() throws ArgsException { + parseSchema(); + parseArguments(); + } + + private boolean parseSchema() throws ArgsException { + for (String element : schema.split(",")) { + if (element.length() > 0) { + parseSchemaElement(element.trim()); + } + } + return true; + } + + private void parseSchemaElement(String element) throws ArgsException { + char elementId = element.charAt(0); + String elementTail = element.substring(1); + validateSchemaElementId(elementId); + if (elementTail.length() == 0) + marshalers.put(elementId, new BooleanArgumentMarshaler()); + else if (elementTail.equals("*")) + marshalers.put(elementId, new StringArgumentMarshaler()); + else if (elementTail.equals("#")) + marshalers.put(elementId, new IntegerArgumentMarshaler()); + else if (elementTail.equals("##")) + marshalers.put(elementId, new DoubleArgumentMarshaler()); + else + throw new ArgsException(ArgsException.ErrorCode.INVALID_FORMAT, + elementId, elementTail); + } + + private void validateSchemaElementId(char elementId) throws ArgsException { + if (!Character.isLetter(elementId)) { + throw new ArgsException(ArgsException.ErrorCode.INVALID_ARGUMENT_NAME, + elementId, null); + } + } + + private void parseArguments() throws ArgsException { + for (currentArgument = argsList.iterator(); currentArgument.hasNext();) { + String arg = currentArgument.next(); + parseArgument(arg); + } + } + + private void parseArgument(String arg) throws ArgsException { + if (arg.startsWith("-")) + parseElements(arg); + } + + private void parseElements(String arg) throws ArgsException { + for (int i = 1; i < arg.length(); i++) + parseElement(arg.charAt(i)); + } + + private void parseElement(char argChar) throws ArgsException { + if (setArgument(argChar)) + argsFound.add(argChar); + else { + throw new ArgsException(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, + argChar, null); + } + } + + private boolean setArgument(char argChar) throws ArgsException { + ArgumentMarshaler m = marshalers.get(argChar); + if (m == null) + return false; + try { + m.set(currentArgument); + return true; + } catch (ArgsException e) { + e.setErrorArgumentId(argChar); + throw e; + } + } + public int cardinality() { + return argsFound.size(); + } + + public String usage() { + if (schema.length() > 0) + return "-[" + schema + "]"; + else + return ""; + } + + public boolean getBoolean(char arg) { + ArgumentMarshaler am = marshalers.get(arg); + boolean b = false; + try { + b = am != null && (Boolean) am.get(); + } catch (ClassCastException e) { + b = false; + } + return b; + } + + public String getString(char arg) { + ArgumentMarshaler am = marshalers.get(arg); + try { + return am == null ? "" : (String) am.get(); + } catch (ClassCastException e) { + return ""; + } + } + + public int getInt(char arg) { + ArgumentMarshaler am = marshalers.get(arg); + try { + return am == null ? 0 : (Integer) am.get(); + } catch (Exception e) { + return 0; + } + } + + + public double getDouble(char arg) { + ArgumentMarshaler am = marshalers.get(arg); + try { + return am == null ? 0 : (Double) am.get(); + } catch (Exception e) { + return 0.0; + } + } + + public boolean has(char arg) { + return argsFound.contains(arg); + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/refactored/second/ArgsException.java b/src/main/java/clean/code/chapter14/refactored/second/ArgsException.java new file mode 100644 index 0000000..4da03dd --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/second/ArgsException.java @@ -0,0 +1,81 @@ +package clean.code.chapter14.refactored.second; + +public class ArgsException extends Exception { + private char errorArgumentId = '\0'; + private String errorParameter = "TILT"; + private ErrorCode errorCode = ErrorCode.OK; + + public ArgsException() {} + + public ArgsException(String message) {super(message);} + + public ArgsException(ErrorCode errorCode) { + this.errorCode = errorCode; + } + public ArgsException(ErrorCode errorCode, String errorParameter) { + this.errorCode = errorCode; + this.errorParameter = errorParameter; + } + + public ArgsException(ErrorCode errorCode, char errorArgumentId, + String errorParameter) { + this.errorCode = errorCode; + this.errorParameter = errorParameter; + this.errorArgumentId = errorArgumentId; + } + + public char getErrorArgumentId() { + return errorArgumentId; + } + + public void setErrorArgumentId(char errorArgumentId) { + this.errorArgumentId = errorArgumentId; + } + + public String getErrorParameter() { + return errorParameter; + } + + public void setErrorParameter(String errorParameter) { + this.errorParameter = errorParameter; + } + + public ErrorCode getErrorCode() { + return errorCode; + } + + public void setErrorCode(ErrorCode errorCode) { + this.errorCode = errorCode; + } + + public String errorMessage() throws Exception { + switch (errorCode) { + case OK: + throw new Exception("TILT: Should not get here."); + case UNEXPECTED_ARGUMENT: + return String.format("Argument -%c unexpected.", errorArgumentId); + case MISSING_STRING: + return String.format("Could not find string parameter for -%c.", + errorArgumentId); + case INVALID_INTEGER: + return String.format("Argument -%c expects an integer but was '%s'.", + errorArgumentId, errorParameter); + case MISSING_INTEGER: + return String.format("Could not find integer parameter for -%c.", + errorArgumentId); + case INVALID_DOUBLE: + return String.format("Argument -%c expects a double but was '%s'.", + errorArgumentId, errorParameter); + case MISSING_DOUBLE: + return String.format("Could not find double parameter for -%c.", + errorArgumentId); + } + return ""; + } + + public enum ErrorCode { + OK, INVALID_FORMAT, UNEXPECTED_ARGUMENT, INVALID_ARGUMENT_NAME, + MISSING_STRING, + MISSING_INTEGER, INVALID_INTEGER, + MISSING_DOUBLE, MISSING_BOOLEAN, INVALID_BOOLEAN, INVALID_DOUBLE} +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/refactored/second/ArgumentMarshaler.java b/src/main/java/clean/code/chapter14/refactored/second/ArgumentMarshaler.java new file mode 100644 index 0000000..f3bde1e --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/second/ArgumentMarshaler.java @@ -0,0 +1,8 @@ +package clean.code.chapter14.refactored.second; + +import java.util.Iterator; + +public interface ArgumentMarshaler { + void set(Iterator currentArgument) throws ArgsException; + Object get(); +} diff --git a/src/main/java/clean/code/chapter14/refactored/second/BooleanArgumentMarshaler.java b/src/main/java/clean/code/chapter14/refactored/second/BooleanArgumentMarshaler.java new file mode 100644 index 0000000..f5f71c5 --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/second/BooleanArgumentMarshaler.java @@ -0,0 +1,27 @@ +package clean.code.chapter14.refactored.second; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import static clean.code.chapter14.refactored.second.ArgsException.ErrorCode.INVALID_BOOLEAN; +import static clean.code.chapter14.refactored.second.ArgsException.ErrorCode.MISSING_BOOLEAN; + +public class BooleanArgumentMarshaler implements ArgumentMarshaler { + private boolean booleanValue = false; + + public void set(Iterator currentArgument) throws ArgsException { + String parameter = null; + try { + parameter = currentArgument.next(); + booleanValue = Boolean.parseBoolean(parameter); + } catch (NoSuchElementException e) { + throw new ArgsException(MISSING_BOOLEAN); + } catch (NumberFormatException e) { + throw new ArgsException(INVALID_BOOLEAN, parameter); + } + } + + public Object get() { + return booleanValue; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/refactored/second/DoubleArgumentMarshaler.java b/src/main/java/clean/code/chapter14/refactored/second/DoubleArgumentMarshaler.java new file mode 100644 index 0000000..d95cb42 --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/second/DoubleArgumentMarshaler.java @@ -0,0 +1,28 @@ +package clean.code.chapter14.refactored.second; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import static clean.code.chapter14.refactored.second.ArgsException.ErrorCode.INVALID_DOUBLE; +import static clean.code.chapter14.refactored.second.ArgsException.ErrorCode.MISSING_DOUBLE; + +public class DoubleArgumentMarshaler implements ArgumentMarshaler { + + private double doubleValue = 0; + + public void set(Iterator currentArgument) throws ArgsException { + String parameter = null; + try { + parameter = currentArgument.next(); + doubleValue = Double.parseDouble(parameter); + } catch (NoSuchElementException e) { + throw new ArgsException(MISSING_DOUBLE); + } catch (NumberFormatException e) { + throw new ArgsException(INVALID_DOUBLE, parameter); + } + } + + public Object get() { + return doubleValue; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/refactored/second/IntegerArgumentMarshaler.java b/src/main/java/clean/code/chapter14/refactored/second/IntegerArgumentMarshaler.java new file mode 100644 index 0000000..7cf3a80 --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/second/IntegerArgumentMarshaler.java @@ -0,0 +1,27 @@ +package clean.code.chapter14.refactored.second; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import static clean.code.chapter14.refactored.second.ArgsException.ErrorCode.INVALID_INTEGER; +import static clean.code.chapter14.refactored.second.ArgsException.ErrorCode.MISSING_INTEGER; + +public class IntegerArgumentMarshaler implements ArgumentMarshaler { + private int intValue = 0; + + public void set(Iterator currentArgument) throws ArgsException { + String parameter = null; + try { + parameter = currentArgument.next(); + intValue = Integer.parseInt(parameter); + } catch (NoSuchElementException e) { + throw new ArgsException(MISSING_INTEGER); + } catch (NumberFormatException e) { + throw new ArgsException(INVALID_INTEGER, parameter); + } + } + + public Object get() { + return intValue; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/refactored/second/StringArgumentMarshaler.java b/src/main/java/clean/code/chapter14/refactored/second/StringArgumentMarshaler.java new file mode 100644 index 0000000..2f91ba4 --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/second/StringArgumentMarshaler.java @@ -0,0 +1,22 @@ +package clean.code.chapter14.refactored.second; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import static clean.code.chapter14.refactored.second.ArgsException.ErrorCode.MISSING_STRING; + +public class StringArgumentMarshaler implements ArgumentMarshaler { + private String stringValue = ""; + + public void set(Iterator currentArgument) throws ArgsException { + try { + stringValue = currentArgument.next(); + } catch (NoSuchElementException e) { + throw new ArgsException(MISSING_STRING); + } + } + + public Object get() { + return stringValue; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/refactored/step/by/step/Args.java b/src/main/java/clean/code/chapter14/refactored/step/by/step/Args.java new file mode 100644 index 0000000..451244c --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/step/by/step/Args.java @@ -0,0 +1,109 @@ +package clean.code.chapter14.refactored.step.by.step; + +import java.util.*; + +import static clean.code.chapter14.refactored.step.by.step.ArgsException.ErrorCode.*; + +public class Args { + private Map marshallers = + new HashMap(); + private Set argsFound = new HashSet(); + private int noOfArguments = 0; + private Iterator currentArgument; + + public Args(String schema, String[] args) throws ArgsException { + parseSchema(schema); + parseArgumentStrings(Arrays.asList(args)); + } + + private void parseSchema(String schema) throws ArgsException { + for (String element : schema.split(",")) { + if (element.length() > 0) { + parseSchemaElement(element.trim()); + } + } + } + + private void parseSchemaElement(String element) throws ArgsException { + char elementId = element.charAt(0); + String elementTail = element.substring(1); + validateSchemaElementId(elementId); + + if (elementTail.length() == 0) + marshallers.put(elementId, new BooleanArgumentMarshaller()); + else if (elementTail.equals("*")) + marshallers.put(elementId, new StringArgumentMarshaller()); + else if (elementTail.equals("#")) { + marshallers.put(elementId, new IntegerArgumentMarshaller()); + } else if (elementTail.equals("##")) { + marshallers.put(elementId, new DoubleArgumentMarshaller()); + } else { + throw new ArgsException(INVALID_ARGUMENT_FORMAT, elementId, elementTail); + } + } + + private void validateSchemaElementId(char elementId) throws ArgsException { + if (!Character.isLetter(elementId)) { + throw new ArgsException(INVALID_ARGUMENT_NAME, elementId); + } + } + + private void parseArgumentStrings(List argsList) throws ArgsException { + for (currentArgument = argsList.iterator(); currentArgument.hasNext(); ) { + String arg = currentArgument.next(); + if (arg.startsWith("-")) { + noOfArguments = ++noOfArguments; + parseArgumentCharacters(arg.substring(1)); + } else { + break; + } + } + if ((marshallers.size() > 0) && (noOfArguments == 0)) { + throw new ArgsException(INVALID_ARGUMENT_FORMAT, '-'); + } + } + + private void parseArgumentCharacters(String arg) throws ArgsException { + for (int i = 0; i < arg.length(); i++) { + parseArgumentCharacter(arg.charAt(i)); + } + } + + private void parseArgumentCharacter(final char argChar) throws ArgsException { + ArgumentMarshaller m = marshallers.get(argChar); + if (m == null) { + throw new ArgsException(UNEXPECTED_ARGUMENT, argChar); + } + argsFound.add(argChar); + try { + m.set(currentArgument); + } catch (ArgsException e) { + e.setErrorArgumentId(argChar); + throw e; + } + } + + public int cardinality() { + return argsFound.size(); + } + + public String getString(char arg) { + return StringArgumentMarshaller.getValue(marshallers.get(arg)); + } + + public int getInt(char arg) { + return IntegerArgumentMarshaller.getValue(marshallers.get(arg)); + } + + public boolean getBoolean(char arg) { + return BooleanArgumentMarshaller.getValue(marshallers.get(arg)); + } + + public double getDouble(char arg) { + return DoubleArgumentMarshaller.getValue(marshallers.get(arg)); + } + + public boolean has(char arg) { + return argsFound.contains(arg); + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/refactored/step/by/step/ArgsException.java b/src/main/java/clean/code/chapter14/refactored/step/by/step/ArgsException.java new file mode 100644 index 0000000..0f27520 --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/step/by/step/ArgsException.java @@ -0,0 +1,72 @@ +package clean.code.chapter14.refactored.step.by.step; + +public class ArgsException extends Exception { + private char errorArgumentId = '\0'; + + private String errorParameter = "TILT"; + private ErrorCode errorCode = ErrorCode.OK; + public ArgsException(ErrorCode errorCode) { + this.errorCode = errorCode; + } + + public ArgsException(ErrorCode errorCode, char errorArgumentId) { + this(errorCode); + this.errorArgumentId = errorArgumentId; + } + + public ArgsException(ErrorCode errorCode, String errorParameter) { + this(errorCode); + this.errorParameter = errorParameter; + } + + public ArgsException(ErrorCode errorCode, char errorArgumentId, String errorParameter) { + this(errorCode, errorArgumentId); + this.errorParameter = errorParameter; + } + + public ErrorCode getErrorCode() { + return errorCode; + } + + public void setErrorArgumentId(char errorArgumentId) { + this.errorArgumentId = errorArgumentId; + } + + public char getErrorArgumentId() { + return errorArgumentId; + } + + public String getErrorParameter() { + return errorParameter; + } + + public enum ErrorCode { + OK, MISSING_STRING, MISSING_INTEGER, MISSING_BOOLEAN, + INVALID_INTEGER, MISSING_DOUBLE, INVALID_DOUBLE, + UNEXPECTED_ARGUMENT, INVALID_ARGUMENT_NAME, INVALID_ARGUMENT_FORMAT; + } + + public String errorMessage() throws Exception { + switch (errorCode) { + case OK: + throw new Exception("TILT: Should not get here."); + case UNEXPECTED_ARGUMENT: + return String.format("Argument -%s unexpected.", errorArgumentId); + case INVALID_ARGUMENT_NAME: + return String.format("Bad character:%s in Args format", errorArgumentId); + case MISSING_STRING: + return String.format("Could not find string parameter for -%c.", errorArgumentId); + case MISSING_INTEGER: + return String.format("Could not find integer parameter for -%c.", errorArgumentId); + case INVALID_INTEGER: + return String.format("Argument -%c expects an integer but was '%s'.", errorArgumentId, errorParameter); + case MISSING_BOOLEAN: + return String.format("Could not find boolean parameter for -%c.", errorArgumentId); + case MISSING_DOUBLE: + return String.format("Could not find double parameter for -%c.", errorArgumentId); + case INVALID_DOUBLE: + return String.format("Argument -%c expects a double but was '%s'.", errorArgumentId, errorParameter); + } + return ""; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/refactored/step/by/step/ArgumentMarshaller.java b/src/main/java/clean/code/chapter14/refactored/step/by/step/ArgumentMarshaller.java new file mode 100644 index 0000000..1522009 --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/step/by/step/ArgumentMarshaller.java @@ -0,0 +1,7 @@ +package clean.code.chapter14.refactored.step.by.step; + +import java.util.Iterator; + +public interface ArgumentMarshaller { + void set(Iterator currentArgument) throws ArgsException; +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/refactored/step/by/step/BooleanArgumentMarshaller.java b/src/main/java/clean/code/chapter14/refactored/step/by/step/BooleanArgumentMarshaller.java new file mode 100644 index 0000000..c2fdbc6 --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/step/by/step/BooleanArgumentMarshaller.java @@ -0,0 +1,25 @@ +package clean.code.chapter14.refactored.step.by.step; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +public class BooleanArgumentMarshaller implements ArgumentMarshaller { + private boolean booleanValue = false; + + @Override + public void set(Iterator currentArgument) throws ArgsException { + try { + booleanValue = new Boolean(currentArgument.next()); + } catch (NoSuchElementException e) { + throw new ArgsException(ArgsException.ErrorCode.MISSING_BOOLEAN); + } + } + + public static boolean getValue(ArgumentMarshaller am) { + if ((am != null) && am instanceof BooleanArgumentMarshaller) { + return ((BooleanArgumentMarshaller) am).booleanValue; + } else { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/refactored/step/by/step/DoubleArgumentMarshaller.java b/src/main/java/clean/code/chapter14/refactored/step/by/step/DoubleArgumentMarshaller.java new file mode 100644 index 0000000..19a6c1f --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/step/by/step/DoubleArgumentMarshaller.java @@ -0,0 +1,29 @@ +package clean.code.chapter14.refactored.step.by.step; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +public class DoubleArgumentMarshaller implements ArgumentMarshaller { + private double doubleValue = 0; + + @Override + public void set(Iterator currentArgument) throws ArgsException { + String parameter = null; + try { + parameter = currentArgument.next(); + doubleValue = Double.parseDouble(parameter); + } catch (NoSuchElementException e) { + throw new ArgsException(ArgsException.ErrorCode.MISSING_DOUBLE); + } catch (NumberFormatException e) { + throw new ArgsException(ArgsException.ErrorCode.INVALID_DOUBLE, parameter); + } + } + + public static double getValue(ArgumentMarshaller am) { + if ((am != null) && am instanceof DoubleArgumentMarshaller) { + return ((DoubleArgumentMarshaller) am).doubleValue; + } else { + return 0.0; + } + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/refactored/step/by/step/IntegerArgumentMarshaller.java b/src/main/java/clean/code/chapter14/refactored/step/by/step/IntegerArgumentMarshaller.java new file mode 100644 index 0000000..e66ca7b --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/step/by/step/IntegerArgumentMarshaller.java @@ -0,0 +1,29 @@ +package clean.code.chapter14.refactored.step.by.step; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +public class IntegerArgumentMarshaller implements ArgumentMarshaller { + private Integer intValue; + + @Override + public void set(Iterator currentArgument) throws ArgsException { + String parameter = null; + try { + parameter = currentArgument.next(); + intValue = new Integer(parameter); + } catch (NoSuchElementException e) { + throw new ArgsException(ArgsException.ErrorCode.MISSING_INTEGER); + } catch (NumberFormatException e) { + throw new ArgsException(ArgsException.ErrorCode.INVALID_INTEGER, parameter); + } + } + + public static int getValue(ArgumentMarshaller am) { + if ((am != null) && am instanceof IntegerArgumentMarshaller) { + return ((IntegerArgumentMarshaller) am).intValue; + } else { + return 0; + } + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/refactored/step/by/step/StringArgumentMarshaller.java b/src/main/java/clean/code/chapter14/refactored/step/by/step/StringArgumentMarshaller.java new file mode 100644 index 0000000..13cf429 --- /dev/null +++ b/src/main/java/clean/code/chapter14/refactored/step/by/step/StringArgumentMarshaller.java @@ -0,0 +1,25 @@ +package clean.code.chapter14.refactored.step.by.step; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +public class StringArgumentMarshaller implements ArgumentMarshaller { + private String stringValue; + + @Override + public void set(Iterator currentArgument) throws ArgsException { + try { + stringValue = currentArgument.next(); + } catch (NoSuchElementException e) { + throw new ArgsException(ArgsException.ErrorCode.MISSING_STRING); + } + } + + public static String getValue(ArgumentMarshaller am) { + if ((am != null) && am instanceof StringArgumentMarshaller) { + return ((StringArgumentMarshaller) am).stringValue; + } else { + return ""; + } + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/solution/Args.java b/src/main/java/clean/code/chapter14/solution/Args.java new file mode 100644 index 0000000..49d50d3 --- /dev/null +++ b/src/main/java/clean/code/chapter14/solution/Args.java @@ -0,0 +1,127 @@ +package clean.code.chapter14.solution; + +import static clean.code.chapter14.solution.ArgsException.ErrorCode.INVALID_ARGUMENT_FORMAT; +import static clean.code.chapter14.solution.ArgsException.ErrorCode.INVALID_ARGUMENT_NAME; +import static clean.code.chapter14.solution.ArgsException.ErrorCode.UNEXPECTED_ARGUMENT; + +import java.util.*; + +public class Args { + private Map marshalers; + private Set argsFound; + private ListIterator currentArgument; + + public Args(String schema, String[] args) throws ArgsException { + marshalers = new HashMap(); + argsFound = new HashSet(); + + parseSchema(schema); + parseArgumentStrings(Arrays.asList(args)); + } + + private void parseSchema(String schema) throws ArgsException { + for (String element : schema.split(",")) + if (element.length() > 0) + parseSchemaElement(element.trim()); + } + + private void parseSchemaElement(String element) throws ArgsException { + char elementId = element.charAt(0); + String elementTail = element.substring(1); + validateSchemaElementId(elementId); + if (elementTail.length() == 0) + marshalers.put(elementId, new BooleanArgumentMarshaler()); + else if (elementTail.equals("*")) + marshalers.put(elementId, new StringArgumentMarshaler()); + else if (elementTail.equals("#")) + marshalers.put(elementId, new IntegerArgumentMarshaler()); + else if (elementTail.equals("##")) + marshalers.put(elementId, new DoubleArgumentMarshaler()); + else if (elementTail.equals("[*]")) + marshalers.put(elementId, new StringArrayArgumentMarshaler()); + else + throw new ArgsException(INVALID_ARGUMENT_FORMAT, elementId, elementTail); + } + + private void validateSchemaElementId(char elementId) throws ArgsException { + if (!Character.isLetter(elementId)) + throw new ArgsException(INVALID_ARGUMENT_NAME, elementId, null); + } + + private void parseArgumentStrings(List argsList) throws ArgsException { + if (argsList.size() > 0) { + currentArgument = argsList.listIterator(); + String argString = currentArgument.next(); + if (argString.startsWith("-")) { + parseArgumentCharacters(argString.substring(1)); + } else { + throw new ArgsException(INVALID_ARGUMENT_FORMAT, '-', ""); + } + } + } + + private void parseArgumentCharacters(String argChars) throws ArgsException { + for (int i = 0; i < argChars.length(); i++) + parseArgumentCharacter(argChars.charAt(i)); + } + + private void parseArgumentCharacter(char argChar) throws ArgsException { + ArgumentMarshaler m = marshalers.get(argChar); + if (m == null) { + throw new ArgsException(UNEXPECTED_ARGUMENT, argChar, null); + } else { + argsFound.add(argChar); + try { + m.set(currentArgument); + } catch (ArgsException e) { + e.setErrorArgumentId(argChar); + throw e; + } + } + } + + public boolean has(char arg) { + return argsFound.contains(arg); + } + + public int cardinality() { + return argsFound.size(); + } + + public int nextArgument() { + return currentArgument.nextIndex(); + } + + public boolean getBoolean(char arg) { + return BooleanArgumentMarshaler.getValue(marshalers.get(arg)); + } + + public String getString(char arg) { + return StringArgumentMarshaler.getValue(marshalers.get(arg)); + } + + public int getInt(char arg) { + return IntegerArgumentMarshaler.getValue(marshalers.get(arg)); + } + + public double getDouble(char arg) { + return DoubleArgumentMarshaler.getValue(marshalers.get(arg)); + } + + public String[] getStringArray(char arg) { + return StringArrayArgumentMarshaler.getValue(marshalers.get(arg)); + } + + public static void main(String[] args) { + try { + Args arg = new Args("l,p#,d*", args); + boolean logging = arg.getBoolean('l'); + int port = arg.getInt('p'); + String directory = arg.getString('d'); + + System.out.println(String.format("logging [%b] port [%d] directory [%s]", logging, port, directory)); + } catch (ArgsException e) { + System.out.printf("Argument error: %s\n", e.errorMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/solution/ArgsException.java b/src/main/java/clean/code/chapter14/solution/ArgsException.java new file mode 100644 index 0000000..837f2f0 --- /dev/null +++ b/src/main/java/clean/code/chapter14/solution/ArgsException.java @@ -0,0 +1,101 @@ +package clean.code.chapter14.solution; + +public class ArgsException extends Exception { + private char errorArgumentId = '\0'; + private String errorParameter = null; + private ErrorCode errorCode = ErrorCode.OK; + + public ArgsException() { + } + + public ArgsException(String message) { + super(message); + } + + public ArgsException(ErrorCode errorCode) { + this.errorCode = errorCode; + } + + public ArgsException(ErrorCode errorCode, String errorParameter) { + this.errorCode = errorCode; + this.errorParameter = errorParameter; + } + + public ArgsException(ErrorCode errorCode, + char errorArgumentId, String errorParameter) { + this.errorCode = errorCode; + this.errorParameter = errorParameter; + this.errorArgumentId = errorArgumentId; + } + + public char getErrorArgumentId() { + return errorArgumentId; + } + + public void setErrorArgumentId(char errorArgumentId) { + this.errorArgumentId = errorArgumentId; + } + + public String getErrorParameter() { + return errorParameter; + } + + public void setErrorParameter(String errorParameter) { + this.errorParameter = errorParameter; + } + + public ErrorCode getErrorCode() { + return errorCode; + } + + public void setErrorCode(ErrorCode errorCode) { + this.errorCode = errorCode; + } + + public String errorMessage() { + switch (errorCode) { + case OK: + return "TILT: Should not get here."; + case UNEXPECTED_ARGUMENT: + return String.format("Argument -%c unexpected.", errorArgumentId); + case MISSING_STRING: + return String.format("Could not find string parameter for -%c.", + errorArgumentId); + case INVALID_INTEGER: + return String.format("Argument -%c expects an integer but was '%s'.", + errorArgumentId, errorParameter); + case MISSING_INTEGER: + return String.format("Could not find integer parameter for -%c.", + errorArgumentId); + case INVALID_DOUBLE: + return String.format("Argument -%c expects a double but was '%s'.", + errorArgumentId, errorParameter); + case MISSING_DOUBLE: + return String.format("Could not find double parameter for -%c.", + errorArgumentId); + case MISSING_BOOLEAN: + return String.format("Could not find boolean parameter for -%c.", + errorArgumentId); + case INVALID_ARGUMENT_NAME: + return String.format("'%c' is not a valid argument name.", + errorArgumentId); + case INVALID_ARGUMENT_FORMAT: + return String.format("'%s' is not a valid argument format.", + errorParameter); + } + return ""; + } + + @Override + public String getMessage() { + return errorMessage(); + } + + public enum ErrorCode { + OK, INVALID_ARGUMENT_FORMAT, UNEXPECTED_ARGUMENT, INVALID_ARGUMENT_NAME, + MISSING_STRING, + MISSING_INTEGER, INVALID_INTEGER, + MISSING_DOUBLE, INVALID_DOUBLE, + MISSING_BOOLEAN + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/solution/ArgumentMarshaler.java b/src/main/java/clean/code/chapter14/solution/ArgumentMarshaler.java new file mode 100644 index 0000000..d7c3588 --- /dev/null +++ b/src/main/java/clean/code/chapter14/solution/ArgumentMarshaler.java @@ -0,0 +1,7 @@ +package clean.code.chapter14.solution; + +import java.util.Iterator; + +public interface ArgumentMarshaler { + void set(Iterator currentArgument) throws ArgsException; +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/solution/BooleanArgumentMarshaler.java b/src/main/java/clean/code/chapter14/solution/BooleanArgumentMarshaler.java new file mode 100644 index 0000000..aad9256 --- /dev/null +++ b/src/main/java/clean/code/chapter14/solution/BooleanArgumentMarshaler.java @@ -0,0 +1,27 @@ +package clean.code.chapter14.solution; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import static clean.code.chapter14.solution.ArgsException.ErrorCode.MISSING_BOOLEAN; + +public class BooleanArgumentMarshaler implements ArgumentMarshaler { + private boolean booleanValue = false; + + public void set(Iterator currentArgument) throws ArgsException { + String parameter = null; + try { + parameter = currentArgument.next(); + booleanValue = Boolean.parseBoolean(parameter); + } catch (NoSuchElementException e) { + throw new ArgsException(MISSING_BOOLEAN); + } + } + + public static boolean getValue(ArgumentMarshaler am) { + if (am != null && am instanceof BooleanArgumentMarshaler) + return ((BooleanArgumentMarshaler) am).booleanValue; + else + return false; + } +} diff --git a/src/main/java/clean/code/chapter14/solution/DoubleArgumentMarshaler.java b/src/main/java/clean/code/chapter14/solution/DoubleArgumentMarshaler.java new file mode 100644 index 0000000..1c23d25 --- /dev/null +++ b/src/main/java/clean/code/chapter14/solution/DoubleArgumentMarshaler.java @@ -0,0 +1,29 @@ +package clean.code.chapter14.solution; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import static clean.code.chapter14.solution.ArgsException.ErrorCode.*; + +public class DoubleArgumentMarshaler implements ArgumentMarshaler { + private double doubleValue = 0; + + public void set(Iterator currentArgument) throws ArgsException { + String parameter = null; + try { + parameter = currentArgument.next(); + doubleValue = Double.parseDouble(parameter); + } catch (NoSuchElementException e) { + throw new ArgsException(MISSING_DOUBLE); + } catch (NumberFormatException e) { + throw new ArgsException(INVALID_DOUBLE, parameter); + } + } + + public static double getValue(ArgumentMarshaler am) { + if (am != null && am instanceof DoubleArgumentMarshaler) + return ((DoubleArgumentMarshaler) am).doubleValue; + else + return 0; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/solution/IntegerArgumentMarshaler.java b/src/main/java/clean/code/chapter14/solution/IntegerArgumentMarshaler.java new file mode 100644 index 0000000..a17ce26 --- /dev/null +++ b/src/main/java/clean/code/chapter14/solution/IntegerArgumentMarshaler.java @@ -0,0 +1,30 @@ +package clean.code.chapter14.solution; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import static clean.code.chapter14.solution.ArgsException.ErrorCode.INVALID_INTEGER; +import static clean.code.chapter14.solution.ArgsException.ErrorCode.MISSING_INTEGER; + +public class IntegerArgumentMarshaler implements ArgumentMarshaler { + private int intValue = 0; + + public void set(Iterator currentArgument) throws ArgsException { + String parameter = null; + try { + parameter = currentArgument.next(); + intValue = Integer.parseInt(parameter); + } catch (NoSuchElementException e) { + throw new ArgsException(MISSING_INTEGER); + } catch (NumberFormatException e) { + throw new ArgsException(INVALID_INTEGER, parameter); + } + } + + public static int getValue(ArgumentMarshaler am) { + if (am != null && am instanceof IntegerArgumentMarshaler) + return ((IntegerArgumentMarshaler) am).intValue; + else + return 0; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/solution/StringArgumentMarshaler.java b/src/main/java/clean/code/chapter14/solution/StringArgumentMarshaler.java new file mode 100644 index 0000000..1002822 --- /dev/null +++ b/src/main/java/clean/code/chapter14/solution/StringArgumentMarshaler.java @@ -0,0 +1,25 @@ +package clean.code.chapter14.solution; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import static clean.code.chapter14.solution.ArgsException.ErrorCode.MISSING_STRING; + +public class StringArgumentMarshaler implements ArgumentMarshaler { + private String stringValue = ""; + + public void set(Iterator currentArgument) throws ArgsException { + try { + stringValue = currentArgument.next(); + } catch (NoSuchElementException e) { + throw new ArgsException(MISSING_STRING); + } + } + + public static String getValue(ArgumentMarshaler am) { + if (am != null && am instanceof StringArgumentMarshaler) + return ((StringArgumentMarshaler) am).stringValue; + else + return ""; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter14/solution/StringArrayArgumentMarshaler.java b/src/main/java/clean/code/chapter14/solution/StringArrayArgumentMarshaler.java new file mode 100644 index 0000000..e2e64e6 --- /dev/null +++ b/src/main/java/clean/code/chapter14/solution/StringArrayArgumentMarshaler.java @@ -0,0 +1,14 @@ +package clean.code.chapter14.solution; + +import java.util.Iterator; + +public class StringArrayArgumentMarshaler implements ArgumentMarshaler { + @Override + public void set(Iterator currentArgument) throws ArgsException { + //TODO: Auto-generated + } + + public static String[] getValue(ArgumentMarshaler argumentMarshaler) { + return new String[0]; //TODO: Auto-generated + } +} diff --git a/src/main/java/clean/code/chapter15/Assert.java b/src/main/java/clean/code/chapter15/Assert.java new file mode 100644 index 0000000..6731e38 --- /dev/null +++ b/src/main/java/clean/code/chapter15/Assert.java @@ -0,0 +1,11 @@ +package clean.code.chapter15; + +public class Assert { + // Copied from Assert class, since cannot access it here + public static String format(String message, String expected, String actual) { + String formatted = ""; + if (message != null) + formatted = message + " "; + return formatted + "expected:<" + expected + "> but was:<" + actual + ">"; + } +} diff --git a/src/main/java/clean/code/chapter15/defactored/ComparisonCompactor.java b/src/main/java/clean/code/chapter15/defactored/ComparisonCompactor.java new file mode 100644 index 0000000..6bec48a --- /dev/null +++ b/src/main/java/clean/code/chapter15/defactored/ComparisonCompactor.java @@ -0,0 +1,52 @@ +package clean.code.chapter15.defactored; + +import clean.code.chapter15.Assert; + +public class ComparisonCompactor { + private int ctxt; + private String s1; + private String s2; + private int pfx; + private int sfx; + + public ComparisonCompactor(int ctxt, String s1, String s2) { + this.ctxt = ctxt; + this.s1 = s1; + this.s2 = s2; + } + + public String compact(String msg) { + if (s1 == null || s2 == null || s1.equals(s2)) + return Assert.format(msg, s1, s2); + + pfx = 0; + for (; pfx < Math.min(s1.length(), s2.length()); pfx++) { + if (s1.charAt(pfx) != s2.charAt(pfx)) + break; + } + int sfx1 = s1.length() - 1; + int sfx2 = s2.length() - 1; + for (; sfx2 >= pfx && sfx1 >= pfx; sfx2--, sfx1--) { + if (s1.charAt(sfx1) != s2.charAt(sfx2)) + break; + } + sfx = s1.length() - sfx1; + String cmp1 = compactString(s1); + String cmp2 = compactString(s2); + return Assert.format(msg, cmp1, cmp2); + } + + private String compactString(String s) { + String result = + "[" + s.substring(pfx, s.length() - sfx + 1) + "]"; + if (pfx > 0) + result = (pfx > ctxt ? "..." : "") + + s1.substring(Math.max(0, pfx - ctxt), pfx) + result; + if (sfx > 0) { + int end = Math.min(s1.length() - sfx + 1 + ctxt, s1.length()); + result = result + (s1.substring(s1.length() - sfx + 1, end) + + (s1.length() - sfx + 1 < s1.length() - ctxt ? "..." : "")); + } + return result; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter15/interim/ComparisonCompactor.java b/src/main/java/clean/code/chapter15/interim/ComparisonCompactor.java new file mode 100644 index 0000000..5d4f860 --- /dev/null +++ b/src/main/java/clean/code/chapter15/interim/ComparisonCompactor.java @@ -0,0 +1,52 @@ +package clean.code.chapter15.interim; + +public class ComparisonCompactor { +/* + ... + private int suffixLength; +... + private void findCommonPrefixAndSuffix() { + findCommonPrefix(); + suffixLength = 0; + for (; !suffixOverlapsPrefix(suffixLength); suffixLength++) { + if (charFromEnd(expected, suffixLength) != + charFromEnd(actual, suffixLength)) + break; + } + } + + private char charFromEnd(String s, int i) { + return s.charAt(s.length() - i - 1); + } + + private boolean suffixOverlapsPrefix(int suffixLength) { + return actual.length() - suffixLength <= prefixLength || + expected.length() - suffixLength <= prefixLength; + } + +... + private String compactString(String source) { + String result = + DELTA_START + + source.substring(prefixLength, source.length() - suffixLength) + + DELTA_END; + if (prefixLength > 0) + result = computeCommonPrefix() + result; + if (suffixLength > 0) + result = result + computeCommonSuffix(); + return result; + } + +... + private String computeCommonSuffix() { + int end = Math.min(expected.length() - suffixLength + + contextLength, expected.length() + ); + return + expected.substring(expected.length() - suffixLength, end) + + (expected.length() - suffixLength < + expected.length() - contextLength ? + ELLIPSIS : ""); + } + */ +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter15/original/ComparisonCompactor.java b/src/main/java/clean/code/chapter15/original/ComparisonCompactor.java new file mode 100644 index 0000000..db557e9 --- /dev/null +++ b/src/main/java/clean/code/chapter15/original/ComparisonCompactor.java @@ -0,0 +1,87 @@ +package clean.code.chapter15.original; + +import clean.code.chapter15.Assert; + +public class ComparisonCompactor { + + private static final String ELLIPSIS = "..."; + private static final String DELTA_END = "]"; + private static final String DELTA_START = "["; + + private int fContextLength; + private String fExpected; + private String fActual; + private int fPrefix; + private int fSuffix; + + public ComparisonCompactor(int contextLength, + String expected, + String actual) { + fContextLength = contextLength; + fExpected = expected; + fActual = actual; + } + + public String compact(String message) { + if (fExpected == null || fActual == null || areStringsEqual()) + // Was Assert.format + return Assert.format(message, fExpected, fActual); + + findCommonPrefix(); + findCommonSuffix(); + String expected = compactString(fExpected); + String actual = compactString(fActual); + // Was Assert.format + return Assert.format(message, expected, actual); + } + + private String compactString(String source) { + String result = DELTA_START + + source.substring(fPrefix, source.length() - + fSuffix + 1) + DELTA_END; + if (fPrefix > 0) + result = computeCommonPrefix() + result; + if (fSuffix > 0) + result = result + computeCommonSuffix(); + return result; + } + + private void findCommonPrefix() { + fPrefix = 0; + int end = Math.min(fExpected.length(), fActual.length()); + for (; fPrefix < end; fPrefix++) { + if (fExpected.charAt(fPrefix) != fActual.charAt(fPrefix)) + break; + } + } + + private void findCommonSuffix() { + int expectedSuffix = fExpected.length() - 1; + int actualSuffix = fActual.length() - 1; + for (; + actualSuffix >= fPrefix && expectedSuffix >= fPrefix; + actualSuffix--, expectedSuffix--) { + if (fExpected.charAt(expectedSuffix) != fActual.charAt(actualSuffix)) + break; + } + fSuffix = fExpected.length() - expectedSuffix; + } + + private String computeCommonPrefix() { + return (fPrefix > fContextLength ? ELLIPSIS : "") + + fExpected.substring(Math.max(0, fPrefix - fContextLength), + fPrefix); + } + + private String computeCommonSuffix() { + int end = Math.min(fExpected.length() - fSuffix + 1 + fContextLength, + fExpected.length()); + return fExpected.substring(fExpected.length() - fSuffix + 1, end) + + (fExpected.length() - fSuffix + 1 < fExpected.length() - + fContextLength ? ELLIPSIS : ""); + } + + private boolean areStringsEqual() { + return fExpected.equals(fActual); + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter15/solution/ComparisonCompactor.java b/src/main/java/clean/code/chapter15/solution/ComparisonCompactor.java new file mode 100644 index 0000000..1dff880 --- /dev/null +++ b/src/main/java/clean/code/chapter15/solution/ComparisonCompactor.java @@ -0,0 +1,112 @@ +package clean.code.chapter15.solution; + +import clean.code.chapter15.Assert; + +public class ComparisonCompactor { + + private static final String ELLIPSIS = "..."; + private static final String DELTA_END = "]"; + private static final String DELTA_START = "["; + + private int contextLength; + private String expected; + private String actual; + private int prefixLength; + private int suffixLength; + + public ComparisonCompactor( + int contextLength, String expected, String actual + ) { + this.contextLength = contextLength; + this.expected = expected; + this.actual = actual; + } + + public String formatCompactedComparison(String message) { + String compactExpected = expected; + String compactActual = actual; + if (shouldBeCompacted()) { + findCommonPrefixAndSuffix(); + compactExpected = compact(expected); + compactActual = compact(actual); + } + return Assert.format(message, compactExpected, compactActual); + } + + private boolean shouldBeCompacted() { + return !shouldNotBeCompacted(); + } + + private boolean shouldNotBeCompacted() { + return expected == null || + actual == null || + expected.equals(actual); + } + + private void findCommonPrefixAndSuffix() { + findCommonPrefix(); + suffixLength = 0; + for (; !suffixOverlapsPrefix(); suffixLength++) { + if (charFromEnd(expected, suffixLength) != + charFromEnd(actual, suffixLength) + ) + break; + } + } + + private char charFromEnd(String s, int i) { + return s.charAt(s.length() - i - 1); + } + + private boolean suffixOverlapsPrefix() { + return actual.length() - suffixLength <= prefixLength || + expected.length() - suffixLength <= prefixLength; + } + + private void findCommonPrefix() { + prefixLength = 0; + int end = Math.min(expected.length(), actual.length()); + for (; prefixLength < end; prefixLength++) + if (expected.charAt(prefixLength) != actual.charAt(prefixLength)) + break; + } + + private String compact(String s) { + return new StringBuilder() + .append(startingEllipsis()) + .append(startingContext()) + .append(DELTA_START) + .append(delta(s)) + .append(DELTA_END) + .append(endingContext()) + .append(endingEllipsis()) + .toString(); + } + + private String startingEllipsis() { + return prefixLength > contextLength ? ELLIPSIS : ""; + } + + private String startingContext() { + int contextStart = Math.max(0, prefixLength - contextLength); + int contextEnd = prefixLength; + return expected.substring(contextStart, contextEnd); + } + + private String delta(String s) { + int deltaStart = prefixLength; + int deltaEnd = s.length() - suffixLength; + return s.substring(deltaStart, deltaEnd); + } + + private String endingContext() { + int contextStart = expected.length() - suffixLength; + int contextEnd = + Math.min(contextStart + contextLength, expected.length()); + return expected.substring(contextStart, contextEnd); + } + + private String endingEllipsis() { + return (suffixLength > contextLength ? ELLIPSIS : ""); + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter16/original/MonthConstants.java b/src/main/java/clean/code/chapter16/original/MonthConstants.java new file mode 100644 index 0000000..70f841a --- /dev/null +++ b/src/main/java/clean/code/chapter16/original/MonthConstants.java @@ -0,0 +1,91 @@ +package clean.code.chapter16.original; + +/* ======================================================================== +ßary for the Java(tm) platform +ß============================= +ß +ß Limited and Contributors. +ß +ß/index.html +ß +ßstribute it and/or modify it +ßublic License as published by +ßn 2.1 of the License, or +ß +ß +ßt it will be useful, but +ßied warranty of MERCHANTABILITY +ße GNU Lesser General Public +ß +ß +ßLesser General Public +ßite to the Free Software +ß Floor, Boston, MA 02110-1301, + * USA. + * + * [Java is a trademark or registered trademark of Sun Microsystems, Inc. + * in the United States and other countries.] + * + * ------------------- + * MonthConstants.java + * ------------------- + * (C) Copyright 2002, 2003, by Object Refinery Limited. + * + * Original Author: David Gilbert (for Object Refinery Limited); + * Contributor(s): -; + * + * $Id: MonthConstants.java,v 1.4 2005/11/16 15:58:40 taqua Exp $ + * + * Changes + * ------- + * 29-May-2002 : Version 1 (code moved from SerialDate class) (DG); + * + */ + + /** + * Useful constants for months. Note that these are NOT equivalent to the + * constants defined by java.util.Calendar (where JANUARY=0 and DECEMBER=11). + *

+ * Used by the SerialDate and RegularTimePeriod classes. + * + * @author David Gilbert + */ + public interface MonthConstants { + + /** Constant for January. */ + public static final int JANUARY = 1; + + /** Constant for February. */ + public static final int FEBRUARY = 2; + + /** Constant for March. */ + public static final int MARCH = 3; + + /** Constant for April. */ + public static final int APRIL = 4; + + /** Constant for May. */ + public static final int MAY = 5; + + /** Constant for June. */ + public static final int JUNE = 6; + + /** Constant for July. */ + public static final int JULY = 7; + + /** Constant for August. */ + public static final int AUGUST = 8; + + /** Constant for September. */ + public static final int SEPTEMBER = 9; + + /** Constant for October. */ + public static final int OCTOBER = 10; + + /** Constant for November. */ + public static final int NOVEMBER = 11; + + /** Constant for December. */ + public static final int DECEMBER = 12; + +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter16/original/SerialDate.java b/src/main/java/clean/code/chapter16/original/SerialDate.java new file mode 100644 index 0000000..aa39ad5 --- /dev/null +++ b/src/main/java/clean/code/chapter16/original/SerialDate.java @@ -0,0 +1,1132 @@ +package clean.code.chapter16.original; + +/* ======================================================================== + * JCommon : a free general purpose class library for the Java(tm) platform + * ======================================================================== + * + * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors. + * + * Project Info: http://www.jfree.org/jcommon/index.html + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * [Java is a trademark or registered trademark of Sun Microsystems, Inc. + * in the United States and other countries.] + * + * --------------- + * SerialDate.java + * --------------- + * (C) Copyright 2001-2005, by Object Refinery Limited. + * + * Original Author: David Gilbert (for Object Refinery Limited); + * Contributor(s): -; + * + * $Id: SerialDate.java,v 1.7 2005/11/03 09:25:17 mungady Exp $ + * + * + * Changes (from 11-Oct-2001) + * fixme (1) : changelog is redundant here + * -------------------------- + * 11-Oct-2001 : Re-organised the class and moved it to new package + * com.jrefinery.date (DG); + * 05-Nov-2001 : Added a getDescription() method, and eliminated NotableDate + * class (DG); + * 12-Nov-2001 : IBD requires setDescription() method, now that NotableDate + * class is gone (DG); Changed getPreviousDayOfWeek(), + * getFollowingDayOfWeek() and getNearestDayOfWeek() to correct + * bugs (DG); + * 05-Dec-2001 : Fixed bug in SpreadsheetDate class (DG); + * 29-May-2002 : Moved the month constants into a separate interface + * (MonthConstants) (DG); + * 27-Aug-2002 : Fixed bug in addMonths() method, thanks to N???levka Petr (DG); + * 03-Oct-2002 : Fixed errors reported by Checkstyle (DG); + * 13-Mar-2003 : Implemented Serializable (DG); + * 29-May-2003 : Fixed bug in addMonths method (DG); + * 04-Sep-2003 : Implemented Comparable. Updated the isInRange javadocs (DG); + * 05-Jan-2005 : Fixed bug in addYears() method (1096282) (DG); + * + */ + +import java.io.Serializable; +import java.text.DateFormatSymbols; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.GregorianCalendar; + +/** + * An abstract class that defines our requirements for manipulating dates, + * without tying down a particular implementation. + * fixme (2): html tags are redundant + *

+ * Requirement 1 : match at least what Excel does for dates; + * Requirement 2 : class is immutable; + *

+ * Why not just use java.util.Date? We will, when it makes sense. At times, + * java.util.Date can be *too* precise - it represents an instant in time, + * accurate to 1/1000th of a second (with the date itself depending on the + * time-zone). Sometimes we just want to represent a particular day (e.g. 21 + * January 2015) without concerning ourselves about the time of day, or the + * time-zone, or anything else. That's what we've defined SerialDate for. + *

+ * You can call getInstance() to get a concrete subclass of SerialDate, + * without worrying about the exact implementation. + * + * @author David Gilbert + */ +// fixme (3): `Serial` part of the class name is not a good choice in naming +// => using `DayDate` instead of `SerialDate` is a better option +public abstract class SerialDate implements Comparable, Serializable, + // fixme (5): extending MonthConstants which is tricky, you can add an enum inside this class + MonthConstants { + + /** + * For serialization. + */ + // fixme (8): I know that all the documents + // recommend manual control of this variable, but it seems to me that automatic control + // of serialization is a lot safer because we wont forget to change it. + private static final long serialVersionUID = -293716040467423637L; + + /** + * Date format symbols. + */ + public static final DateFormatSymbols + DATE_FORMAT_SYMBOLS = new SimpleDateFormat().getDateFormatSymbols(); + + /** + * The serial number for 1 January 1900. // fixme (9): this comment is going to be misinformation + * // fixme (11): this has nothing to do with current class and is related to SpreadsheetDate, just press Alt + F7 + */ + public static final int SERIAL_LOWER_BOUND = 2; + + /** + * The serial number for 31 December 9999. // fixme (10): this comment is going to be misinformation + * // fixme (12): this has nothing to do with current class and is related to SpreadsheetDate, just press Alt + F7 + */ + public static final int SERIAL_UPPER_BOUND = 2958465; + + /** + * The lowest year value supported by this date format. + * // fixme (14): this does not belong to current abstract class, should be moved to implementation + */ + public static final int MINIMUM_YEAR_SUPPORTED = 1900; + + /** + * The highest year value supported by this date format. + * // fixme (15): this does not belong to current abstract class, should be moved to implementation + */ + public static final int MAXIMUM_YEAR_SUPPORTED = 9999; + + // fixme (16): there should be an enum class containing these day related constants + /** + * Useful constant for Monday. Equivalent to java.util.Calendar.MONDAY. + */ + public static final int MONDAY = Calendar.MONDAY; + + /** + * Useful constant for Tuesday. Equivalent to java.util.Calendar.TUESDAY. + */ + public static final int TUESDAY = Calendar.TUESDAY; + + /** + * Useful constant for Wednesday. Equivalent to + * java.util.Calendar.WEDNESDAY. + */ + public static final int WEDNESDAY = Calendar.WEDNESDAY; + + /** + * Useful constant for Thrusday. Equivalent to java.util.Calendar.THURSDAY. + */ + public static final int THURSDAY = Calendar.THURSDAY; + + /** + * Useful constant for Friday. Equivalent to java.util.Calendar.FRIDAY. + */ + public static final int FRIDAY = Calendar.FRIDAY; + + /** + * Useful constant for Saturday. Equivalent to java.util.Calendar.SATURDAY. + */ + public static final int SATURDAY = Calendar.SATURDAY; + + /** + * Useful constant for Sunday. Equivalent to java.util.Calendar.SUNDAY. + */ + public static final int SUNDAY = Calendar.SUNDAY; + + /** + * The number of days in each month in non leap years. + * fixme (17): redundant comment here + */ + static final int[] LAST_DAY_OF_MONTH = + {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + + /** + * The number of days in a (non-leap) year up to the end of each month. + * fixme (18): redundant comment here + */ + static final int[] AGGREGATE_DAYS_TO_END_OF_MONTH = + {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}; + + /** + * The number of days in a year up to the end of the preceding month. + * fixme (19): redundant comment here + * fixme (22): this is used only in SpreadsheetDate => move it there + */ + static final int[] AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH = + {0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}; + + /** + * The number of days in a leap year up to the end of each month. + * fixme (20): redundant comment here + */ + static final int[] LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_MONTH = + {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}; + + /** + * The number of days in a leap year up to the end of the preceding month. + * fixme (21): redundant comment here + * fixme (23): this is used only in SpreadsheetDate => move it there + */ + static final int[] + LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH = + {0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}; + + /** + * A useful constant for referring to the first week in a month. + * fixme (24): we can move this and five below constants to an enum class + */ + public static final int FIRST_WEEK_IN_MONTH = 1; + + /** + * A useful constant for referring to the second week in a month. + */ + public static final int SECOND_WEEK_IN_MONTH = 2; + + /** + * A useful constant for referring to the third week in a month. + */ + public static final int THIRD_WEEK_IN_MONTH = 3; + + /** + * A useful constant for referring to the fourth week in a month. + */ + public static final int FOURTH_WEEK_IN_MONTH = 4; + + /** + * A useful constant for referring to the last week in a month. + */ + public static final int LAST_WEEK_IN_MONTH = 0; + + /** + * Useful range constant. + * fixme (25): move these Include_* constants to a class named DateInterval + */ + public static final int INCLUDE_NONE = 0; + + /** + * Useful range constant. + */ + public static final int INCLUDE_FIRST = 1; + + /** + * Useful range constant. + */ + public static final int INCLUDE_SECOND = 2; + + /** + * Useful range constant. + */ + public static final int INCLUDE_BOTH = 3; + + /** + * Useful constant for specifying a day of the week relative to a fixed + * date. + * fixme (26): These three constants define a range in weekday so move it to WeekdayRange + */ + public static final int PRECEDING = -1; + + /** + * Useful constant for specifying a day of the week relative to a fixed + * date. + */ + public static final int NEAREST = 0; + + /** + * Useful constant for specifying a day of the week relative to a fixed + * date. + */ + public static final int FOLLOWING = 1; + + /** + * A description for the date. + * fixme (27): This field is not used anywhere => remove it and it's accessor and mutator + */ + private String description; + + /** + * Default constructor. + * fixme (28): Redundant declaration because class is abstract already! + */ + protected SerialDate() { + } + + /** + * Returns true if the supplied integer code represents a + * valid day-of-the-week, and false otherwise. + * + * @param code the code being checked for validity. + * @return true if the supplied integer code represents a + * valid day-of-the-week, and false otherwise. + * fixme (29): We can remove this function because we can use Day enum + */ + public static boolean isValidWeekdayCode(final int code) { + + switch (code) { + case SUNDAY: + case MONDAY: + case TUESDAY: + case WEDNESDAY: + case THURSDAY: + case FRIDAY: + case SATURDAY: + return true; + default: + return false; + } + + } + + /** + * fixme (30): java doc is not adding worthwhile information => remove javadoc + * fixme (33): this method does not really belong here => Move to Day.parse() function + * Converts the supplied string to a day of the week. + * + * @param s a string representing the day of the week. + * @return -1 if the string is not convertable, the day of + * the week otherwise. + */ + public static int stringToWeekdayCode(String s) { + + // fixme (31): declaring these variables as final does not do any purpose + final String[] shortWeekdayNames + = DATE_FORMAT_SYMBOLS.getShortWeekdays(); + final String[] weekDayNames = DATE_FORMAT_SYMBOLS.getWeekdays(); + + int result = -1; + s = s.trim(); + for (int i = 0; i < weekDayNames.length; i++) { + // TODO (0): 5/1/2021 we just changed equals to equalsIgnoreCase here and 4 lines below + // fixme (32): What's the point of duplicate if statements here? we can use || operator + if (s.equalsIgnoreCase(shortWeekdayNames[i])) { + result = i; + break; + } + if (s.equalsIgnoreCase(weekDayNames[i])) { + result = i; + break; + } + } + return result; + + } + + /** + * Returns a string representing the supplied day-of-the-week. + *

+ * Need to find a better approach. + * + * @param weekday the day of the week. + * @return a string representing the supplied day-of-the-week. + * fixme (34): this method does not really belong here => Move to Day.toString() function + */ + public static String weekdayCodeToString(final int weekday) { + + final String[] weekdays = DATE_FORMAT_SYMBOLS.getWeekdays(); + return weekdays[weekday]; + + } + + /** + * Returns an array of month names. + * + * @return an array of month names. + * fixme (36): this method is not used + */ + public static String[] getMonths() { + + return getMonths(false); + + } + + /** + * Returns an array of month names. + * + * @param shortened a flag indicating that shortened month names should + * be returned. + * @return an array of month names. + * fixme (35): function name is not good, it's returning only name of month + */ + public static String[] getMonths(final boolean shortened) { + + if (shortened) { + return DATE_FORMAT_SYMBOLS.getShortMonths(); + } else { + return DATE_FORMAT_SYMBOLS.getMonths(); + } + + } + + /** + * Returns true if the supplied integer code represents a valid month. + * + * @param code the code being checked for validity. + * @return true if the supplied integer code represents a + * valid month. + */ + // fixme (6): using Month constant you can get rid of this method because everything is using enum + public static boolean isValidMonthCode(final int code) { + + switch (code) { + case JANUARY: + case FEBRUARY: + case MARCH: + case APRIL: + case MAY: + case JUNE: + case JULY: + case AUGUST: + case SEPTEMBER: + case OCTOBER: + case NOVEMBER: + case DECEMBER: + return true; + default: + return false; + } + + } + + /** + * Returns the quarter for the specified month. + * + * @param code the month code (1-12). + * @return the quarter that the month belongs to. + * @throws IllegalArgumentException + * + */ + // fixme (7): using Month constant you can get rid of this method because everything is using enum + public static int monthCodeToQuarter(final int code) { + + switch (code) { + case JANUARY: + case FEBRUARY: + case MARCH: + return 1; + case APRIL: + case MAY: + case JUNE: + return 2; + case JULY: + case AUGUST: + case SEPTEMBER: + return 3; + case OCTOBER: + case NOVEMBER: + case DECEMBER: + return 4; + default: + throw new IllegalArgumentException( + "SerialDate.monthCodeToQuarter: invalid month code."); + } + + } + + /** + * Returns a string representing the supplied month. + *

+ * The string returned is the long form of the month name taken from the + * default locale. + * + * @param month the month. + * @return a string representing the supplied month. + * fixme (37): this function does not belong here => move to Month class and use a better name + */ + public static String monthCodeToString(final int month) { + + return monthCodeToString(month, false); + + } + + /** + * Returns a string representing the supplied month. + *

+ * The string returned is the long or short form of the month name taken + * from the default locale. + * + * @param month the month. + * @param shortened if true return the abbreviation of the + * month. + * @return a string representing the supplied month. + * @throws IllegalArgumentException + * fixme (38): this function does not belong here => move to Month class and use a better name + */ + public static String monthCodeToString(final int month, + final boolean shortened) { + + // check arguments... + if (!isValidMonthCode(month)) { + throw new IllegalArgumentException( + "SerialDate.monthCodeToString: month outside valid range."); + } + + final String[] months; + + if (shortened) { + months = DATE_FORMAT_SYMBOLS.getShortMonths(); + } else { + months = DATE_FORMAT_SYMBOLS.getMonths(); + } + + return months[month - 1]; + + } + + /** + * Converts a string to a month code. + *

+ * This method will return one of the constants JANUARY, FEBRUARY, ..., + * DECEMBER that corresponds to the string. If the string is not + * recognised, this method returns -1. + * + * @param s the string to parse. + * @return -1 if the string is not parseable, the month of the + * year otherwise. + * fixme (39): this function does not belong here => move to Month class and use a better name + */ + public static int stringToMonthCode(String s) { + + final String[] shortMonthNames = DATE_FORMAT_SYMBOLS.getShortMonths(); + final String[] monthNames = DATE_FORMAT_SYMBOLS.getMonths(); + + int result = -1; + s = s.trim(); + + // first try parsing the string as an integer (1-12)... + try { + result = Integer.parseInt(s); + } catch (NumberFormatException e) { + // suppress + } + + // now search through the month names... + if ((result < 1) || (result > 12)) { + // TODO (1, 2): 5/1/2021 just added result = -1 and changed s.equals to s.equalsIgnoreCase at two of below statements + result = -1; + for (int i = 0; i < monthNames.length; i++) { + if (s.equalsIgnoreCase(shortMonthNames[i])) { + result = i + 1; + break; + } + if (s.equalsIgnoreCase(monthNames[i])) { + result = i + 1; + break; + } + } + } + + return result; + + } + + /** + * Returns true if the supplied integer code represents a valid + * week-in-the-month, and false otherwise. + * + * @param code the code being checked for validity. + * @return true if the supplied integer code represents a + * valid week-in-the-month. + * fixme (40): redundant method because we moved these constants to WeekInMonth + */ + public static boolean isValidWeekInMonthCode(final int code) { + + switch (code) { + case FIRST_WEEK_IN_MONTH: + case SECOND_WEEK_IN_MONTH: + case THIRD_WEEK_IN_MONTH: + case FOURTH_WEEK_IN_MONTH: + case LAST_WEEK_IN_MONTH: + return true; + default: + return false; + } + + } + + /** + * Determines whether or not the specified year is a leap year. + * + * @param yyyy the year (in the range 1900 to 9999). + * @return true if the specified year is a leap year. + */ + public static boolean isLeapYear(final int yyyy) { + // fixme (41): not readable code, also it can be moved to a DateUtil class instead + if ((yyyy % 4) != 0) { + return false; + } else if ((yyyy % 400) == 0) { + return true; + } else if ((yyyy % 100) == 0) { + return false; + } else { + return true; + } + + } + + /** + * Returns the number of leap years from 1900 to the specified year + * INCLUSIVE. + *

+ * Note that 1900 is not a leap year. + * + * @param yyyy the year (in the range 1900 to 9999). + * @return the number of leap years from 1900 to the specified year. + * fixme (42): this function is used only in SpreadsheetDate class => move it there + */ + public static int leapYearCount(final int yyyy) { + + final int leap4 = (yyyy - 1896) / 4; + final int leap100 = (yyyy - 1800) / 100; + final int leap400 = (yyyy - 1600) / 400; + return leap4 - leap100 + leap400; + + } + + /** + * Returns the number of the last day of the month, taking into account + * leap years. + * + * @param month the month. + * @param yyyy the year (in the range 1900 to 9999). + * @return the number of the last day of the month. + * fixme (43): This function does not belong here => move it to Month class + */ + public static int lastDayOfMonth(final int month, final int yyyy) { + + final int result = LAST_DAY_OF_MONTH[month]; + if (month != FEBRUARY) { + return result; + } else if (isLeapYear(yyyy)) { + return result + 1; + } else { + return result; + } + + } + + /** + * Creates a new date by adding the specified number of days to the base + * date. + * + * @param days the number of days to add (can be negative). + * @param base the base date. + * @return a new date. + * fixme (44): this function must be instance method not static + */ + public static SerialDate addDays(final int days, final SerialDate base) { + + final int serialDayNumber = base.toSerial() + days; + return SerialDate.createInstance(serialDayNumber); + + } + + /** + * Creates a new date by adding the specified number of months to the base + * date. + *

+ * If the base date is close to the end of the month, the day on the result + * may be adjusted slightly: 31 May + 1 month = 30 June. + * + * @param months the number of months to add (can be negative). + * @param base the base date. + * @return a new date. + * fixme (45): this function must be instance method not static + */ + public static SerialDate addMonths(final int months, + final SerialDate base) { + // fixme (46): algorithm and namings are a bit complicated + // fixme (47): instead of getYYYY() name use getYear()! + final int yy = (12 * base.getYYYY() + base.getMonth() + months - 1) + / 12; + final int mm = (12 * base.getYYYY() + base.getMonth() + months - 1) + % 12 + 1; + final int dd = Math.min( + base.getDayOfMonth(), SerialDate.lastDayOfMonth(mm, yy) + ); + return SerialDate.createInstance(dd, mm, yy); + + } + + /** + * Creates a new date by adding the specified number of years to the base + * date. + * + * @param years the number of years to add (can be negative). + * @param base the base date. + * @return A new date. + * fixme (48): this function must be instance method not static + */ + public static SerialDate addYears(final int years, final SerialDate base) { + + final int baseY = base.getYYYY(); + final int baseM = base.getMonth(); + final int baseD = base.getDayOfMonth(); + + final int targetY = baseY + years; + final int targetD = Math.min( + baseD, SerialDate.lastDayOfMonth(baseM, targetY) + ); + + return SerialDate.createInstance(targetD, baseM, targetY); + + } + + /** + * Returns the latest date that falls on the specified day-of-the-week and + * is BEFORE the base date. + * + * @param targetWeekday a code for the target day-of-the-week. + * @param base the base date. + * @return the latest date that falls on the specified day-of-the-week and + * is BEFORE the base date. + * fixme (49): this function must be instance method not static + */ + public static SerialDate getPreviousDayOfWeek(final int targetWeekday, + final SerialDate base) { + + // check arguments... + if (!SerialDate.isValidWeekdayCode(targetWeekday)) { + throw new IllegalArgumentException( + "Invalid day-of-the-week code." + ); + } + + // fixme (50): Use some explaining temporary variables for god sake! + // find the date... + final int adjust; + final int baseDOW = base.getDayOfWeek(); + if (baseDOW > targetWeekday) { + adjust = Math.min(0, targetWeekday - baseDOW); + } else { + adjust = -7 + Math.max(0, targetWeekday - baseDOW); + } + + return SerialDate.addDays(adjust, base); + + } + + /** + * Returns the earliest date that falls on the specified day-of-the-week + * and is AFTER the base date. + * + * @param targetWeekday a code for the target day-of-the-week. + * @param base the base date. + * @return the earliest date that falls on the specified day-of-the-week + * and is AFTER the base date. + * fixme (51): this function must be instance method not static + */ + public static SerialDate getFollowingDayOfWeek(final int targetWeekday, + final SerialDate base) { + + // check arguments... + if (!SerialDate.isValidWeekdayCode(targetWeekday)) { + throw new IllegalArgumentException( + "Invalid day-of-the-week code." + ); + } + + // fixme (52): Use some explaining temporary variables for god sake! + // find the date... + final int adjust; + final int baseDOW = base.getDayOfWeek(); + // TODO (3): 5/1/2021 changed below condition from > to >= + if (baseDOW >= targetWeekday) { + adjust = 7 + Math.min(0, targetWeekday - baseDOW); + } else { + adjust = Math.max(0, targetWeekday - baseDOW); + } + + return SerialDate.addDays(adjust, base); + } + + /** + * Returns the date that falls on the specified day-of-the-week and is + * CLOSEST to the base date. + * + * @param targetDOW a code for the target day-of-the-week. + * @param base the base date. + * @return the date that falls on the specified day-of-the-week and is + * CLOSEST to the base date. + * fixme (53): this function must be instance method not static + */ + public static SerialDate getNearestDayOfWeek(final int targetDOW, + final SerialDate base) { + + // check arguments... + if (!SerialDate.isValidWeekdayCode(targetDOW)) { + throw new IllegalArgumentException( + "Invalid day-of-the-week code." + ); + } + + // fixme (54): Use some explaining temporary variables for god sake! + // find the date... + int delta = targetDOW - base.getDayOfWeek(); + int positiveDelta = delta + 7; + int adjust = positiveDelta % 7; + if (adjust > 3) + adjust -= 7; + // TODO (4): 5/1/2021 below algorithm is wrong and above is correct +// final int baseDOW = base.getDayOfWeek(); +// int adjust = -Math.abs(targetDOW - baseDOW); +// if (adjust >= 4) { +// adjust = 7 - adjust; +// } +// if (adjust <= -4) { +// adjust = 7 + adjust; +// } + return SerialDate.addDays(adjust, base); + + } + + /** + * Rolls the date forward to the last day of the month. + * + * @param base the base date. + * @return a new serial date. + * fixme (55): What's the use of passing current object in argument? + */ + public SerialDate getEndOfCurrentMonth(final SerialDate base) { + final int last = SerialDate.lastDayOfMonth( + base.getMonth(), base.getYYYY() + ); + return SerialDate.createInstance(last, base.getMonth(), base.getYYYY()); + } + + /** + * Returns a string corresponding to the week-in-the-month code. + *

+ * Need to find a better approach. + * + * @param count an integer code representing the week-in-the-month. + * @return a string corresponding to the week-in-the-month code. + * fixme (56): Move this function to where it belongs (WeekInMonth) + */ + public static String weekInMonthToString(final int count) { + + switch (count) { + case SerialDate.FIRST_WEEK_IN_MONTH: + return "First"; + case SerialDate.SECOND_WEEK_IN_MONTH: + return "Second"; + case SerialDate.THIRD_WEEK_IN_MONTH: + return "Third"; + case SerialDate.FOURTH_WEEK_IN_MONTH: + return "Fourth"; + case SerialDate.LAST_WEEK_IN_MONTH: + return "Last"; + default: + // todo (5): fixed test by throwing exception instead of returning string + throw new IllegalArgumentException("SerialDate.weekInMonthToString(): invalid code."); +// return "SerialDate.weekInMonthToString(): invalid code."; + } + + } + + /** + * Returns a string representing the supplied 'relative'. + *

+ * Need to find a better approach. + * + * @param relative a constant representing the 'relative'. + * @return a string representing the supplied 'relative'. + */ + public static String relativeToString(final int relative) { + + switch (relative) { + case SerialDate.PRECEDING: + return "Preceding"; + case SerialDate.NEAREST: + return "Nearest"; + case SerialDate.FOLLOWING: + return "Following"; + default: + // todo (6): fixed test by throwing exception instead of returning string + throw new IllegalArgumentException("ERROR : Relative To String"); +// return "ERROR : Relative To String"; + } + + } + + /** + * Factory method that returns an instance of some concrete subclass of + * {@link SerialDate}. + * + * @param day the day (1-31). + * @param month the month (1-12). + * @param yyyy the year (in the range 1900 to 9999). + * @return An instance of {@link SerialDate}. + */ + public static SerialDate createInstance(final int day, final int month, + final int yyyy) { + return new SpreadsheetDate(day, month, yyyy); + } + + /** + * Factory method that returns an instance of some concrete subclass of + * {@link SerialDate}. + * + * @param serial the serial number for the day (1 January 1900 = 2). + * @return a instance of SerialDate. + */ + // fixme (13): Really?!! current class is abstract and here we are returning one of it's Implementation (SpreadsheetDate) + // => abstract class should not know about implementation use ABSTRACT FACTORY pattern and create a DayDateFactory + public static SerialDate createInstance(final int serial) { + return new SpreadsheetDate(serial); + } + + /** + * Factory method that returns an instance of a subclass of SerialDate. + * + * @param date A Java date object. + * @return a instance of SerialDate. + */ + public static SerialDate createInstance(final java.util.Date date) { + + final GregorianCalendar calendar = new GregorianCalendar(); + calendar.setTime(date); + return new SpreadsheetDate(calendar.get(Calendar.DATE), + calendar.get(Calendar.MONTH) + 1, + calendar.get(Calendar.YEAR)); + + } + + /** + * Returns the serial number for the date, where 1 January 1900 = 2 (this + * corresponds, almost, to the numbering system used in Microsoft Excel for + * Windows and Lotus 1-2-3). + * + * @return the serial number for the date. + */ + // fixme (4): This is not a descriptive name, given date is more like offset than serial number + // => more descriptive name might be ordinal + public abstract int toSerial(); + + /** + * Returns a java.util.Date. Since java.util.Date has more precision than + * SerialDate, we need to define a convention for the 'time of day'. + * + * @return this as java.util.Date. + * fixme (57): Why is this method abstract? it can be implemented here + */ + public abstract java.util.Date toDate(); + + /** + * Returns a description of the date. + * + * @return a description of the date. + */ + public String getDescription() { + return this.description; + } + + /** + * Sets the description for the date. + * + * @param description the new description for the date. + */ + public void setDescription(final String description) { + this.description = description; + } + + /** + * Converts the date to a string. + * + * @return a string representation of the date. + */ + public String toString() { + return getDayOfMonth() + "-" + SerialDate.monthCodeToString(getMonth()) + + "-" + getYYYY(); + } + + /** + * Returns the year (assume a valid range of 1900 to 9999). + * + * @return the year. + */ + public abstract int getYYYY(); + + /** + * Returns the month (January = 1, February = 2, March = 3). + * + * @return the month of the year. + */ + public abstract int getMonth(); + + /** + * Returns the day of the month. + * + * @return the day of the month. + */ + public abstract int getDayOfMonth(); + + /** + * Returns the day of the week. + * + * @return the day of the week. + * fixme (58): why is this method abstract? this method can be implemented here + */ + public abstract int getDayOfWeek(); + + /** + * Returns the difference (in days) between this date and the specified + * 'other' date. + *

+ * The result is positive if this date is after the 'other' date and + * negative if it is before the 'other' date. + * + * @param other the date being compared to. + * @return the difference between this and the other date. + * fixme (59): why is this method abstract? this method can be implemented here + */ + public abstract int compare(SerialDate other); + + /** + * Returns true if this SerialDate represents the same date as the + * specified SerialDate. + * + * @param other the date being compared to. + * @return true if this SerialDate represents the same date as + * the specified SerialDate. + * fixme (60): why is this method abstract? this method can be implemented here + */ + public abstract boolean isOn(SerialDate other); + + /** + * Returns true if this SerialDate represents an earlier date compared to + * the specified SerialDate. + * + * @param other The date being compared to. + * @return true if this SerialDate represents an earlier date + * compared to the specified SerialDate. + * fixme (61): why is this method abstract? this method can be implemented here + */ + public abstract boolean isBefore(SerialDate other); + + /** + * Returns true if this SerialDate represents the same date as the + * specified SerialDate. + * + * @param other the date being compared to. + * @return true if this SerialDate represents the same date + * as the specified SerialDate. + * fixme (62): why is this method abstract? this method can be implemented here + */ + public abstract boolean isOnOrBefore(SerialDate other); + + /** + * Returns true if this SerialDate represents the same date as the + * specified SerialDate. + * + * @param other the date being compared to. + * @return true if this SerialDate represents the same date + * as the specified SerialDate. + * fixme (63): why is this method abstract? this method can be implemented here + */ + public abstract boolean isAfter(SerialDate other); + + /** + * Returns true if this SerialDate represents the same date as the + * specified SerialDate. + * + * @param other the date being compared to. + * @return true if this SerialDate represents the same date + * as the specified SerialDate. + * fixme (64): why is this method abstract? this method can be implemented here + */ + public abstract boolean isOnOrAfter(SerialDate other); + + /** + * Returns true if this {@link SerialDate} is within the + * specified range (INCLUSIVE). The date order of d1 and d2 is not + * important. + * + * @param d1 a boundary date for the range. + * @param d2 the other boundary date for the range. + * @return A boolean. + * fixme (65): why is this method abstract? this method can be implemented here + */ + public abstract boolean isInRange(SerialDate d1, SerialDate d2); + + /** + * Returns true if this {@link SerialDate} is within the + * specified range (caller specifies whether or not the end-points are + * included). The date order of d1 and d2 is not important. + * + * @param d1 a boundary date for the range. + * @param d2 the other boundary date for the range. + * @param include a code that controls whether or not the start and end + * dates are included in the range. + * @return A boolean. + * fixme (66): why is this method abstract? this method can be implemented here + */ + public abstract boolean isInRange(SerialDate d1, SerialDate d2, + int include); + + /** + * Returns the latest date that falls on the specified day-of-the-week and + * is BEFORE this date. + * + * @param targetDOW a code for the target day-of-the-week. + * @return the latest date that falls on the specified day-of-the-week and + * is BEFORE this date. + */ + public SerialDate getPreviousDayOfWeek(final int targetDOW) { + return getPreviousDayOfWeek(targetDOW, this); + } + + /** + * Returns the earliest date that falls on the specified day-of-the-week + * and is AFTER this date. + * + * @param targetDOW a code for the target day-of-the-week. + * @return the earliest date that falls on the specified day-of-the-week + * and is AFTER this date. + */ + public SerialDate getFollowingDayOfWeek(final int targetDOW) { + return getFollowingDayOfWeek(targetDOW, this); + } + + /** + * Returns the nearest date that falls on the specified day-of-the-week. + * + * @param targetDOW a code for the target day-of-the-week. + * @return the nearest date that falls on the specified day-of-the-week. + */ + public SerialDate getNearestDayOfWeek(final int targetDOW) { + return getNearestDayOfWeek(targetDOW, this); + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter16/original/SpreadsheetDate.java b/src/main/java/clean/code/chapter16/original/SpreadsheetDate.java new file mode 100644 index 0000000..386ab3f --- /dev/null +++ b/src/main/java/clean/code/chapter16/original/SpreadsheetDate.java @@ -0,0 +1,496 @@ +package clean.code.chapter16.original; + +/* ======================================================================== + * JCommon : a free general purpose class library for the Java(tm) platform + * ======================================================================== + * + * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors. + * + * Project Info: http://www.jfree.org/jcommon/index.html + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * [Java is a trademark or registered trademark of Sun Microsystems, Inc. + * in the United States and other countries.] + * + * -------------------- + * SpreadsheetDate.java + * -------------------- + * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors. + * + * Original Author: David Gilbert (for Object Refinery Limited); + * Contributor(s): -; + * + * $Id: SpreadsheetDate.java,v 1.8 2005/11/03 09:25:39 mungady Exp $ + * + * Changes + * ------- + * 11-Oct-2001 : Version 1 (DG); + * 05-Nov-2001 : Added getDescription() and setDescription() methods (DG); + * 12-Nov-2001 : Changed name from ExcelDate.java to SpreadsheetDate.java (DG); + * Fixed a bug in calculating day, month and year from serial + * number (DG); + * 24-Jan-2002 : Fixed a bug in calculating the serial number from the day, + * month and year. Thanks to Trevor Hills for the report (DG); + * 29-May-2002 : Added equals(Object) method (SourceForge ID 558850) (DG); + * 03-Oct-2002 : Fixed errors reported by Checkstyle (DG); + * 13-Mar-2003 : Implemented Serializable (DG); + * 04-Sep-2003 : Completed isInRange() methods (DG); + * 05-Sep-2003 : Implemented Comparable (DG); + * 21-Oct-2003 : Added hashCode() method (DG); + * + */ + + +import java.util.Calendar; +import java.util.Date; + +/** + * Represents a date using an integer, in a similar fashion to the + * implementation in Microsoft Excel. The range of dates supported is + * 1-Jan-1900 to 31-Dec-9999. + *

+ * Be aware that there is a deliberate bug in Excel that recognises the year + * 1900 as a leap year when in fact it is not a leap year. You can find more + * information on the Microsoft website in article Q181370: + *

+ * http://support.microsoft.com/support/kb/articles/Q181/3/70.asp + *

+ * Excel uses the convention that 1-Jan-1900 = 1. This class uses the + * convention 1-Jan-1900 = 2. + * The result is that the day number in this class will be different to the + * Excel figure for January and February 1900...but then Excel adds in an extra + * day (29-Feb-1900 which does not actually exist!) and from that point forward + * the day numbers will match. + * + * @author David Gilbert + */ +public class SpreadsheetDate extends SerialDate { + + /** For serialization. */ + private static final long serialVersionUID = -2039586705374454461L; + + /** + * The day number (1-Jan-1900 = 2, 2-Jan-1900 = 3, ..., 31-Dec-9999 = + * 2958465). + */ + private int serial; + + /** The day of the month (1 to 28, 29, 30 or 31 depending on the month). */ + private int day; + + /** The month of the year (1 to 12). */ + private int month; + + /** The year (1900 to 9999). */ + private int year; + + /** An optional description for the date. */ + private String description; + + /** + * Creates a new date instance. + * + * @param day the day (in the range 1 to 28/29/30/31). + * @param month the month (in the range 1 to 12). + * @param year the year (in the range 1900 to 9999). + */ + public SpreadsheetDate(final int day, final int month, final int year) { + + if ((year >= 1900) && (year <= 9999)) { + this.year = year; + } + else { + throw new IllegalArgumentException( + "The 'year' argument must be in range 1900 to 9999." + ); + } + + if ((month >= MonthConstants.JANUARY) + && (month <= MonthConstants.DECEMBER)) { + this.month = month; + } + else { + throw new IllegalArgumentException( + "The 'month' argument must be in the range 1 to 12." + ); + } + + if ((day >= 1) && (day <= SerialDate.lastDayOfMonth(month, year))) { + this.day = day; + } + else { + throw new IllegalArgumentException("Invalid 'day' argument."); + } + + // the serial number needs to be synchronised with the day-month-year... + this.serial = calcSerial(day, month, year); + + this.description = null; + + } + + /** + * Standard constructor - creates a new date object representing the + * specified day number (which should be in the range 2 to 2958465. + * + * @param serial the serial number for the day (range: 2 to 2958465). + */ + public SpreadsheetDate(final int serial) { + + if ((serial >= SERIAL_LOWER_BOUND) && (serial <= SERIAL_UPPER_BOUND)) { + this.serial = serial; + } + else { + throw new IllegalArgumentException( + "SpreadsheetDate: Serial must be in range 2 to 2958465."); + } + + // the day-month-year needs to be synchronised with the serial number... + calcDayMonthYear(); + + } + + /** + * Returns the description that is attached to the date. It is not + * required that a date have a description, but for some applications it + * is useful. + * + * @return The description that is attached to the date. + */ + public String getDescription() { + return this.description; + } + + /** + * Sets the description for the date. + * + * @param description the description for this date (null + * permitted). + */ + public void setDescription(final String description) { + this.description = description; + } + + /** + * Returns the serial number for the date, where 1 January 1900 = 2 + * (this corresponds, almost, to the numbering system used in Microsoft + * Excel for Windows and Lotus 1-2-3). + * + * @return The serial number of this date. + */ + public int toSerial() { + return this.serial; + } + + /** + * Returns a java.util.Date equivalent to this date. + * + * @return The date. + */ + public Date toDate() { + final Calendar calendar = Calendar.getInstance(); + calendar.set(getYYYY(), getMonth() - 1, getDayOfMonth(), 0, 0, 0); + return calendar.getTime(); + } + + /** + * Returns the year (assume a valid range of 1900 to 9999). + * + * @return The year. + */ + public int getYYYY() { + return this.year; + } + + /** + * Returns the month (January = 1, February = 2, March = 3). + * + * @return The month of the year. + */ + public int getMonth() { + return this.month; + } + + /** + * Returns the day of the month. + * + * @return The day of the month. + */ + public int getDayOfMonth() { + return this.day; + } + + /** + * Returns a code representing the day of the week. + *

+ * The codes are defined in the {@link SerialDate} class as: + * SUNDAY, MONDAY, TUESDAY, + * WEDNESDAY, THURSDAY, FRIDAY, and + * SATURDAY. + * + * @return A code representing the day of the week. + */ + public int getDayOfWeek() { + return (this.serial + 6) % 7 + 1; + } + + /** + * Tests the equality of this date with an arbitrary object. + *

+ * This method will return true ONLY if the object is an instance of the + * {@link SerialDate} base class, and it represents the same day as this + * {@link SpreadsheetDate}. + * + * @param object the object to compare (null permitted). + * + * @return A boolean. + */ + public boolean equals(final Object object) { + + if (object instanceof SerialDate) { + final SerialDate s = (SerialDate) object; + return (s.toSerial() == this.toSerial()); + } + else { + return false; + } + + } + + /** + * Returns a hash code for this object instance. + * + * @return A hash code. + */ + public int hashCode() { + return toSerial(); + } + + /** + * Returns the difference (in days) between this date and the specified + * 'other' date. + * + * @param other the date being compared to. + * + * @return The difference (in days) between this date and the specified + * 'other' date. + */ + public int compare(final SerialDate other) { + return this.serial - other.toSerial(); + } + + /** + * Implements the method required by the Comparable interface. + * + * @param other the other object (usually another SerialDate). + * + * @return A negative integer, zero, or a positive integer as this object + * is less than, equal to, or greater than the specified object. + */ + public int compareTo(final Object other) { + return compare((SerialDate) other); + } + + /** + * Returns true if this SerialDate represents the same date as the + * specified SerialDate. + * + * @param other the date being compared to. + * + * @return true if this SerialDate represents the same date as + * the specified SerialDate. + */ + public boolean isOn(final SerialDate other) { + return (this.serial == other.toSerial()); + } + + /** + * Returns true if this SerialDate represents an earlier date compared to + * the specified SerialDate. + * + * @param other the date being compared to. + * + * @return true if this SerialDate represents an earlier date + * compared to the specified SerialDate. + */ + public boolean isBefore(final SerialDate other) { + return (this.serial < other.toSerial()); + } + + /** + * Returns true if this SerialDate represents the same date as the + * specified SerialDate. + * + * @param other the date being compared to. + * + * @return true if this SerialDate represents the same date + * as the specified SerialDate. + */ + public boolean isOnOrBefore(final SerialDate other) { + return (this.serial <= other.toSerial()); + } + + /** + * Returns true if this SerialDate represents the same date as the + * specified SerialDate. + * + * @param other the date being compared to. + * + * @return true if this SerialDate represents the same date + * as the specified SerialDate. + */ + public boolean isAfter(final SerialDate other) { + return (this.serial > other.toSerial()); + } + + /** + * Returns true if this SerialDate represents the same date as the + * specified SerialDate. + * + * @param other the date being compared to. + * + * @return true if this SerialDate represents the same date as + * the specified SerialDate. + */ + public boolean isOnOrAfter(final SerialDate other) { + return (this.serial >= other.toSerial()); + } + + /** + * Returns true if this {@link SerialDate} is within the + * specified range (INCLUSIVE). The date order of d1 and d2 is not + * important. + * + * @param d1 a boundary date for the range. + * @param d2 the other boundary date for the range. + * + * @return A boolean. + */ + public boolean isInRange(final SerialDate d1, final SerialDate d2) { + return isInRange(d1, d2, SerialDate.INCLUDE_BOTH); + } + + /** + * Returns true if this SerialDate is within the specified range (caller + * specifies whether or not the end-points are included). The order of d1 + * and d2 is not important. + * + * @param d1 one boundary date for the range. + * @param d2 a second boundary date for the range. + * @param include a code that controls whether or not the start and end + * dates are included in the range. + * + * @return true if this SerialDate is within the specified + * range. + */ + public boolean isInRange(final SerialDate d1, final SerialDate d2, + final int include) { + final int s1 = d1.toSerial(); + final int s2 = d2.toSerial(); + final int start = Math.min(s1, s2); + final int end = Math.max(s1, s2); + + final int s = toSerial(); + if (include == SerialDate.INCLUDE_BOTH) { + return (s >= start && s <= end); + } + else if (include == SerialDate.INCLUDE_FIRST) { + return (s >= start && s < end); + } + else if (include == SerialDate.INCLUDE_SECOND) { + return (s > start && s <= end); + } + else { + return (s > start && s < end); + } + } + + /** + * Calculate the serial number from the day, month and year. + *

+ * 1-Jan-1900 = 2. + * + * @param d the day. + * @param m the month. + * @param y the year. + * + * @return the serial number from the day, month and year. + */ + private int calcSerial(final int d, final int m, final int y) { + final int yy = ((y - 1900) * 365) + SerialDate.leapYearCount(y - 1); + int mm = SerialDate.AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH[m]; + if (m > MonthConstants.FEBRUARY) { + if (SerialDate.isLeapYear(y)) { + mm = mm + 1; + } + } + final int dd = d; + return yy + mm + dd + 1; + } + + /** + * Calculate the day, month and year from the serial number. + */ + private void calcDayMonthYear() { + + // get the year from the serial date + final int days = this.serial - SERIAL_LOWER_BOUND; + // overestimated because we ignored leap days + final int overestimatedYYYY = 1900 + (days / 365); + final int leaps = SerialDate.leapYearCount(overestimatedYYYY); + final int nonleapdays = days - leaps; + // underestimated because we overestimated years + int underestimatedYYYY = 1900 + (nonleapdays / 365); + + if (underestimatedYYYY == overestimatedYYYY) { + this.year = underestimatedYYYY; + } + else { + int ss1 = calcSerial(1, 1, underestimatedYYYY); + while (ss1 <= this.serial) { + underestimatedYYYY = underestimatedYYYY + 1; + ss1 = calcSerial(1, 1, underestimatedYYYY); + } + this.year = underestimatedYYYY - 1; + } + + final int ss2 = calcSerial(1, 1, this.year); + + int[] daysToEndOfPrecedingMonth + = AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH; + + if (isLeapYear(this.year)) { + daysToEndOfPrecedingMonth + = LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH; + } + + // get the month from the serial date + int mm = 1; + int sss = ss2 + daysToEndOfPrecedingMonth[mm] - 1; + while (sss < this.serial) { + mm = mm + 1; + sss = ss2 + daysToEndOfPrecedingMonth[mm] - 1; + } + this.month = mm - 1; + + // what's left is d(+1); + this.day = this.serial - ss2 + - daysToEndOfPrecedingMonth[this.month] + 1; + + } + + } \ No newline at end of file diff --git a/src/main/java/clean/code/chapter16/solution/DateInterval.java b/src/main/java/clean/code/chapter16/solution/DateInterval.java new file mode 100644 index 0000000..39925e7 --- /dev/null +++ b/src/main/java/clean/code/chapter16/solution/DateInterval.java @@ -0,0 +1,26 @@ +package clean.code.chapter16.solution; + +public enum DateInterval { + OPEN { + public boolean isIn(int d, int left, int right) { + return d > left && d < right; + } + }, + CLOSED_LEFT { + public boolean isIn(int d, int left, int right) { + return d >= left && d < right; + } + }, + CLOSED_RIGHT { + public boolean isIn(int d, int left, int right) { + return d > left && d <= right; + } + }, + CLOSED { + public boolean isIn(int d, int left, int right) { + return d >= left && d <= right; + } + }; + + public abstract boolean isIn(int d, int left, int right); +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter16/solution/DateUtil.java b/src/main/java/clean/code/chapter16/solution/DateUtil.java new file mode 100644 index 0000000..d580147 --- /dev/null +++ b/src/main/java/clean/code/chapter16/solution/DateUtil.java @@ -0,0 +1,32 @@ +package clean.code.chapter16.solution; + +import java.text.DateFormatSymbols; + +public class DateUtil { + private static DateFormatSymbols dateFormatSymbols = new DateFormatSymbols(); + + public static String[] getMonthNames() { + return dateFormatSymbols.getMonths(); + } + + public static boolean isLeapYear(int year) { + boolean fourth = year % 4 == 0; + boolean hundredth = year % 100 == 0; + boolean fourHundredth = year % 400 == 0; + return fourth && (!hundredth || fourHundredth); + } + + public static int lastDayOfMonth(Month month, int year) { + if (month == Month.FEBRUARY && isLeapYear(year)) + return month.lastDay() + 1; + else + return month.lastDay(); + } + + public static int leapYearCount(int year) { + int leap4 = (year - 1896) / 4; + int leap100 = (year - 1800) / 100; + int leap400 = (year - 1600) / 400; + return leap4 - leap100 + leap400; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter16/solution/Day.java b/src/main/java/clean/code/chapter16/solution/Day.java new file mode 100644 index 0000000..e01b666 --- /dev/null +++ b/src/main/java/clean/code/chapter16/solution/Day.java @@ -0,0 +1,54 @@ +package clean.code.chapter16.solution; + +import java.util.Calendar; +import java.text.DateFormatSymbols; + +public enum Day { + MONDAY(Calendar.MONDAY), + TUESDAY(Calendar.TUESDAY), + WEDNESDAY(Calendar.WEDNESDAY), + THURSDAY(Calendar.THURSDAY), + FRIDAY(Calendar.FRIDAY), + SATURDAY(Calendar.SATURDAY), + SUNDAY(Calendar.SUNDAY); + + private final int index; + private static DateFormatSymbols dateSymbols = new DateFormatSymbols(); + + Day(int day) { + index = day; + } + + public static Day fromInt(int index) throws IllegalArgumentException { + for (Day d : Day.values()) + if (d.index == index) + return d; + throw new IllegalArgumentException( + String.format("Illegal day index: %d.", index)); + } + + public static Day parse(String s) throws IllegalArgumentException { + String[] shortWeekdayNames = + dateSymbols.getShortWeekdays(); + String[] weekDayNames = + dateSymbols.getWeekdays(); + + s = s.trim(); + for (Day day : Day.values()) { + if (s.equalsIgnoreCase(shortWeekdayNames[day.index]) || + s.equalsIgnoreCase(weekDayNames[day.index])) { + return day; + } + } + throw new IllegalArgumentException( + String.format("%s is not a valid weekday string", s)); + } + + public String toString() { + return dateSymbols.getWeekdays()[index]; + } + + public int toInt() { + return index; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter16/solution/DayDate.java b/src/main/java/clean/code/chapter16/solution/DayDate.java new file mode 100644 index 0000000..c961d67 --- /dev/null +++ b/src/main/java/clean/code/chapter16/solution/DayDate.java @@ -0,0 +1,154 @@ +package clean.code.chapter16.solution; + +/* ======================================================================== + * JCommon : a free general purpose class library for the Java(tm) platform + * ======================================================================== + * + * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors. +. +*/ + +import java.io.Serializable; +import java.util.*; + +/** + * An abstract class that represents immutable dates with a precision of + * one day. The implementation will map each date to an integer that + * represents an ordinal number of days from some fixed origin. + *

+ * Why not just use java.util.Date? We will, when it makes sense. At times, + * java.util.Date can be *too* precise - it represents an instant in time, + * accurate to 1/1000th of a second (with the date itself depending on the + * time-zone). Sometimes we just want to represent a particular day (e.g. 21 + * January 2015) without concerning ourselves about the time of day, or the + * time-zone, or anything else. That's what we've defined DayDate for. + *

+ * Use DayDateFactory.makeDate to create an instance. + * + * @author David Gilbert + * @author Robert C. Martin did a lot of refactoring. + */ + +public abstract class DayDate implements Comparable, Serializable { + public abstract int getOrdinalDay(); + + public abstract int getYear(); + + public abstract Month getMonth(); + + public abstract int getDayOfMonth(); + + protected abstract Day getDayOfWeekForOrdinalZero(); + + public DayDate plusDays(int days) { + return DayDateFactory.makeDate(getOrdinalDay() + days); + } + + public DayDate plusMonths(int months) { + int thisMonthAsOrdinal = getMonth().toInt() - Month.JANUARY.toInt(); + int thisMonthAndYearAsOrdinal = 12 * getYear() + thisMonthAsOrdinal; + int resultMonthAndYearAsOrdinal = thisMonthAndYearAsOrdinal + months; + int resultYear = resultMonthAndYearAsOrdinal / 12; + int resultMonthAsOrdinal = resultMonthAndYearAsOrdinal % 12 + Month.JANUARY.toInt(); + Month resultMonth = Month.fromInt(resultMonthAsOrdinal); + int resultDay = correctLastDayOfMonth(getDayOfMonth(), resultMonth, resultYear); + return DayDateFactory.makeDate(resultDay, resultMonth, resultYear); + } + + public DayDate plusYears(int years) { + int resultYear = getYear() + years; + int resultDay = correctLastDayOfMonth(getDayOfMonth(), getMonth(), resultYear); + return DayDateFactory.makeDate(resultDay, getMonth(), resultYear); + } + + private int correctLastDayOfMonth(int day, Month month, int year) { + int lastDayOfMonth = DateUtil.lastDayOfMonth(month, year); + if (day > lastDayOfMonth) + day = lastDayOfMonth; + return day; + } + + public DayDate getPreviousDayOfWeek(Day targetDayOfWeek) { + int offsetToTarget = targetDayOfWeek.toInt() - getDayOfWeek().toInt(); + if (offsetToTarget >= 0) + offsetToTarget -= 7; + return plusDays(offsetToTarget); + } + + public DayDate getFollowingDayOfWeek(Day targetDayOfWeek) { + int offsetToTarget = targetDayOfWeek.toInt() - getDayOfWeek().toInt(); + if (offsetToTarget <= 0) + offsetToTarget += 7; + return plusDays(offsetToTarget); + } + + public DayDate getNearestDayOfWeek(Day targetDayOfWeek) { + int offsetToThisWeeksTarget = targetDayOfWeek.toInt() - getDayOfWeek().toInt(); + int offsetToFutureTarget = (offsetToThisWeeksTarget + 7) % 7; + int offsetToPreviousTarget = offsetToFutureTarget - 7; + + if (offsetToFutureTarget > 3) + return plusDays(offsetToPreviousTarget); + else + return plusDays(offsetToFutureTarget); + } + + public DayDate getEndOfMonth() { + Month month = getMonth(); + int year = getYear(); + int lastDay = DateUtil.lastDayOfMonth(month, year); + return DayDateFactory.makeDate(lastDay, month, year); + } + + public Date toDate() { + final Calendar calendar = Calendar.getInstance(); + int ordinalMonth = getMonth().toInt() - Month.JANUARY.toInt(); + calendar.set(getYear(), ordinalMonth, getDayOfMonth(), 0, 0, 0); + return calendar.getTime(); + } + + public String toString() { + return String.format("%02d-%s-%d", getDayOfMonth(), getMonth(), getYear()); + } + + public Day getDayOfWeek() { + Day startingDay = getDayOfWeekForOrdinalZero(); + int startingOffset = startingDay.toInt() - Day.SUNDAY.toInt(); + int ordinalOfDayOfWeek = (getOrdinalDay() + startingOffset) % 7; + return Day.fromInt(ordinalOfDayOfWeek + Day.SUNDAY.toInt()); + } + + public int daysSince(DayDate date) { + return getOrdinalDay() - date.getOrdinalDay(); + } + + public boolean isOn(DayDate other) { + return getOrdinalDay() == other.getOrdinalDay(); + } + + public boolean isBefore(DayDate other) { + return getOrdinalDay() < other.getOrdinalDay(); + } + + public boolean isOnOrBefore(DayDate other) { + return getOrdinalDay() <= other.getOrdinalDay(); + } + + public boolean isAfter(DayDate other) { + return getOrdinalDay() > other.getOrdinalDay(); + } + + public boolean isOnOrAfter(DayDate other) { + return getOrdinalDay() >= other.getOrdinalDay(); + } + + public boolean isInRange(DayDate d1, DayDate d2) { + return isInRange(d1, d2, DateInterval.CLOSED); + } + + public boolean isInRange(DayDate d1, DayDate d2, DateInterval interval) { + int left = Math.min(d1.getOrdinalDay(), d2.getOrdinalDay()); + int right = Math.max(d1.getOrdinalDay(), d2.getOrdinalDay()); + return interval.isIn(getOrdinalDay(), left, right); + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter16/solution/DayDateFactory.java b/src/main/java/clean/code/chapter16/solution/DayDateFactory.java new file mode 100644 index 0000000..c27a7f6 --- /dev/null +++ b/src/main/java/clean/code/chapter16/solution/DayDateFactory.java @@ -0,0 +1,45 @@ +package clean.code.chapter16.solution; + +public abstract class DayDateFactory { + private static DayDateFactory factory = new SpreadsheetDateFactory(); + + public static void setInstance(DayDateFactory factory) { + DayDateFactory.factory = factory; + } + + protected abstract DayDate _makeDate(int ordinal); + + protected abstract DayDate _makeDate(int day, Month month, int year); + + protected abstract DayDate _makeDate(int day, int month, int year); + + protected abstract DayDate _makeDate(java.util.Date date); + + protected abstract int _getMinimumYear(); + + protected abstract int _getMaximumYear(); + + public static DayDate makeDate(int ordinal) { + return factory._makeDate(ordinal); + } + + public static DayDate makeDate(int day, Month month, int year) { + return factory._makeDate(day, month, year); + } + + public static DayDate makeDate(int day, int month, int year) { + return factory._makeDate(day, month, year); + } + + public static DayDate makeDate(java.util.Date date) { + return factory._makeDate(date); + } + + public static int getMinimumYear() { + return factory._getMinimumYear(); + } + + public static int getMaximumYear() { + return factory._getMaximumYear(); + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter16/solution/Month.java b/src/main/java/clean/code/chapter16/solution/Month.java new file mode 100644 index 0000000..3c4228c --- /dev/null +++ b/src/main/java/clean/code/chapter16/solution/Month.java @@ -0,0 +1,74 @@ +package clean.code.chapter16.solution; + +import java.text.DateFormatSymbols; + +public enum Month { + JANUARY(1), + FEBRUARY(2), + MARCH(3), + APRIL(4), + MAY(5), + JUNE(6), + JULY(7), + AUGUST(8), + SEPTEMBER(9), + OCTOBER(10), + NOVEMBER(11), + DECEMBER(12); + + private static DateFormatSymbols dateFormatSymbols = new DateFormatSymbols(); + private static final int[] LAST_DAY_OF_MONTH = + {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + + private int index; + + Month(int index) { + this.index = index; + } + + public static Month fromInt(int monthIndex) { + for (Month m : Month.values()) { + if (m.index == monthIndex) + return m; + } + throw new IllegalArgumentException("Invalid month index " + monthIndex); + } + + public int lastDay() { + return LAST_DAY_OF_MONTH[index]; + } + + public int quarter() { + return 1 + (index - 1) / 3; + } + + public String toString() { + return dateFormatSymbols.getMonths()[index - 1]; + } + + public String toShortString() { + return dateFormatSymbols.getShortMonths()[index - 1]; + } + + public static Month parse(String s) { + s = s.trim(); + for (Month m : Month.values()) + if (m.matches(s)) + return m; + + try { + return fromInt(Integer.parseInt(s)); + } catch (NumberFormatException e) { + } + throw new IllegalArgumentException("Invalid month " + s); + } + + private boolean matches(String s) { + return s.equalsIgnoreCase(toString()) || + s.equalsIgnoreCase(toShortString()); + } + + public int toInt() { + return index; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter16/solution/RelativeDayOfWeekRule.java b/src/main/java/clean/code/chapter16/solution/RelativeDayOfWeekRule.java new file mode 100644 index 0000000..a0c6f18 --- /dev/null +++ b/src/main/java/clean/code/chapter16/solution/RelativeDayOfWeekRule.java @@ -0,0 +1,212 @@ +package clean.code.chapter16.solution; + +/* ======================================================================== + * JCommon : a free general purpose class library for the Java(tm) platform + * ======================================================================== + * + * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors. + * + * Project Info: http://www.jfree.org/jcommon/index.html + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * [Java is a trademark or registered trademark of Sun Microsystems, Inc. + * in the United States and other countries.] + * + * -------------------------- + * RelativeDayOfWeekRule.java + * -------------------------- + * (C) Copyright 2000-2003, by Object Refinery Limited and Contributors. + * + * Original Author: David Gilbert (for Object Refinery Limited); + * Contributor(s): -; + * + * $Id: RelativeDayOfWeekRule.java,v 1.6 2005/11/16 15:58:40 taqua Exp $ + * + * Changes (from 26-Oct-2001) + * -------------------------- + * 26-Oct-2001 : Changed package to com.jrefinery.date.*; + * 03-Oct-2002 : Fixed errors reported by Checkstyle (DG); + * + */ + +import clean.code.added.to.make.code.build.AnnualDateRule; +import clean.code.added.to.make.code.build.DayAndMonthRule; +import clean.code.added.to.make.code.build.SerialDate; + +/** + * An annual date rule that returns a date for each year based on (a) a + * reference rule; (b) a day of the week; and (c) a selection parameter + * (SerialDate.PRECEDING, SerialDate.NEAREST, SerialDate.FOLLOWING). + *

+ * For example, Good Friday can be specified as 'the Friday PRECEDING Easter + * Sunday'. + * + * @author David Gilbert + */ + public class RelativeDayOfWeekRule extends AnnualDateRule { + + /** A reference to the annual date rule on which this rule is based. */ + private AnnualDateRule subrule; + + /** + * The day of the week (SerialDate.MONDAY, SerialDate.TUESDAY, and so on). + */ + private int dayOfWeek; + + /** Specifies which day of the week (PRECEDING, NEAREST or FOLLOWING). */ + private int relative; + + /** + * Default constructor - builds a rule for the Monday following 1 January. + */ + public RelativeDayOfWeekRule() { + this(new DayAndMonthRule(), SerialDate.MONDAY, SerialDate.FOLLOWING); + } + + /** + * Standard constructor - builds rule based on the supplied sub-rule. + * + * @param subrule the rule that determines the reference date. + * @param dayOfWeek the day-of-the-week relative to the reference date. + * @param relative indicates *which* day-of-the-week (preceding, nearest + * or following). + */ + public RelativeDayOfWeekRule(final AnnualDateRule subrule, + final int dayOfWeek, final int relative) { + this.subrule = subrule; + this.dayOfWeek = dayOfWeek; + this.relative = relative; + } + + /** + * Returns the sub-rule (also called the reference rule). + * + * @return The annual date rule that determines the reference date for this + * rule. + */ + public AnnualDateRule getSubrule() { + return this.subrule; + } + + /** + * Sets the sub-rule. + * + * @param subrule the annual date rule that determines the reference date + * for this rule. + */ + public void setSubrule(final AnnualDateRule subrule) { + this.subrule = subrule; + } + + /** + * Returns the day-of-the-week for this rule. + * + * @return the day-of-the-week for this rule. + */ + public int getDayOfWeek() { + return this.dayOfWeek; + } + + /** + * Sets the day-of-the-week for this rule. + * + * @param dayOfWeek the day-of-the-week (SerialDate.MONDAY, + * SerialDate.TUESDAY, and so on). + */ + public void setDayOfWeek(final int dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } + + /** + * Returns the 'relative' attribute, that determines *which* + * day-of-the-week we are interested in (SerialDate.PRECEDING, + * SerialDate.NEAREST or SerialDate.FOLLOWING). + * + * @return The 'relative' attribute. + */ + public int getRelative() { + return this.relative; + } + + /** + * Sets the 'relative' attribute (SerialDate.PRECEDING, SerialDate.NEAREST, + * SerialDate.FOLLOWING). + * + * @param relative determines *which* day-of-the-week is selected by this + * rule. + */ + public void setRelative(final int relative) { + this.relative = relative; + } + + /** + * Creates a clone of this rule. + * + * @return a clone of this rule. + * + * @throws CloneNotSupportedException this should never happen. + */ + public Object clone() throws CloneNotSupportedException { + final RelativeDayOfWeekRule duplicate + = (RelativeDayOfWeekRule) super.clone(); + duplicate.subrule = (AnnualDateRule) duplicate.getSubrule().clone(); + return duplicate; + } + + /** + * Returns the date generated by this rule, for the specified year. + * + * @param year the year (1900 <= year <= 9999). + * + * @return The date generated by the rule for the given year (possibly + * null). + */ + public SerialDate getDate(final int year) { + + // check argument... + if ((year < SerialDate.MINIMUM_YEAR_SUPPORTED) + || (year > SerialDate.MAXIMUM_YEAR_SUPPORTED)) { + throw new IllegalArgumentException( + "RelativeDayOfWeekRule.getDate(): year outside valid range."); + } + + // calculate the date... + SerialDate result = null; + final SerialDate base = this.subrule.getDate(year); + + if (base != null) { + switch (this.relative) { + case(SerialDate.PRECEDING): + result = SerialDate.getPreviousDayOfWeek(this.dayOfWeek, + base); + break; + case(SerialDate.NEAREST): + result = SerialDate.getNearestDayOfWeek(this.dayOfWeek, + base); + break; + case(SerialDate.FOLLOWING): + result = SerialDate.getFollowingDayOfWeek(this.dayOfWeek, + base); + break; + default: + break; + } + } + return result; + + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter16/solution/SpreadsheetDate.java b/src/main/java/clean/code/chapter16/solution/SpreadsheetDate.java new file mode 100644 index 0000000..4933d05 --- /dev/null +++ b/src/main/java/clean/code/chapter16/solution/SpreadsheetDate.java @@ -0,0 +1,169 @@ +package clean.code.chapter16.solution; + +/* ======================================================================== + * JCommon : a free general purpose class library for the Java(tm) platform + * ======================================================================== + * + * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors. + * + + * + */ + + import java.util.*; + import static clean.code.chapter16.solution.Month.FEBRUARY; + /** + * Represents a date using an integer, in a similar fashion to the + * implementation in Microsoft Excel. The range of dates supported is + * 1-Jan-1900 to 31-Dec-9999. + *

+ * Be aware that there is a deliberate bug in Excel that recognises the year + * 1900 as a leap year when in fact it is not a leap year. You can find more + * information on the Microsoft website in article Q181370: + *

+ * http://support.microsoft.com/support/kb/articles/Q181/3/70.asp + *

+ * Excel uses the convention that 1-Jan-1900 = 1. This class uses the + * convention 1-Jan-1900 = 2. + * The result is that the day number in this class will be different to the + * Excel figure for January and February 1900...but then Excel adds in an extra + * day (29-Feb-1900 which does not actually exist!) and from that point forward + * the day numbers will match. + * + * @author David Gilbert + */ + public class SpreadsheetDate extends DayDate { + public static final int EARLIEST_DATE_ORDINAL = 2; // 1/1/1900 + public static final int LATEST_DATE_ORDINAL = 2958465; // 12/31/9999 + public static final int MINIMUM_YEAR_SUPPORTED = 1900; + public static final int MAXIMUM_YEAR_SUPPORTED = 9999; + static final int[] AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH = + {0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}; + static final int[] LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH = + {0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}; + + private int ordinalDay; + private int day; + private Month month; + private int year; + + public SpreadsheetDate(int day, Month month, int year) { + if (year < MINIMUM_YEAR_SUPPORTED || year > MAXIMUM_YEAR_SUPPORTED) + throw new IllegalArgumentException( + "The 'year' argument must be in range " + + MINIMUM_YEAR_SUPPORTED + " to " + MAXIMUM_YEAR_SUPPORTED + "."); + if (day < 1 || day > DateUtil.lastDayOfMonth(month, year)) + throw new IllegalArgumentException("Invalid 'day' argument."); + + this.year = year; + this.month = month; + this.day = day; + ordinalDay = calcOrdinal(day, month, year); + } + + public SpreadsheetDate(int day, int month, int year) { + this(day, Month.fromInt(month), year); + } + + public SpreadsheetDate(int serial) { + if (serial < EARLIEST_DATE_ORDINAL || serial > LATEST_DATE_ORDINAL) + throw new IllegalArgumentException( + "SpreadsheetDate: Serial must be in range 2 to 2958465."); + + ordinalDay = serial; + calcDayMonthYear(); + } + + public int getOrdinalDay() { + return ordinalDay; + } + + public int getYear() { + return year; + } + + public Month getMonth() { + return month; + } + + public int getDayOfMonth() { + return day; + } + + protected Day getDayOfWeekForOrdinalZero() {return Day.SATURDAY;} + + public boolean equals(Object object) { + if (!(object instanceof DayDate)) + return false; + + DayDate date = (DayDate) object; + return date.getOrdinalDay() == getOrdinalDay(); + } + + public int hashCode() { + return getOrdinalDay(); + } + + public int compareTo(Object other) { + return daysSince((DayDate) other); + } + + private int calcOrdinal(int day, Month month, int year) { + int leapDaysForYear = DateUtil.leapYearCount(year - 1); + int daysUpToYear = (year - MINIMUM_YEAR_SUPPORTED) * 365 + leapDaysForYear; + int daysUpToMonth = AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH[month.toInt()]; + if (DateUtil.isLeapYear(year) && month.toInt() > FEBRUARY.toInt()) + daysUpToMonth++; + int daysInMonth = day - 1; + return daysUpToYear + daysUpToMonth + daysInMonth + EARLIEST_DATE_ORDINAL; + } + + private void calcDayMonthYear() { + int days = ordinalDay - EARLIEST_DATE_ORDINAL; + int overestimatedYear = MINIMUM_YEAR_SUPPORTED + days / 365; + int nonleapdays = days - DateUtil.leapYearCount(overestimatedYear); + int underestimatedYear = MINIMUM_YEAR_SUPPORTED + nonleapdays / 365; + + year = huntForYearContaining(ordinalDay, underestimatedYear); + int firstOrdinalOfYear = firstOrdinalOfYear(year); + month = huntForMonthContaining(ordinalDay, firstOrdinalOfYear); + day = ordinalDay - firstOrdinalOfYear - daysBeforeThisMonth(month.toInt()); + } + + private Month huntForMonthContaining(int anOrdinal, int firstOrdinalOfYear) { + int daysIntoThisYear = anOrdinal - firstOrdinalOfYear; + int aMonth = 1; + while (daysBeforeThisMonth(aMonth) < daysIntoThisYear) + aMonth++; + + return Month.fromInt(aMonth - 1); + } + + private int daysBeforeThisMonth(int aMonth) { + if (DateUtil.isLeapYear(year)) + return LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH[aMonth] - 1; + else + return AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH[aMonth] - 1; + } + + private int huntForYearContaining(int anOrdinalDay, int startingYear) { + int aYear = startingYear; + while (firstOrdinalOfYear(aYear) <= anOrdinalDay) + aYear++; + + return aYear - 1; + } + + private int firstOrdinalOfYear(int year) { + return calcOrdinal(1, Month.JANUARY, year); + } + + public static DayDate createInstance(Date date) { + GregorianCalendar calendar = new GregorianCalendar(); + calendar.setTime(date); + return new SpreadsheetDate(calendar.get(Calendar.DATE), + Month.fromInt(calendar.get(Calendar.MONTH) + 1), + calendar.get(Calendar.YEAR)); + + } + } \ No newline at end of file diff --git a/src/main/java/clean/code/chapter16/solution/SpreadsheetDateFactory.java b/src/main/java/clean/code/chapter16/solution/SpreadsheetDateFactory.java new file mode 100644 index 0000000..df801c1 --- /dev/null +++ b/src/main/java/clean/code/chapter16/solution/SpreadsheetDateFactory.java @@ -0,0 +1,34 @@ +package clean.code.chapter16.solution; + +import java.util.*; + +public class SpreadsheetDateFactory extends DayDateFactory { + public DayDate _makeDate(int ordinal) { + return new SpreadsheetDate(ordinal); + } + + public DayDate _makeDate(int day, Month month, int year) { + return new SpreadsheetDate(day, month, year); + } + + public DayDate _makeDate(int day, int month, int year) { + return new SpreadsheetDate(day, month, year); + } + + public DayDate _makeDate(Date date) { + final GregorianCalendar calendar = new GregorianCalendar(); + calendar.setTime(date); + return new SpreadsheetDate( + calendar.get(Calendar.DATE), + Month.fromInt(calendar.get(Calendar.MONTH) + 1), + calendar.get(Calendar.YEAR)); + } + + protected int _getMinimumYear() { + return SpreadsheetDate.MINIMUM_YEAR_SUPPORTED; + } + + protected int _getMaximumYear() { + return SpreadsheetDate.MAXIMUM_YEAR_SUPPORTED; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter16/solution/WeekInMonth.java b/src/main/java/clean/code/chapter16/solution/WeekInMonth.java new file mode 100644 index 0000000..bdd188e --- /dev/null +++ b/src/main/java/clean/code/chapter16/solution/WeekInMonth.java @@ -0,0 +1,14 @@ +package clean.code.chapter16.solution; + +public enum WeekInMonth { + FIRST(1), SECOND(2), THIRD(3), FOURTH(4), LAST(0); + private final int index; + + WeekInMonth(int index) { + this.index = index; + } + + public int toInt() { + return index; + } +} \ No newline at end of file diff --git a/src/main/java/clean/code/chapter16/solution/WeekdayRange.java b/src/main/java/clean/code/chapter16/solution/WeekdayRange.java new file mode 100644 index 0000000..ef9d2cb --- /dev/null +++ b/src/main/java/clean/code/chapter16/solution/WeekdayRange.java @@ -0,0 +1,5 @@ +package clean.code.chapter16.solution; + +public enum WeekdayRange { + LAST, NEAREST, NEXT +} \ No newline at end of file diff --git a/src/test/java/appendixA/ClassWithThreadingProblemTest.java b/src/test/java/appendixA/ClassWithThreadingProblemTest.java new file mode 100644 index 0000000..0e016e4 --- /dev/null +++ b/src/test/java/appendixA/ClassWithThreadingProblemTest.java @@ -0,0 +1,39 @@ +package appendixA; + +import static org.junit.Assert.fail; + +import clean.code.appendixA.ClassWithThreadingProblem; +import org.junit.Test; + +public class ClassWithThreadingProblemTest { + @Test + public void twoThreadsShouldFailEventually() throws Exception { + final ClassWithThreadingProblem classWithThreadingProblem + = new ClassWithThreadingProblem(); + + Runnable runnable = new Runnable() { + public void run() { + classWithThreadingProblem.takeNextId(); + } + }; + + for (int i = 0; i < 50000; ++i) { + int startingId = classWithThreadingProblem.nextId; + int expectedResult = 2 + startingId; + + Thread t1 = new Thread(runnable); + Thread t2 = new Thread(runnable); + t1.start(); + t2.start(); + t1.join(); + t2.join(); + + int endingId = classWithThreadingProblem.nextId; + + if (endingId != expectedResult) + return; + } + + fail("Should have exposed a threading issue but it did not."); + } +} \ No newline at end of file diff --git a/src/test/java/appendixA/nonthreaded/ClientTest.java b/src/test/java/appendixA/nonthreaded/ClientTest.java new file mode 100644 index 0000000..07cfe63 --- /dev/null +++ b/src/test/java/appendixA/nonthreaded/ClientTest.java @@ -0,0 +1,78 @@ +package appendixA.nonthreaded; + +import java.io.IOException; +import java.net.Socket; + +import clean.code.appendixA.MessageUtils; +import clean.code.appendixA.nonthreaded.Server; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class ClientTest { + private static final int PORT = 8009; + private static final int TIMEOUT = 2000; + + Server server; + Thread serverThread; + + @Before + public void createServer() throws Exception { + try { + server = new Server(PORT, TIMEOUT); + serverThread = new Thread(server); + serverThread.start(); + } catch (Exception e) { + e.printStackTrace(System.err); + throw e; + } + } + + @After + public void shutdownServer() throws InterruptedException { + if (server != null) { + server.stopProcessing(); + serverThread.join(); + } + } + + class TrivialClient implements Runnable { + int clientNumber; + + TrivialClient(int clientNumber) { + this.clientNumber = clientNumber; + } + + public void run() { + try { + connectSendReceive(clientNumber); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Test(timeout = 10000) + public void shouldRunInUnder10Seconds() throws Exception { + Thread[] threads = new Thread[10]; + for (int i = 0; i < threads.length; ++i) { + threads[i] = new Thread(new TrivialClient(i)); + threads[i].start(); + } + + for (int i = 0; i < threads.length; ++i) { + threads[i].join(); + } + } + + private void connectSendReceive(int i) throws IOException { + System.out.printf("Client %2d: connecting\n", i); + Socket socket = new Socket("localhost", PORT); + System.out.printf("Client %2d: sending message\n", i); + MessageUtils.sendMessage(socket, Integer.toString(i)); + System.out.printf("Client %2d: getting reply\n", i); + MessageUtils.getMessage(socket); + System.out.printf("Client %2d: finished\n", i); + socket.close(); + } +} \ No newline at end of file diff --git a/src/test/java/appendixA/threaded/ClientTest.java b/src/test/java/appendixA/threaded/ClientTest.java new file mode 100644 index 0000000..03a5fc7 --- /dev/null +++ b/src/test/java/appendixA/threaded/ClientTest.java @@ -0,0 +1,78 @@ +package appendixA.threaded; + +import clean.code.appendixA.MessageUtils; +import clean.code.appendixA.threaded.Server; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.net.Socket; + +public class ClientTest { + private static final int PORT = 8009; + private static final int TIMEOUT = 2000; + + Server server; + Thread serverThread; + + @Before + public void createServer() throws Exception { + try { + server = new Server(PORT, TIMEOUT); + serverThread = new Thread(server); + serverThread.start(); + } catch (Exception e) { + e.printStackTrace(System.err); + throw e; + } + } + + @After + public void shutdownServer() throws InterruptedException { + if (server != null) { + server.stopProcessing(); + serverThread.join(); + } + } + + class TrivialClient implements Runnable { + int clientNumber; + + TrivialClient(int clientNumber) { + this.clientNumber = clientNumber; + } + + public void run() { + try { + connectSendReceive(clientNumber); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Test(timeout = 10000) + public void shouldRunInUnder10Seconds() throws Exception { + Thread[] threads = new Thread[10]; + for (int i = 0; i < threads.length; ++i) { + threads[i] = new Thread(new TrivialClient(i)); + threads[i].start(); + } + + for (int i = 0; i < threads.length; ++i) { + threads[i].join(); + } + } + + private void connectSendReceive(int i) throws IOException { + System.out.printf("Client %2d: connecting\n", i); + Socket socket = new Socket("localhost", PORT); + System.out.printf("Client %2d: sending message\n", i); + MessageUtils.sendMessage(socket, Integer.toString(i)); + System.out.printf("Client %2d: getting reply\n", i); + MessageUtils.getMessage(socket); + System.out.printf("Client %2d: finished\n", i); + socket.close(); + } +} \ No newline at end of file diff --git a/src/test/java/chapter14/draft/bool/and/str/ArgsTest.java b/src/test/java/chapter14/draft/bool/and/str/ArgsTest.java new file mode 100644 index 0000000..9852665 --- /dev/null +++ b/src/test/java/chapter14/draft/bool/and/str/ArgsTest.java @@ -0,0 +1,130 @@ +package chapter14.draft.bool.and.str; + +import clean.code.chapter14.draft.bool.and.str.Args; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.text.ParseException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(JUnit4.class) +public class ArgsTest { + + @Test + public void testCreateWithNoSchemaOrArguments() throws Exception { + Args args = new Args("", new String[0]); + assertEquals(0, args.cardinality()); + } + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void testWithNoSchemaButWithOneArgument() throws Exception { + Args args = new Args("", new String[]{"-x"}); + assertEquals(false, args.isValid()); + assertEquals("Argument(s) -x unexpected.", args.errorMessage()); + } + + @Test + public void testWithNoSchemaButWithMultipleArguments() throws Exception { + Args args = new Args("", new String[]{"-x", "-y"}); + assertEquals(false, args.isValid()); + assertEquals("Argument(s) -xy unexpected.", args.errorMessage()); + } + + // FIXED! + @Test + public void testNonLetterSchema() throws Exception { + exception.expect(ParseException.class); + exception.expectMessage("Bad character:*in Args format: *"); + + new Args("*", new String[]{}); + } + + // Currently fails... + @Test + public void testInvalidArgumentFormat() throws Exception { + exception.expect(ParseException.class); + exception.expectMessage("Argument: f has invalid format: ~."); + + new Args("f~", new String[]{}); + } + + @Test + public void testSimpleBooleanTruePresent() throws Exception { + Args args = new Args("x", new String[]{"-x", "true"}); + assertEquals(1, args.cardinality()); + assertEquals(true, args.getBoolean('x')); + } + + // Currently fails... + @Test + public void testSimpleBooleanFalsePresent() throws Exception { + Args args = new Args("x", new String[]{"-x", "false"}); + assertEquals(1, args.cardinality()); + assertEquals(false, args.getBoolean('x')); + } + + // Currently fails... + @Test + public void testMissingBooleanArgument() throws Exception { + Args args = new Args("x", new String[]{"-x"}); + assertEquals(false, args.isValid()); + } + + // Currently fails... + @Test + public void testInvalidBoolean() throws Exception { + Args args = new Args("x", new String[]{"-x", "Truthy"}); + assertEquals(1, args.cardinality()); + assertEquals(false, args.getBoolean('x')); + } + + @Test + public void testMultipleBooleans() throws Exception { + Args args = new Args("x,y", new String[]{"-xy", "true", "true"}); + assertEquals(2, args.cardinality()); + assertEquals(true, args.getBoolean('x')); + assertEquals(true, args.getBoolean('y')); + } + + // FIXED! + @Test + public void testSpacesInFormat() throws Exception { + Args args = new Args("x, y", new String[]{"-xy", "true", "true"}); + assertEquals(2, args.cardinality()); + assertTrue(args.has('x')); + assertTrue(args.has('y')); + assertEquals(true, args.getBoolean('x')); + assertEquals(true, args.getBoolean('y')); + } + + // Currently fails... + @Test + public void testInvalidArgumentValueFormat() throws Exception { + Args args = new Args("x,y", new String[]{"xy", "true", "false"}); + assertEquals(false, args.isValid()); + } + + @Test + public void testSimpleStringPresent() throws Exception { + Args args = new Args("x*", new String[]{"-x", "param"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals("param", args.getString('x')); + } + + @Test + public void testMissingStringArgument() throws Exception { + Args args = new Args("x*", new String[]{"-x"}); + assertEquals(false, args.isValid()); + assertEquals("Could not find string parameter for -x.", + args.errorMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/chapter14/draft/bool/only/ArgsTest.java b/src/test/java/chapter14/draft/bool/only/ArgsTest.java new file mode 100644 index 0000000..573b144 --- /dev/null +++ b/src/test/java/chapter14/draft/bool/only/ArgsTest.java @@ -0,0 +1,111 @@ +package chapter14.draft.bool.only; + +import clean.code.chapter14.draft.bool.only.Args; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.text.ParseException; + +import static org.junit.Assert.assertEquals; + +@RunWith(JUnit4.class) +public class ArgsTest { + + @Test + public void testCreateWithNoSchemaOrArguments() throws Exception { + Args args = new Args("", new String[0]); + assertEquals(0, args.cardinality()); + } + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void testWithNoSchemaButWithOneArgument() throws Exception { + Args args = new Args("", new String[]{"-x"}); + assertEquals(false, args.isValid()); + assertEquals("Argument(s) -x unexpected.", args.errorMessage()); + } + + @Test + public void testWithNoSchemaButWithMultipleArguments() throws Exception { + Args args = new Args("", new String[]{"-x", "-y"}); + assertEquals(false, args.isValid()); + assertEquals("Argument(s) -xy unexpected.", args.errorMessage()); + } + + // Currently fails... + @Test + public void testNonLetterSchema() throws Exception { + exception.expect(ParseException.class); + exception.expectMessage("Bad character:*in Args format: *"); + + new Args("*", new String[]{}); + } + + // Currently fails... + @Test + public void testInvalidArgumentFormat() throws Exception { + exception.expect(ParseException.class); + exception.expectMessage("Argument: f has invalid format: ~."); + + new Args("f~", new String[]{}); + } + + @Test + public void testSimpleBooleanTruePresent() throws Exception { + Args args = new Args("x", new String[]{"-x", "true"}); + assertEquals(1, args.cardinality()); + assertEquals(true, args.getBoolean('x')); + } + + // Currently fails... + @Test + public void testSimpleBooleanFalsePresent() throws Exception { + Args args = new Args("x", new String[]{"-x", "false"}); + assertEquals(1, args.cardinality()); + assertEquals(false, args.getBoolean('x')); + } + + // Currently fails... + @Test + public void testMissingBooleanArgument() throws Exception { + Args args = new Args("x", new String[]{"-x"}); + assertEquals(false, args.isValid()); + } + + // Currently fails... + @Test + public void testInvalidBoolean() throws Exception { + Args args = new Args("x", new String[]{"-x", "Truthy"}); + assertEquals(1, args.cardinality()); + assertEquals(false, args.getBoolean('x')); + } + + @Test + public void testMultipleBooleans() throws Exception { + Args args = new Args("x,y", new String[]{"-xy", "true", "true"}); + assertEquals(2, args.cardinality()); + assertEquals(true, args.getBoolean('x')); + assertEquals(true, args.getBoolean('y')); + } + + // Currently fails... + @Test + public void testSpacesInFormat() throws Exception { + Args args = new Args("x, y", new String[]{"-xy", "true", "true"}); + assertEquals(2, args.cardinality()); + assertEquals(true, args.getBoolean('x')); + assertEquals(true, args.getBoolean('y')); + } + + // Currently fails... + @Test + public void testInvalidArgumentValueFormat() throws Exception { + Args args = new Args("x,y", new String[]{"xy", "true", "false"}); + assertEquals(false, args.isValid()); + } +} \ No newline at end of file diff --git a/src/test/java/chapter14/needs/refactoring/ArgsTest.java b/src/test/java/chapter14/needs/refactoring/ArgsTest.java new file mode 100644 index 0000000..b34c439 --- /dev/null +++ b/src/test/java/chapter14/needs/refactoring/ArgsTest.java @@ -0,0 +1,153 @@ +package chapter14.needs.refactoring; + +import clean.code.chapter14.draft.needs.refactoring.Args; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.text.ParseException; + +import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; + +@RunWith(JUnit4.class) +public class ArgsTest { + + @Test + public void testCreateWithNoSchemaOrArguments() throws Exception { + Args args = new Args("", new String[0]); + assertEquals(0, args.cardinality()); + } + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void testWithNoSchemaButWithOneArgument() throws Exception { + Args args = new Args("", new String[]{"-x"}); + assertEquals(false, args.isValid()); + assertEquals("Argument(s) -x unexpected.", args.errorMessage()); + } + + @Test + public void testWithNoSchemaButWithMultipleArguments() throws Exception { + Args args = new Args("", new String[]{"-x", "-y"}); + assertEquals(false, args.isValid()); + assertEquals("Argument(s) -xy unexpected.", args.errorMessage()); + } + + @Test + public void testNonLetterSchema() throws Exception { + exception.expect(ParseException.class); + exception.expectMessage("Bad character:*in Args format: *"); + + new Args("*", new String[]{}); + } + + // FIXED! + @Test + public void testInvalidArgumentFormat() throws Exception { + exception.expect(ParseException.class); + exception.expectMessage("Argument: f has invalid format: ~."); + + new Args("f~", new String[]{}); + } + + @Test + public void testSimpleBooleanTruePresent() throws Exception { + Args args = new Args("x", new String[]{"-x", "true"}); + assertEquals(1, args.cardinality()); + assertEquals(true, args.getBoolean('x')); + } + + // Currently fails... + @Test + public void testSimpleBooleanFalsePresent() throws Exception { + Args args = new Args("x", new String[]{"-x", "false"}); + assertEquals(1, args.cardinality()); + assertEquals(false, args.getBoolean('x')); + } + + // Currently fails... + @Test + public void testMissingBooleanArgument() throws Exception { + Args args = new Args("x", new String[]{"-x"}); + assertEquals(false, args.isValid()); + } + + // Currently fails... + @Test + public void testInvalidBoolean() throws Exception { + Args args = new Args("x", new String[]{"-x", "Truthy"}); + assertEquals(1, args.cardinality()); + assertEquals(false, args.getBoolean('x')); + } + + @Test + public void testMultipleBooleans() throws Exception { + Args args = new Args("x,y", new String[]{"-xy", "true", "true"}); + assertEquals(2, args.cardinality()); + assertEquals(true, args.getBoolean('x')); + assertEquals(true, args.getBoolean('y')); + } + + @Test + public void testSpacesInFormat() throws Exception { + Args args = new Args("x, y", new String[]{"-xy", "true", "true"}); + assertEquals(2, args.cardinality()); + assertTrue(args.has('x')); + assertTrue(args.has('y')); + assertEquals(true, args.getBoolean('x')); + assertEquals(true, args.getBoolean('y')); + } + + // Currently fails... + @Test + public void testInvalidArgumentValueFormat() throws Exception { + Args args = new Args("x,y", new String[]{"xy", "true", "false"}); + assertEquals(false, args.isValid()); + } + + @Test + public void testSimpleStringPresent() throws Exception { + Args args = new Args("x*", new String[]{"-x", "param"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals("param", args.getString('x')); + } + + @Test + public void testMissingStringArgument() throws Exception { + Args args = new Args("x*", new String[]{"-x"}); + assertEquals(false, args.isValid()); + assertEquals("Could not find string parameter for -x.", + args.errorMessage()); + } + + @Test + public void testSimpleIntPresent() throws Exception { + Args args = new Args("x#", new String[]{"-x", "42"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals(42, args.getInt('x')); + } + + @Test + public void testInvalidInteger() throws Exception { + Args args = new Args("x#", new String[]{"-x", "Forty two"}); + assertEquals(false, args.isValid()); + assertEquals("Argument -x expects an integer but was 'Forty two'.", + args.errorMessage()); + } + + @Test + public void testMissingInteger() throws Exception { + Args args = new Args("x#", new String[]{"-x"}); + assertEquals(false, args.isValid()); + assertEquals("Could not find integer parameter for -x.", + args.errorMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/chapter14/refactored/second/ArgsExceptionTest.java b/src/test/java/chapter14/refactored/second/ArgsExceptionTest.java new file mode 100644 index 0000000..a0160d5 --- /dev/null +++ b/src/test/java/chapter14/refactored/second/ArgsExceptionTest.java @@ -0,0 +1,46 @@ +package chapter14.refactored.second; + +import clean.code.chapter14.refactored.second.ArgsException; +import junit.framework.TestCase; + +public class ArgsExceptionTest extends TestCase { + public void testUnexpectedMessage() throws Exception { + ArgsException e = + new ArgsException(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, + 'x', null); + assertEquals("Argument -x unexpected.", e.errorMessage()); + } + + public void testMissingStringMessage() throws Exception { + ArgsException e = new ArgsException(ArgsException.ErrorCode.MISSING_STRING, + 'x', null); + assertEquals("Could not find string parameter for -x.", e.errorMessage()); + } + + public void testInvalidIntegerMessage() throws Exception { + ArgsException e = + new ArgsException(ArgsException.ErrorCode.INVALID_INTEGER, + 'x', "Forty two"); + assertEquals("Argument -x expects an integer but was 'Forty two'.", + e.errorMessage()); + } + + public void testMissingIntegerMessage() throws Exception { + ArgsException e = + new ArgsException(ArgsException.ErrorCode.MISSING_INTEGER, 'x', null); + assertEquals("Could not find integer parameter for -x.", e.errorMessage()); + } + + public void testInvalidDoubleMessage() throws Exception { + ArgsException e = new ArgsException(ArgsException.ErrorCode.INVALID_DOUBLE, + 'x', "Forty two"); + assertEquals("Argument -x expects a double but was 'Forty two'.", + e.errorMessage()); + } + + public void testMissingDoubleMessage() throws Exception { + ArgsException e = new ArgsException(ArgsException.ErrorCode.MISSING_DOUBLE, + 'x', null); + assertEquals("Could not find double parameter for -x.", e.errorMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/chapter14/refactored/second/ArgsTest.java b/src/test/java/chapter14/refactored/second/ArgsTest.java new file mode 100644 index 0000000..ce3d76e --- /dev/null +++ b/src/test/java/chapter14/refactored/second/ArgsTest.java @@ -0,0 +1,143 @@ +package chapter14.refactored.second; + +import clean.code.chapter14.refactored.second.Args; +import clean.code.chapter14.refactored.second.ArgsException; +import junit.framework.TestCase; + +public class ArgsTest extends TestCase { + public void testCreateWithNoSchemaOrArguments() throws Exception { + Args args = new Args("", new String[0]); + assertEquals(0, args.cardinality()); + } + + public void testWithNoSchemaButWithOneArgument() throws Exception { + try { + new Args("", new String[]{"-x"}); + fail(); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, + e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + } + } + + public void testWithNoSchemaButWithMultipleArguments() throws Exception { + try { + new Args("", new String[]{"-x", "-y"}); + fail(); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, + e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + } + + } + + public void testNonLetterSchema() throws Exception { + try { + new Args("*", new String[]{}); + fail("Args constructor should have thrown exception"); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.INVALID_ARGUMENT_NAME, + e.getErrorCode()); + assertEquals('*', e.getErrorArgumentId()); + } + } + + public void testInvalidArgumentFormat() throws Exception { + try { + new Args("f~", new String[]{}); + fail("Args constructor should have throws exception"); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.INVALID_FORMAT, e.getErrorCode()); + assertEquals('f', e.getErrorArgumentId()); + } + } + + public void testSimpleBooleanPresent() throws Exception { + Args args = new Args("x", new String[]{"-x", "true"}); + assertEquals(1, args.cardinality()); + assertEquals(true, args.getBoolean('x')); + } + + public void testSimpleStringPresent() throws Exception { + Args args = new Args("x*", new String[]{"-x", "param"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals("param", args.getString('x')); + } + + public void testMissingStringArgument() throws Exception { + try { + new Args("x*", new String[]{"-x"}); + fail(); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.MISSING_STRING, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + } + } + + public void testSpacesInFormat() throws Exception { + Args args = new Args("x, y", new String[]{"-xy", "true", "false"}); + assertEquals(2, args.cardinality()); + assertTrue(args.has('x')); + assertTrue(args.has('y')); + } + + public void testSimpleIntPresent() throws Exception { + Args args = new Args("x#", new String[]{"-x", "42"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals(42, args.getInt('x')); + } + + public void testInvalidInteger() throws Exception { + try { + new Args("x#", new String[]{"-x", "Forty two"}); + fail(); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.INVALID_INTEGER, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + assertEquals("Forty two", e.getErrorParameter()); + } + + } + + public void testMissingInteger() throws Exception { + try { + new Args("x#", new String[]{"-x"}); + fail(); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.MISSING_INTEGER, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + } + } + + public void testSimpleDoublePresent() throws Exception { + Args args = new Args("x##", new String[]{"-x", "42.3"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals(42.3, args.getDouble('x'), .001); + } + + public void testInvalidDouble() throws Exception { + try { + new Args("x##", new String[]{"-x", "Forty two"}); + fail(); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.INVALID_DOUBLE, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + assertEquals("Forty two", e.getErrorParameter()); + } + } + + public void testMissingDouble() throws Exception { + try { + new Args("x##", new String[]{"-x"}); + fail(); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.MISSING_DOUBLE, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + } + } +} \ No newline at end of file diff --git a/src/test/java/chapter14/refactored/step/by/step/ArgsExceptionTest.java b/src/test/java/chapter14/refactored/step/by/step/ArgsExceptionTest.java new file mode 100644 index 0000000..2185924 --- /dev/null +++ b/src/test/java/chapter14/refactored/step/by/step/ArgsExceptionTest.java @@ -0,0 +1,46 @@ +package chapter14.refactored.step.by.step; + +import clean.code.chapter14.refactored.step.by.step.ArgsException; +import junit.framework.TestCase; + +public class ArgsExceptionTest extends TestCase { + public void testUnexpectedMessage() throws Exception { + ArgsException e = + new ArgsException(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, + 'x', null); + assertEquals("Argument -x unexpected.", e.errorMessage()); + } + + public void testMissingStringMessage() throws Exception { + ArgsException e = new ArgsException(ArgsException.ErrorCode.MISSING_STRING, + 'x', null); + assertEquals("Could not find string parameter for -x.", e.errorMessage()); + } + + public void testInvalidIntegerMessage() throws Exception { + ArgsException e = + new ArgsException(ArgsException.ErrorCode.INVALID_INTEGER, + 'x', "Forty two"); + assertEquals("Argument -x expects an integer but was 'Forty two'.", + e.errorMessage()); + } + + public void testMissingIntegerMessage() throws Exception { + ArgsException e = + new ArgsException(ArgsException.ErrorCode.MISSING_INTEGER, 'x', null); + assertEquals("Could not find integer parameter for -x.", e.errorMessage()); + } + + public void testInvalidDoubleMessage() throws Exception { + ArgsException e = new ArgsException(ArgsException.ErrorCode.INVALID_DOUBLE, + 'x', "Forty two"); + assertEquals("Argument -x expects a double but was 'Forty two'.", + e.errorMessage()); + } + + public void testMissingDoubleMessage() throws Exception { + ArgsException e = new ArgsException(ArgsException.ErrorCode.MISSING_DOUBLE, + 'x', null); + assertEquals("Could not find double parameter for -x.", e.errorMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/chapter14/refactored/step/by/step/ArgsTest.java b/src/test/java/chapter14/refactored/step/by/step/ArgsTest.java new file mode 100644 index 0000000..e54e481 --- /dev/null +++ b/src/test/java/chapter14/refactored/step/by/step/ArgsTest.java @@ -0,0 +1,318 @@ +package chapter14.refactored.step.by.step; + +import clean.code.chapter14.refactored.step.by.step.Args; +import clean.code.chapter14.refactored.step.by.step.ArgsException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static clean.code.chapter14.refactored.step.by.step.ArgsException.ErrorCode.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(JUnit4.class) +public class ArgsTest { + + @Test + public void cardinalityShouldBeZeroIfNoSchemaOrArguments() throws Exception { + Args args = new Args("", new String[0]); + assertEquals(0, args.cardinality()); + } + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void shouldIdentifyUnexpectedArgumentIfNoSchemaButOneArgument() throws Exception { + exception.expect(ArgsException.class); + try { + new Args("", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(UNEXPECTED_ARGUMENT, e.getErrorCode()); + assertEquals("Argument -x unexpected.", e.errorMessage()); + assertEquals('x', e.getErrorArgumentId()); + + throw e; + } + } + + @Test + public void shouldIdentifyFirstUnexpectedArgumentIfNoSchemaButMultipleArguments() throws Exception { + exception.expect(ArgsException.class); + try { + new Args("", new String[]{"-x", "-y"}); + } catch (ArgsException e) { + assertEquals(UNEXPECTED_ARGUMENT, e.getErrorCode()); + assertEquals("Argument -x unexpected.", e.errorMessage()); + assertEquals('x', e.getErrorArgumentId()); + + throw e; + } + } + + @Test + public void shouldIdentifyNonLetterSchema() throws Exception { + exception.expect(ArgsException.class); + try { + new Args("*", new String[]{}); + } catch (ArgsException e) { + assertEquals(INVALID_ARGUMENT_NAME, e.getErrorCode()); + assertEquals("Bad character:* in Args format", e.errorMessage()); + assertEquals('*', e.getErrorArgumentId()); + + throw e; + } + } + + @Test + public void testInvalidArgumentFormat() throws Exception { + exception.expect(ArgsException.class); + try { + new Args("f~", new String[]{}); + } catch (ArgsException e) { + assertEquals(INVALID_ARGUMENT_FORMAT, e.getErrorCode()); + assertEquals('f', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void shouldAllowSpacesInArgumentFormat() throws Exception { + Args args = new Args("x, y", new String[]{"-xy", "true", "false"}); + assertEquals(2, args.cardinality()); + assertTrue(args.has('x')); + assertTrue(args.has('y')); + assertEquals(true, args.getBoolean('x')); + assertEquals(false, args.getBoolean('y')); + } + + @Test + public void shouldIdentifyInvalidArgumentNameFormat1() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x,y", new String[]{"xy", "true", "false"}); + } catch (ArgsException e) { + assertEquals(INVALID_ARGUMENT_FORMAT, e.getErrorCode()); + assertEquals('-', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void shouldIdentifyInvalidArgumentNameFormat2() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x*", new String[]{}); + } catch (ArgsException e) { + assertEquals(INVALID_ARGUMENT_FORMAT, e.getErrorCode()); + assertEquals('-', e.getErrorArgumentId()); + throw e; + } + } + + // Boolean tests... + @Test + public void shouldSupportBooleanTrue() throws Exception { + Args args = new Args("x", new String[]{"-x", "true"}); + assertEquals(1, args.cardinality()); + assertEquals(true, args.getBoolean('x')); + } + + @Test + public void shouldSupportBooleanFalse() throws Exception { + Args args = new Args("x", new String[]{"-x", "false"}); + assertEquals(1, args.cardinality()); + assertEquals(false, args.getBoolean('x')); + } + + @Test + public void getBooleanShouldReturnFalseIfArgumentValueIsNeitherTrueNorFalse() throws Exception { + Args args = new Args("x", new String[]{"-x", "Truthy"}); + assertEquals(1, args.cardinality()); + assertEquals(false, args.getBoolean('x')); + } + + @Test + public void shouldSupportMultipleBooleans() throws Exception { + Args args = new Args("x,y", new String[]{"-xy", "true", "true"}); + assertEquals(2, args.cardinality()); + assertEquals(true, args.getBoolean('x')); + assertEquals(true, args.getBoolean('y')); + } + + @Test + public void getBooleanShouldReturnFalseIfArgumentNameIsNotInSchemaAndOtherArgumentsAreCorrect() throws Exception { + Args args = new Args("x", new String[]{"-x", "true"}); + assertEquals(false, args.getBoolean('y')); + } + + @Test + public void shouldIdentifyMissingBoolean() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(MISSING_BOOLEAN, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + assertEquals("Could not find boolean parameter for -x.", e.errorMessage()); + throw e; + } + } + + // String tests... + @Test + public void testSimpleStringPresent() throws Exception { + Args args = new Args("x*", new String[]{"-x", "param"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals("param", args.getString('x')); + } + + @Test + public void shouldSupportMultipleStrings() throws Exception { + Args args = new Args("x*,y*", new String[]{"-xy", "25", "49"}); + assertEquals(2, args.cardinality()); + assertEquals("25", args.getString('x')); + assertEquals("49", args.getString('y')); + } + + @Test + public void getStringShouldReturnEmptyStringIfArgumentNameIsNotInSchemaAndOtherArgumentsAreCorrect() throws Exception { + Args args = new Args("x*", new String[]{"-x", "23"}); + assertEquals("", args.getString('y')); + } + + @Test + public void shouldIdentifyMissingString() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x*", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(MISSING_STRING, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + assertEquals("Could not find string parameter for -x.", e.errorMessage()); + throw e; + } + } + + // Integer tests... + @Test + public void shouldSupportInteger() throws Exception { + Args args = new Args("x#", new String[]{"-x", "42"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals(42, args.getInt('x')); + } + + @Test + public void shouldReportErrorWhenIntegerArgumentValueIsWrongType() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x#", new String[]{"-x", "Forty two"}); + } catch (ArgsException e) { + assertEquals(INVALID_INTEGER, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + assertEquals("Argument -x expects an integer but was 'Forty two'.", + e.errorMessage()); + throw e; + } + } + + @Test + public void shouldSupportMultipleIntegers() throws Exception { + Args args = new Args("x#,y#", new String[]{"-xy", "25", "49"}); + assertEquals(2, args.cardinality()); + assertEquals(25, args.getInt('x')); + assertEquals(49, args.getInt('y')); + } + + @Test + public void getIntegerShouldReturnZeroIfArgumentNameIsNotInSchemaAndOtherArgumentsAreCorrect() throws Exception { + Args args = new Args("x#", new String[]{"-x", "23"}); + assertEquals(0, args.getInt('y')); + } + + @Test + public void shouldIdentifyMissingInteger() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x#", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(MISSING_INTEGER, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + assertEquals("Could not find integer parameter for -x.", e.errorMessage()); + throw e; + } + } + + // Double + @Test + public void shouldSupportDouble() throws Exception { + Args args = new Args("x##", new String[]{"-x", "42.3"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals(42.3, args.getDouble('x'), .001); + } + + @Test + public void shouldReportErrorWhenDoubleArgumentValueIsWrongType() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x##", new String[]{"-x", "Forty two"}); + } catch (ArgsException e) { + assertEquals(INVALID_DOUBLE, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + assertEquals("Argument -x expects a double but was 'Forty two'.", + e.errorMessage()); + throw e; + } + } + + @Test + public void getDoubleShouldReturnZeroIfArgumentNameIsNotInSchemaAndOtherArgumentsAreCorrect() throws Exception { + Args args = new Args("x##", new String[]{"-x", "25.4"}); + assertEquals(0.0, args.getDouble('y'), 0.01); + } + + @Test + public void shouldIdentifyMissingDouble() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x##", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(MISSING_DOUBLE, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + assertEquals("Could not find double parameter for -x.", e.errorMessage()); + throw e; + } + } + + // Other tests... + @Test + public void shouldReturnFalseIfCallGetBooleanOnNonBooleanArgument() throws Exception { + Args args = new Args("x#", new String[]{"-x", "42"}); + assertEquals(false, args.getBoolean('x')); + } + + @Test + public void shouldReturnEmptyStringIfCallGetStringOnNonStringArgument() throws Exception { + Args args = new Args("x#", new String[]{"-x", "42"}); + assertEquals("", args.getString('x')); + } + + @Test + public void shouldReturnZeroIfCallGetIntOnNonIntegerArgument() throws Exception { + Args args = new Args("x", new String[]{"-x", "false"}); + assertEquals(0, args.getInt('x')); + } +} \ No newline at end of file diff --git a/src/test/java/chapter14/refactored/step/by/step/ArgsTestFromBook.java b/src/test/java/chapter14/refactored/step/by/step/ArgsTestFromBook.java new file mode 100644 index 0000000..1893631 --- /dev/null +++ b/src/test/java/chapter14/refactored/step/by/step/ArgsTestFromBook.java @@ -0,0 +1,238 @@ +package chapter14.refactored.step.by.step; + +import clean.code.chapter14.refactored.step.by.step.Args; +import clean.code.chapter14.refactored.step.by.step.ArgsException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(JUnit4.class) +public class ArgsTestFromBook { + + @Test + public void testCreateWithNoSchemaOrArguments() throws Exception { + Args args = new Args("", new String[0]); + assertEquals(0, args.cardinality()); + } + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void testWithNoSchemaButWithOneArgument() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, + e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testWithSchemaButWithNoArguments() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x", new String[0]); + } catch (ArgsException e) { + throw e; + } + } + + @Test + public void testWithNoSchemaButWithMultipleArguments() throws Exception { + try { + exception.expect(ArgsException.class); + + new Args("", new String[]{"-x", "-y"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, + e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testNonLetterSchema() throws Exception { + exception.expect(ArgsException.class); + try { + new Args("*", new String[]{}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.INVALID_ARGUMENT_NAME, + e.getErrorCode()); + assertEquals('*', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testInvalidArgumentFormat() throws Exception { + exception.expect(ArgsException.class); + try { + new Args("f~", new String[]{}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.INVALID_ARGUMENT_FORMAT, e.getErrorCode()); + assertEquals('f', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testSimpleBooleanTruePresent() throws Exception { + Args args = new Args("x", new String[]{"-x", "true"}); + assertEquals(1, args.cardinality()); + assertEquals(true, args.getBoolean('x')); + } + + @Test + public void testSimpleBooleanFalsePresent() throws Exception { + Args args = new Args("x", new String[]{"-x", "false"}); + assertEquals(1, args.cardinality()); + assertEquals(false, args.getBoolean('x')); + } + + @Test + public void testMissingBooleanArgument() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.MISSING_BOOLEAN, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testInvalidBoolean() throws Exception { + Args args = new Args("x", new String[]{"-x", "Truthy"}); + assertEquals(1, args.cardinality()); + assertEquals(false, args.getBoolean('x')); + } + + @Test + public void testSpacesInFormat() throws Exception { + Args args = new Args("x, y", new String[]{"-xy", "true", "false"}); + assertEquals(2, args.cardinality()); + assertTrue(args.has('x')); + assertTrue(args.has('y')); + assertEquals(true, args.getBoolean('x')); + assertEquals(false, args.getBoolean('y')); + } + + + @Test + public void testInvalidArgumentValueFormat() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x, y", new String[]{"xy", "true", "false"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.INVALID_ARGUMENT_FORMAT, e.getErrorCode()); + assertEquals('-', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testSimpleStringPresent() throws Exception { + Args args = new Args("x*", new String[]{"-x", "param"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals("param", args.getString('x')); + } + + @Test + public void testMissingStringArgument() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x*", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.MISSING_STRING, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testSimpleIntPresent() throws Exception { + Args args = new Args("x#", new String[]{"-x", "42"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals(42, args.getInt('x')); + } + + @Test + public void testInvalidInteger() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x#", new String[]{"-x", "Forty two"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.INVALID_INTEGER, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + assertEquals("Forty two", e.getErrorParameter()); + throw e; + } + } + + @Test + public void testMissingInteger() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x#", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.MISSING_INTEGER, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testSimpleDoublePresent() throws Exception { + Args args = new Args("x##", new String[]{"-x", "42.3"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals(42.3, args.getDouble('x'), .001); + } + + @Test + public void testInvalidDouble() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x##", new String[]{"-x", "Forty two"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.INVALID_DOUBLE, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + assertEquals("Forty two", e.getErrorParameter()); + throw e; + } + } + + @Test + public void testMissingDouble() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x##", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.MISSING_DOUBLE, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + throw e; + } + } +} \ No newline at end of file diff --git a/src/test/java/chapter14/solution/ArgsExceptionTest.java b/src/test/java/chapter14/solution/ArgsExceptionTest.java new file mode 100644 index 0000000..b814819 --- /dev/null +++ b/src/test/java/chapter14/solution/ArgsExceptionTest.java @@ -0,0 +1,46 @@ +package chapter14.solution; + +import clean.code.chapter14.solution.ArgsException; +import junit.framework.TestCase; + +public class ArgsExceptionTest extends TestCase { + public void testUnexpectedMessage() throws Exception { + ArgsException e = + new ArgsException(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, + 'x', null); + assertEquals("Argument -x unexpected.", e.errorMessage()); + } + + public void testMissingStringMessage() throws Exception { + ArgsException e = new ArgsException(ArgsException.ErrorCode.MISSING_STRING, + 'x', null); + assertEquals("Could not find string parameter for -x.", e.errorMessage()); + } + + public void testInvalidIntegerMessage() throws Exception { + ArgsException e = + new ArgsException(ArgsException.ErrorCode.INVALID_INTEGER, + 'x', "Forty two"); + assertEquals("Argument -x expects an integer but was 'Forty two'.", + e.errorMessage()); + } + + public void testMissingIntegerMessage() throws Exception { + ArgsException e = + new ArgsException(ArgsException.ErrorCode.MISSING_INTEGER, 'x', null); + assertEquals("Could not find integer parameter for -x.", e.errorMessage()); + } + + public void testInvalidDoubleMessage() throws Exception { + ArgsException e = new ArgsException(ArgsException.ErrorCode.INVALID_DOUBLE, + 'x', "Forty two"); + assertEquals("Argument -x expects a double but was 'Forty two'.", + e.errorMessage()); + } + + public void testMissingDoubleMessage() throws Exception { + ArgsException e = new ArgsException(ArgsException.ErrorCode.MISSING_DOUBLE, + 'x', null); + assertEquals("Could not find double parameter for -x.", e.errorMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/chapter14/solution/ArgsTest.java b/src/test/java/chapter14/solution/ArgsTest.java new file mode 100644 index 0000000..b98f9ed --- /dev/null +++ b/src/test/java/chapter14/solution/ArgsTest.java @@ -0,0 +1,238 @@ +package chapter14.solution; + +import clean.code.chapter14.solution.Args; +import clean.code.chapter14.solution.ArgsException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; + +@RunWith(JUnit4.class) +public class ArgsTest { + + @Test + public void testCreateWithNoSchemaOrArguments() throws Exception { + Args args = new Args("", new String[0]); + assertEquals(0, args.cardinality()); + } + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void testWithNoSchemaButWithOneArgument() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, + e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testWithSchemaButWithNoArguments() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x", new String[0]); + } catch (ArgsException e) { + throw e; + } + } + + @Test + public void testWithNoSchemaButWithMultipleArguments() throws Exception { + try { + exception.expect(ArgsException.class); + + new Args("", new String[]{"-x", "-y"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, + e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testNonLetterSchema() throws Exception { + exception.expect(ArgsException.class); + try { + new Args("*", new String[]{}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.INVALID_ARGUMENT_NAME, + e.getErrorCode()); + assertEquals('*', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testInvalidArgumentFormat() throws Exception { + exception.expect(ArgsException.class); + try { + new Args("f~", new String[]{}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.INVALID_ARGUMENT_FORMAT, e.getErrorCode()); + assertEquals('f', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testSimpleBooleanTruePresent() throws Exception { + Args args = new Args("x", new String[]{"-x", "true"}); + assertEquals(1, args.cardinality()); + assertEquals(true, args.getBoolean('x')); + } + + @Test + public void testSimpleBooleanFalsePresent() throws Exception { + Args args = new Args("x", new String[]{"-x", "false"}); + assertEquals(1, args.cardinality()); + assertEquals(false, args.getBoolean('x')); + } + + @Test + public void testMissingBooleanArgument() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.MISSING_BOOLEAN, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testInvalidBoolean() throws Exception { + Args args = new Args("x", new String[]{"-x", "Truthy"}); + assertEquals(1, args.cardinality()); + assertEquals(false, args.getBoolean('x')); + } + + @Test + public void testSpacesInFormat() throws Exception { + Args args = new Args("x, y", new String[]{"-xy", "true", "false"}); + assertEquals(2, args.cardinality()); + assertTrue(args.has('x')); + assertTrue(args.has('y')); + assertEquals(true, args.getBoolean('x')); + assertEquals(false, args.getBoolean('y')); + } + + + @Test + public void testInvalidArgumentValueFormat() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x, y", new String[]{"xy", "true", "false"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.INVALID_ARGUMENT_FORMAT, e.getErrorCode()); + assertEquals('-', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testSimpleStringPresent() throws Exception { + Args args = new Args("x*", new String[]{"-x", "param"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals("param", args.getString('x')); + } + + @Test + public void testMissingStringArgument() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x*", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.MISSING_STRING, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testSimpleIntPresent() throws Exception { + Args args = new Args("x#", new String[]{"-x", "42"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals(42, args.getInt('x')); + } + + @Test + public void testInvalidInteger() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x#", new String[]{"-x", "Forty two"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.INVALID_INTEGER, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + assertEquals("Forty two", e.getErrorParameter()); + throw e; + } + } + + @Test + public void testMissingInteger() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x#", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.MISSING_INTEGER, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + throw e; + } + } + + @Test + public void testSimpleDoublePresent() throws Exception { + Args args = new Args("x##", new String[]{"-x", "42.3"}); + assertEquals(1, args.cardinality()); + assertTrue(args.has('x')); + assertEquals(42.3, args.getDouble('x'), .001); + } + + @Test + public void testInvalidDouble() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x##", new String[]{"-x", "Forty two"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.INVALID_DOUBLE, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + assertEquals("Forty two", e.getErrorParameter()); + throw e; + } + } + + @Test + public void testMissingDouble() throws Exception { + exception.expect(ArgsException.class); + + try { + new Args("x##", new String[]{"-x"}); + } catch (ArgsException e) { + assertEquals(ArgsException.ErrorCode.MISSING_DOUBLE, e.getErrorCode()); + assertEquals('x', e.getErrorArgumentId()); + throw e; + } + } +} \ No newline at end of file diff --git a/src/test/java/chapter15/defactored/ComparisonCompactorTest.java b/src/test/java/chapter15/defactored/ComparisonCompactorTest.java new file mode 100644 index 0000000..70763c6 --- /dev/null +++ b/src/test/java/chapter15/defactored/ComparisonCompactorTest.java @@ -0,0 +1,105 @@ +package chapter15.defactored; + +import clean.code.chapter15.defactored.ComparisonCompactor; +import junit.framework.TestCase; + +public class ComparisonCompactorTest extends TestCase { + + public void testMessage() { + String failure= new ComparisonCompactor(0, "b", "c").compact("a"); + assertTrue("a expected:<[b]> but was:<[c]>".equals(failure)); + } + + public void testStartSame() { + String failure= new ComparisonCompactor(1, "ba", "bc").compact(null); + assertEquals("expected: but was:", failure); + } + + public void testEndSame() { + String failure= new ComparisonCompactor(1, "ab", "cb").compact(null); + assertEquals("expected:<[a]b> but was:<[c]b>", failure); + } + + public void testSame() { + String failure= new ComparisonCompactor(1, "ab", "ab").compact(null); + assertEquals("expected: but was:", failure); + } + + public void testNoContextStartAndEndSame() { + String failure= new ComparisonCompactor(0, "abc", "adc").compact(null); + assertEquals("expected:<...[b]...> but was:<...[d]...>", failure); + } + + public void testStartAndEndContext() { + String failure= new ComparisonCompactor(1, "abc", "adc").compact(null); + assertEquals("expected: but was:", failure); + } + + public void testStartAndEndContextWithEllipses() { + String failure= + new ComparisonCompactor(1, "abcde", "abfde").compact(null); + assertEquals("expected:<...b[c]d...> but was:<...b[f]d...>", failure); + } + + public void testComparisonErrorStartSameComplete() { + String failure= new ComparisonCompactor(2, "ab", "abc").compact(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorEndSameComplete() { + String failure= new ComparisonCompactor(0, "bc", "abc").compact(null); + assertEquals("expected:<[]...> but was:<[a]...>", failure); + } + + public void testComparisonErrorEndSameCompleteContext() { + String failure= new ComparisonCompactor(2, "bc", "abc").compact(null); + assertEquals("expected:<[]bc> but was:<[a]bc>", failure); + } + + public void testComparisonErrorOverlapingMatches() { + String failure= new ComparisonCompactor(0, "abc", "abbc").compact(null); + assertEquals("expected:<...[]...> but was:<...[b]...>", failure); + } + + public void testComparisonErrorOverlapingMatchesContext() { + String failure= new ComparisonCompactor(2, "abc", "abbc").compact(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorOverlapingMatches2() { + String failure= new ComparisonCompactor(0, "abcdde", +"abcde").compact(null); + assertEquals("expected:<...[d]...> but was:<...[]...>", failure); + } + + public void testComparisonErrorOverlapingMatches2Context() { + String failure= + new ComparisonCompactor(2, "abcdde", "abcde").compact(null); + assertEquals("expected:<...cd[d]e> but was:<...cd[]e>", failure); + } + + public void testComparisonErrorWithActualNull() { + String failure= new ComparisonCompactor(0, "a", null).compact(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorWithActualNullContext() { + String failure= new ComparisonCompactor(2, "a", null).compact(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorWithExpectedNull() { + String failure= new ComparisonCompactor(0, null, "a").compact(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorWithExpectedNullContext() { + String failure= new ComparisonCompactor(2, null, "a").compact(null); + assertEquals("expected: but was:", failure); + } + + public void testBug609972() { + String failure= new ComparisonCompactor(10, "S&P500", "0").compact(null); + assertEquals("expected:<[S&P50]0> but was:<[]0>", failure); + } +} \ No newline at end of file diff --git a/src/test/java/chapter15/original/ComparisonCompactorTest.java b/src/test/java/chapter15/original/ComparisonCompactorTest.java new file mode 100644 index 0000000..3afe4eb --- /dev/null +++ b/src/test/java/chapter15/original/ComparisonCompactorTest.java @@ -0,0 +1,105 @@ +package chapter15.original; + +import clean.code.chapter15.original.ComparisonCompactor; +import junit.framework.TestCase; + +public class ComparisonCompactorTest extends TestCase { + + public void testMessage() { + String failure= new ComparisonCompactor(0, "b", "c").compact("a"); + assertTrue("a expected:<[b]> but was:<[c]>".equals(failure)); + } + + public void testStartSame() { + String failure= new ComparisonCompactor(1, "ba", "bc").compact(null); + assertEquals("expected: but was:", failure); + } + + public void testEndSame() { + String failure= new ComparisonCompactor(1, "ab", "cb").compact(null); + assertEquals("expected:<[a]b> but was:<[c]b>", failure); + } + + public void testSame() { + String failure= new ComparisonCompactor(1, "ab", "ab").compact(null); + assertEquals("expected: but was:", failure); + } + + public void testNoContextStartAndEndSame() { + String failure= new ComparisonCompactor(0, "abc", "adc").compact(null); + assertEquals("expected:<...[b]...> but was:<...[d]...>", failure); + } + + public void testStartAndEndContext() { + String failure= new ComparisonCompactor(1, "abc", "adc").compact(null); + assertEquals("expected: but was:", failure); + } + + public void testStartAndEndContextWithEllipses() { + String failure= + new ComparisonCompactor(1, "abcde", "abfde").compact(null); + assertEquals("expected:<...b[c]d...> but was:<...b[f]d...>", failure); + } + + public void testComparisonErrorStartSameComplete() { + String failure= new ComparisonCompactor(2, "ab", "abc").compact(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorEndSameComplete() { + String failure= new ComparisonCompactor(0, "bc", "abc").compact(null); + assertEquals("expected:<[]...> but was:<[a]...>", failure); + } + + public void testComparisonErrorEndSameCompleteContext() { + String failure= new ComparisonCompactor(2, "bc", "abc").compact(null); + assertEquals("expected:<[]bc> but was:<[a]bc>", failure); + } + + public void testComparisonErrorOverlapingMatches() { + String failure= new ComparisonCompactor(0, "abc", "abbc").compact(null); + assertEquals("expected:<...[]...> but was:<...[b]...>", failure); + } + + public void testComparisonErrorOverlapingMatchesContext() { + String failure= new ComparisonCompactor(2, "abc", "abbc").compact(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorOverlapingMatches2() { + String failure= new ComparisonCompactor(0, "abcdde", +"abcde").compact(null); + assertEquals("expected:<...[d]...> but was:<...[]...>", failure); + } + + public void testComparisonErrorOverlapingMatches2Context() { + String failure= + new ComparisonCompactor(2, "abcdde", "abcde").compact(null); + assertEquals("expected:<...cd[d]e> but was:<...cd[]e>", failure); + } + + public void testComparisonErrorWithActualNull() { + String failure= new ComparisonCompactor(0, "a", null).compact(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorWithActualNullContext() { + String failure= new ComparisonCompactor(2, "a", null).compact(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorWithExpectedNull() { + String failure= new ComparisonCompactor(0, null, "a").compact(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorWithExpectedNullContext() { + String failure= new ComparisonCompactor(2, null, "a").compact(null); + assertEquals("expected: but was:", failure); + } + + public void testBug609972() { + String failure= new ComparisonCompactor(10, "S&P500", "0").compact(null); + assertEquals("expected:<[S&P50]0> but was:<[]0>", failure); + } +} \ No newline at end of file diff --git a/src/test/java/chapter15/solution/ComparisonCompactorTest.java b/src/test/java/chapter15/solution/ComparisonCompactorTest.java new file mode 100644 index 0000000..01f8773 --- /dev/null +++ b/src/test/java/chapter15/solution/ComparisonCompactorTest.java @@ -0,0 +1,105 @@ +package chapter15.solution; + +import clean.code.chapter15.solution.ComparisonCompactor; +import junit.framework.TestCase; + +public class ComparisonCompactorTest extends TestCase { + + public void testMessage() { + String failure= new ComparisonCompactor(0, "b", "c").formatCompactedComparison("a"); + assertTrue("a expected:<[b]> but was:<[c]>".equals(failure)); + } + + public void testStartSame() { + String failure= new ComparisonCompactor(1, "ba", "bc").formatCompactedComparison(null); + assertEquals("expected: but was:", failure); + } + + public void testEndSame() { + String failure= new ComparisonCompactor(1, "ab", "cb").formatCompactedComparison(null); + assertEquals("expected:<[a]b> but was:<[c]b>", failure); + } + + public void testSame() { + String failure= new ComparisonCompactor(1, "ab", "ab").formatCompactedComparison(null); + assertEquals("expected: but was:", failure); + } + + public void testNoContextStartAndEndSame() { + String failure= new ComparisonCompactor(0, "abc", "adc").formatCompactedComparison(null); + assertEquals("expected:<...[b]...> but was:<...[d]...>", failure); + } + + public void testStartAndEndContext() { + String failure= new ComparisonCompactor(1, "abc", "adc").formatCompactedComparison(null); + assertEquals("expected: but was:", failure); + } + + public void testStartAndEndContextWithEllipses() { + String failure= + new ComparisonCompactor(1, "abcde", "abfde").formatCompactedComparison(null); + assertEquals("expected:<...b[c]d...> but was:<...b[f]d...>", failure); + } + + public void testComparisonErrorStartSameComplete() { + String failure= new ComparisonCompactor(2, "ab", "abc").formatCompactedComparison(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorEndSameComplete() { + String failure= new ComparisonCompactor(0, "bc", "abc").formatCompactedComparison(null); + assertEquals("expected:<[]...> but was:<[a]...>", failure); + } + + public void testComparisonErrorEndSameCompleteContext() { + String failure= new ComparisonCompactor(2, "bc", "abc").formatCompactedComparison(null); + assertEquals("expected:<[]bc> but was:<[a]bc>", failure); + } + + public void testComparisonErrorOverlapingMatches() { + String failure= new ComparisonCompactor(0, "abc", "abbc").formatCompactedComparison(null); + assertEquals("expected:<...[]...> but was:<...[b]...>", failure); + } + + public void testComparisonErrorOverlapingMatchesContext() { + String failure= new ComparisonCompactor(2, "abc", "abbc").formatCompactedComparison(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorOverlapingMatches2() { + String failure= new ComparisonCompactor(0, "abcdde", +"abcde").formatCompactedComparison(null); + assertEquals("expected:<...[d]...> but was:<...[]...>", failure); + } + + public void testComparisonErrorOverlapingMatches2Context() { + String failure= + new ComparisonCompactor(2, "abcdde", "abcde").formatCompactedComparison(null); + assertEquals("expected:<...cd[d]e> but was:<...cd[]e>", failure); + } + + public void testComparisonErrorWithActualNull() { + String failure= new ComparisonCompactor(0, "a", null).formatCompactedComparison(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorWithActualNullContext() { + String failure= new ComparisonCompactor(2, "a", null).formatCompactedComparison(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorWithExpectedNull() { + String failure= new ComparisonCompactor(0, null, "a").formatCompactedComparison(null); + assertEquals("expected: but was:", failure); + } + + public void testComparisonErrorWithExpectedNullContext() { + String failure= new ComparisonCompactor(2, null, "a").formatCompactedComparison(null); + assertEquals("expected: but was:", failure); + } + + public void testBug609972() { + String failure= new ComparisonCompactor(10, "S&P500", "0").formatCompactedComparison(null); + assertEquals("expected:<[S&P50]0> but was:<[]0>", failure); + } +} \ No newline at end of file diff --git a/src/test/java/chapter16/original/BobsSerialDateTest.java b/src/test/java/chapter16/original/BobsSerialDateTest.java new file mode 100644 index 0000000..f76e0e9 --- /dev/null +++ b/src/test/java/chapter16/original/BobsSerialDateTest.java @@ -0,0 +1,458 @@ +package chapter16.original; + +import clean.code.chapter16.original.SerialDate; +import clean.code.chapter16.original.SpreadsheetDate; +import junit.framework.TestCase; + +import java.util.GregorianCalendar; + +import static clean.code.chapter16.original.SerialDate.*; + +public class BobsSerialDateTest extends TestCase { + + public void testIsValidWeekdayCode() throws Exception { + for (int day = 1; day <= 7; day++) + assertTrue(isValidWeekdayCode(day)); + assertFalse(isValidWeekdayCode(0)); + assertFalse(isValidWeekdayCode(8)); + } + + public void testStringToWeekdayCode() throws Exception { + assertEquals(-1, stringToWeekdayCode("Hello")); + assertEquals(MONDAY, stringToWeekdayCode("Monday")); + assertEquals(MONDAY, stringToWeekdayCode("Mon")); + // assertEquals(MONDAY,stringToWeekdayCode("monday")); + // assertEquals(MONDAY,stringToWeekdayCode("MONDAY")); + // assertEquals(MONDAY, stringToWeekdayCode("mon")); + + assertEquals(TUESDAY, stringToWeekdayCode("Tuesday")); + assertEquals(TUESDAY, stringToWeekdayCode("Tue")); + // assertEquals(TUESDAY,stringToWeekdayCode("tuesday")); + // assertEquals(TUESDAY,stringToWeekdayCode("TUESDAY")); + // assertEquals(TUESDAY, stringToWeekdayCode("tue")); + // assertEquals(TUESDAY, stringToWeekdayCode("tues")); + + assertEquals(WEDNESDAY, stringToWeekdayCode("Wednesday")); + assertEquals(WEDNESDAY, stringToWeekdayCode("Wed")); + // assertEquals(WEDNESDAY,stringToWeekdayCode("wednesday")); + // assertEquals(WEDNESDAY,stringToWeekdayCode("WEDNESDAY")); + // assertEquals(WEDNESDAY, stringToWeekdayCode("wed")); + + assertEquals(THURSDAY, stringToWeekdayCode("Thursday")); + assertEquals(THURSDAY, stringToWeekdayCode("Thu")); + // assertEquals(THURSDAY,stringToWeekdayCode("thursday")); + // assertEquals(THURSDAY,stringToWeekdayCode("THURSDAY")); + // assertEquals(THURSDAY, stringToWeekdayCode("thu")); + // assertEquals(THURSDAY, stringToWeekdayCode("thurs")); + + assertEquals(FRIDAY, stringToWeekdayCode("Friday")); + assertEquals(FRIDAY, stringToWeekdayCode("Fri")); + // assertEquals(FRIDAY,stringToWeekdayCode("friday")); + // assertEquals(FRIDAY,stringToWeekdayCode("FRIDAY")); + // assertEquals(FRIDAY, stringToWeekdayCode("fri")); + + assertEquals(SATURDAY, stringToWeekdayCode("Saturday")); + assertEquals(SATURDAY, stringToWeekdayCode("Sat")); + // assertEquals(SATURDAY,stringToWeekdayCode("saturday")); + // assertEquals(SATURDAY,stringToWeekdayCode("SATURDAY")); + // assertEquals(SATURDAY, stringToWeekdayCode("sat")); + + assertEquals(SUNDAY, stringToWeekdayCode("Sunday")); + assertEquals(SUNDAY, stringToWeekdayCode("Sun")); + // assertEquals(SUNDAY,stringToWeekdayCode("sunday")); + // assertEquals(SUNDAY,stringToWeekdayCode("SUNDAY")); + // assertEquals(SUNDAY, stringToWeekdayCode("sun")); + } + + public void testWeekdayCodeToString() throws Exception { + assertEquals("Sunday", weekdayCodeToString(SUNDAY)); + assertEquals("Monday", weekdayCodeToString(MONDAY)); + assertEquals("Tuesday", weekdayCodeToString(TUESDAY)); + assertEquals("Wednesday", weekdayCodeToString(WEDNESDAY)); + assertEquals("Thursday", weekdayCodeToString(THURSDAY)); + assertEquals("Friday", weekdayCodeToString(FRIDAY)); + assertEquals("Saturday", weekdayCodeToString(SATURDAY)); +// assertEquals("saturday", weekdayCodeToString(SATURDAY)); // todo (0) still not working donno what the heck uncle bob did to fix it + } + + public void testIsValidMonthCode() throws Exception { + for (int i = 1; i <= 12; i++) + assertTrue(isValidMonthCode(i)); + assertFalse(isValidMonthCode(0)); + assertFalse(isValidMonthCode(13)); + } + + public void testMonthToQuarter() throws Exception { + assertEquals(1, monthCodeToQuarter(JANUARY)); + assertEquals(1, monthCodeToQuarter(FEBRUARY)); + assertEquals(1, monthCodeToQuarter(MARCH)); + assertEquals(2, monthCodeToQuarter(APRIL)); + assertEquals(2, monthCodeToQuarter(MAY)); + assertEquals(2, monthCodeToQuarter(JUNE)); + assertEquals(3, monthCodeToQuarter(JULY)); + assertEquals(3, monthCodeToQuarter(AUGUST)); + assertEquals(3, monthCodeToQuarter(SEPTEMBER)); + assertEquals(4, monthCodeToQuarter(OCTOBER)); + assertEquals(4, monthCodeToQuarter(NOVEMBER)); + assertEquals(4, monthCodeToQuarter(DECEMBER)); + + try { + monthCodeToQuarter(-1); + fail("Invalid Month Code should throw exception"); + } catch (IllegalArgumentException e) { + } + } + + public void testMonthCodeToString() throws Exception { + assertEquals("January", monthCodeToString(JANUARY)); + assertEquals("February", monthCodeToString(FEBRUARY)); + assertEquals("March", monthCodeToString(MARCH)); + assertEquals("April", monthCodeToString(APRIL)); + assertEquals("May", monthCodeToString(MAY)); + assertEquals("June", monthCodeToString(JUNE)); + assertEquals("July", monthCodeToString(JULY)); + assertEquals("August", monthCodeToString(AUGUST)); + assertEquals("September", monthCodeToString(SEPTEMBER)); + assertEquals("October", monthCodeToString(OCTOBER)); + assertEquals("November", monthCodeToString(NOVEMBER)); + assertEquals("December", monthCodeToString(DECEMBER)); + + assertEquals("Jan", monthCodeToString(JANUARY, true)); + assertEquals("Feb", monthCodeToString(FEBRUARY, true)); + assertEquals("Mar", monthCodeToString(MARCH, true)); + assertEquals("Apr", monthCodeToString(APRIL, true)); + assertEquals("May", monthCodeToString(MAY, true)); + assertEquals("Jun", monthCodeToString(JUNE, true)); + assertEquals("Jul", monthCodeToString(JULY, true)); + assertEquals("Aug", monthCodeToString(AUGUST, true)); + assertEquals("Sep", monthCodeToString(SEPTEMBER, true)); + assertEquals("Oct", monthCodeToString(OCTOBER, true)); + assertEquals("Nov", monthCodeToString(NOVEMBER, true)); + assertEquals("Dec", monthCodeToString(DECEMBER, true)); + + try { + monthCodeToString(-1); + fail("Invalid month code should throw exception"); + } catch (IllegalArgumentException e) { + } + + } + + public void testStringToMonthCode() throws Exception { + assertEquals(JANUARY, stringToMonthCode("1")); + assertEquals(FEBRUARY, stringToMonthCode("2")); + assertEquals(MARCH, stringToMonthCode("3")); + assertEquals(APRIL, stringToMonthCode("4")); + assertEquals(MAY, stringToMonthCode("5")); + assertEquals(JUNE, stringToMonthCode("6")); + assertEquals(JULY, stringToMonthCode("7")); + assertEquals(AUGUST, stringToMonthCode("8")); + assertEquals(SEPTEMBER, stringToMonthCode("9")); + assertEquals(OCTOBER, stringToMonthCode("10")); + assertEquals(NOVEMBER, stringToMonthCode("11")); + assertEquals(DECEMBER, stringToMonthCode("12")); + + assertEquals(-1, stringToMonthCode("0")); // TODO (1): 5/1/2021 uncommented this + assertEquals(-1, stringToMonthCode("13")); // TODO (2): 5/1/2021 uncommented this + + assertEquals(-1, stringToMonthCode("Hello")); + + for (int m = 1; m <= 12; m++) { + assertEquals(m, stringToMonthCode(monthCodeToString(m, false))); + assertEquals(m, stringToMonthCode(monthCodeToString(m, true))); + } + assertEquals(1,stringToMonthCode("jan")); + assertEquals(2,stringToMonthCode("feb")); + assertEquals(3,stringToMonthCode("mar")); + assertEquals(4,stringToMonthCode("apr")); + assertEquals(5,stringToMonthCode("may")); + assertEquals(6,stringToMonthCode("jun")); + assertEquals(7,stringToMonthCode("jul")); + assertEquals(8,stringToMonthCode("aug")); + assertEquals(9,stringToMonthCode("sep")); + assertEquals(10,stringToMonthCode("oct")); + assertEquals(11,stringToMonthCode("nov")); + assertEquals(12,stringToMonthCode("dec")); + + assertEquals(1,stringToMonthCode("JAN")); + assertEquals(2,stringToMonthCode("FEB")); + assertEquals(3,stringToMonthCode("MAR")); + assertEquals(4,stringToMonthCode("APR")); + assertEquals(5,stringToMonthCode("MAY")); + assertEquals(6,stringToMonthCode("JUN")); + assertEquals(7,stringToMonthCode("JUL")); + assertEquals(8,stringToMonthCode("AUG")); + assertEquals(9,stringToMonthCode("SEP")); + assertEquals(10,stringToMonthCode("OCT")); + assertEquals(11,stringToMonthCode("NOV")); + assertEquals(12,stringToMonthCode("DEC")); + + assertEquals(1,stringToMonthCode("january")); + assertEquals(2,stringToMonthCode("february")); + assertEquals(3,stringToMonthCode("march")); + assertEquals(4,stringToMonthCode("april")); + assertEquals(5,stringToMonthCode("may")); + assertEquals(6,stringToMonthCode("june")); + assertEquals(7,stringToMonthCode("july")); + assertEquals(8,stringToMonthCode("august")); + assertEquals(9,stringToMonthCode("september")); + assertEquals(10,stringToMonthCode("october")); + assertEquals(11,stringToMonthCode("november")); + assertEquals(12,stringToMonthCode("december")); + + assertEquals(1,stringToMonthCode("JANUARY")); + assertEquals(2,stringToMonthCode("FEBRUARY")); + assertEquals(3,stringToMonthCode("MAR")); + assertEquals(4,stringToMonthCode("APRIL")); + assertEquals(5,stringToMonthCode("MAY")); + assertEquals(6,stringToMonthCode("JUNE")); + assertEquals(7,stringToMonthCode("JULY")); + assertEquals(8,stringToMonthCode("AUGUST")); + assertEquals(9,stringToMonthCode("SEPTEMBER")); + assertEquals(10,stringToMonthCode("OCTOBER")); + assertEquals(11,stringToMonthCode("NOVEMBER")); + assertEquals(12,stringToMonthCode("DECEMBER")); + } + + public void testIsValidWeekInMonthCode() throws Exception { + for (int w = 0; w <= 4; w++) { + assertTrue(isValidWeekInMonthCode(w)); + } + assertFalse(isValidWeekInMonthCode(5)); + } + + public void testIsLeapYear() throws Exception { + assertFalse(isLeapYear(1900)); + assertFalse(isLeapYear(1901)); + assertFalse(isLeapYear(1902)); + assertFalse(isLeapYear(1903)); + assertTrue(isLeapYear(1904)); + assertTrue(isLeapYear(1908)); + assertFalse(isLeapYear(1955)); + assertTrue(isLeapYear(1964)); + assertTrue(isLeapYear(1980)); + assertTrue(isLeapYear(2000)); + assertFalse(isLeapYear(2001)); + assertFalse(isLeapYear(2100)); + } + + public void testLeapYearCount() throws Exception { + assertEquals(0, leapYearCount(1900)); + assertEquals(0, leapYearCount(1901)); + assertEquals(0, leapYearCount(1902)); + assertEquals(0, leapYearCount(1903)); + assertEquals(1, leapYearCount(1904)); + assertEquals(1, leapYearCount(1905)); + assertEquals(1, leapYearCount(1906)); + assertEquals(1, leapYearCount(1907)); + assertEquals(2, leapYearCount(1908)); + assertEquals(24, leapYearCount(1999)); + assertEquals(25, leapYearCount(2001)); + assertEquals(49, leapYearCount(2101)); + assertEquals(73, leapYearCount(2201)); + assertEquals(97, leapYearCount(2301)); + assertEquals(122, leapYearCount(2401)); + } + + public void testLastDayOfMonth() throws Exception { + assertEquals(31, lastDayOfMonth(JANUARY, 1901)); + assertEquals(28, lastDayOfMonth(FEBRUARY, 1901)); + assertEquals(31, lastDayOfMonth(MARCH, 1901)); + assertEquals(30, lastDayOfMonth(APRIL, 1901)); + assertEquals(31, lastDayOfMonth(MAY, 1901)); + assertEquals(30, lastDayOfMonth(JUNE, 1901)); + assertEquals(31, lastDayOfMonth(JULY, 1901)); + assertEquals(31, lastDayOfMonth(AUGUST, 1901)); + assertEquals(30, lastDayOfMonth(SEPTEMBER, 1901)); + assertEquals(31, lastDayOfMonth(OCTOBER, 1901)); + assertEquals(30, lastDayOfMonth(NOVEMBER, 1901)); + assertEquals(31, lastDayOfMonth(DECEMBER, 1901)); + assertEquals(29, lastDayOfMonth(FEBRUARY, 1904)); + } + + public void testAddDays() throws Exception { + SerialDate newYears = d(1, JANUARY, 1900); + assertEquals(d(2, JANUARY, 1900), addDays(1, newYears)); + assertEquals(d(1, FEBRUARY, 1900), addDays(31, newYears)); + assertEquals(d(1, JANUARY, 1901), addDays(365, newYears)); + assertEquals(d(31, DECEMBER, 1904), addDays(5 * 365, newYears)); + } + + private static SpreadsheetDate d(int day, int month, int year) { + return new SpreadsheetDate(day, month, year); + } + + public void testAddMonths() throws Exception { + assertEquals(d(1, FEBRUARY, 1900), addMonths(1, d(1, JANUARY, 1900))); + assertEquals(d(28, FEBRUARY, 1900), addMonths(1, d(31, JANUARY, 1900))); + assertEquals(d(28, FEBRUARY, 1900), addMonths(1, d(30, JANUARY, 1900))); + assertEquals(d(28, FEBRUARY, 1900), addMonths(1, d(29, JANUARY, 1900))); + assertEquals(d(28, FEBRUARY, 1900), addMonths(1, d(28, JANUARY, 1900))); + assertEquals(d(27, FEBRUARY, 1900), addMonths(1, d(27, JANUARY, 1900))); + + assertEquals(d(30, JUNE, 1900), addMonths(5, d(31, JANUARY, 1900))); + assertEquals(d(30, JUNE, 1901), addMonths(17, d(31, JANUARY, 1900))); + + assertEquals(d(29, FEBRUARY, 1904), addMonths(49, d(31, JANUARY, 1900))); + } + + public void testAddYears() throws Exception { + assertEquals(d(1, JANUARY, 1901), addYears(1, d(1, JANUARY, 1900))); + assertEquals(d(28, FEBRUARY, 1905), addYears(1, d(29, FEBRUARY, 1904))); + assertEquals(d(28, FEBRUARY, 1905), addYears(1, d(28, FEBRUARY, 1904))); + assertEquals(d(28, FEBRUARY, 1904), addYears(1, d(28, FEBRUARY, 1903))); + } + + public void testGetPreviousDayOfWeek() throws Exception { + assertEquals(d(24, FEBRUARY, 2006), getPreviousDayOfWeek(FRIDAY, d(1, MARCH, 2006))); + assertEquals(d(22, FEBRUARY, 2006), getPreviousDayOfWeek(WEDNESDAY, d(1, MARCH, 2006))); + assertEquals(d(29, FEBRUARY, 2004), getPreviousDayOfWeek(SUNDAY, d(3, MARCH, 2004))); + assertEquals(d(29, DECEMBER, 2004), getPreviousDayOfWeek(WEDNESDAY, d(5, JANUARY, 2005))); + + try { + getPreviousDayOfWeek(-1, d(1, JANUARY, 2006)); + fail("Invalid day of week code should throw exception"); + } catch (IllegalArgumentException e) { + } + } + + public void testGetFollowingDayOfWeek() throws Exception { + assertEquals(d(1, JANUARY, 2005),getFollowingDayOfWeek(SATURDAY, d(25, DECEMBER, 2004))); // todo (3) both of these days must be saturday + assertEquals(d(1, JANUARY, 2005), getFollowingDayOfWeek(SATURDAY, d(26, DECEMBER, 2004))); + assertEquals(d(3, MARCH, 2004), getFollowingDayOfWeek(WEDNESDAY, d(28, FEBRUARY, 2004))); + + try { + getFollowingDayOfWeek(-1, d(1, JANUARY, 2006)); + fail("Invalid day of week code should throw exception"); + } catch (IllegalArgumentException e) { + } + } + + public void testGetNearestDayOfWeek() throws Exception { + assertEquals(d(16, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(16, APRIL, 2006))); + assertEquals(d(16, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(17, APRIL, 2006))); + assertEquals(d(16, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(18, APRIL, 2006))); + assertEquals(d(16, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(19, APRIL, 2006))); + assertEquals(d(23, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(20, APRIL, 2006))); + assertEquals(d(23, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(21, APRIL, 2006))); + assertEquals(d(23, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(22, APRIL, 2006))); + + // todo (4): this was failing because of wrong algorithm + assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(16, APRIL, 2006))); + assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(17, APRIL, 2006))); + assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(18, APRIL, 2006))); + assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(19, APRIL, 2006))); + assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(20, APRIL, 2006))); + assertEquals(d(24, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(21, APRIL, 2006))); + assertEquals(d(24, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(22, APRIL, 2006))); + + assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(16, APRIL, 2006))); + assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(17, APRIL, 2006))); + assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(18, APRIL, 2006))); + assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(19, APRIL, 2006))); + assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(20, APRIL, 2006))); + assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(21, APRIL, 2006))); + assertEquals(d(25, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(22, APRIL, 2006))); + + assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(16, APRIL, 2006))); + assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(17, APRIL, 2006))); + assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(18, APRIL, 2006))); + assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(19, APRIL, 2006))); + assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(20, APRIL, 2006))); + assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(21, APRIL, 2006))); + assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(22, APRIL, 2006))); + + assertEquals(d(13, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(16, APRIL, 2006))); + assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(17, APRIL, 2006))); + assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(18, APRIL, 2006))); + assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(19, APRIL, 2006))); + assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(20, APRIL, 2006))); + assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(21, APRIL, 2006))); + assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(22, APRIL, 2006))); + + assertEquals(d(14, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(16, APRIL, 2006))); + assertEquals(d(14, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(17, APRIL, 2006))); + assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(18, APRIL, 2006))); + assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(19, APRIL, 2006))); + assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(20, APRIL, 2006))); + assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(21, APRIL, 2006))); + assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(22, APRIL, 2006))); + + assertEquals(d(15, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(16, APRIL, 2006))); + assertEquals(d(15, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(17, APRIL, 2006))); + assertEquals(d(15, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(18, APRIL, 2006))); + assertEquals(d(22, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(19, APRIL, 2006))); + assertEquals(d(22, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(20, APRIL, 2006))); + assertEquals(d(22, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(21, APRIL, 2006))); + assertEquals(d(22, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(22, APRIL, 2006))); + + try { + getNearestDayOfWeek(-1, d(1, JANUARY, 2006)); + fail("Invalid day of week code should throw exception"); + } catch (IllegalArgumentException e) { + } + } + + public void testEndOfCurrentMonth() throws Exception { + SerialDate d = SerialDate.createInstance(2); + assertEquals(d(31, JANUARY, 2006), d.getEndOfCurrentMonth(d(1, JANUARY, 2006))); + assertEquals(d(28, FEBRUARY, 2006), d.getEndOfCurrentMonth(d(1, FEBRUARY, 2006))); + assertEquals(d(31, MARCH, 2006), d.getEndOfCurrentMonth(d(1, MARCH, 2006))); + assertEquals(d(30, APRIL, 2006), d.getEndOfCurrentMonth(d(1, APRIL, 2006))); + assertEquals(d(31, MAY, 2006), d.getEndOfCurrentMonth(d(1, MAY, 2006))); + assertEquals(d(30, JUNE, 2006), d.getEndOfCurrentMonth(d(1, JUNE, 2006))); + assertEquals(d(31, JULY, 2006), d.getEndOfCurrentMonth(d(1, JULY, 2006))); + assertEquals(d(31, AUGUST, 2006), d.getEndOfCurrentMonth(d(1, AUGUST, 2006))); + assertEquals(d(30, SEPTEMBER, 2006), d.getEndOfCurrentMonth(d(1, SEPTEMBER, 2006))); + assertEquals(d(31, OCTOBER, 2006), d.getEndOfCurrentMonth(d(1, OCTOBER, 2006))); + assertEquals(d(30, NOVEMBER, 2006), d.getEndOfCurrentMonth(d(1, NOVEMBER, 2006))); + assertEquals(d(31, DECEMBER, 2006), d.getEndOfCurrentMonth(d(1, DECEMBER, 2006))); + assertEquals(d(29, FEBRUARY, 2008), d.getEndOfCurrentMonth(d(1, FEBRUARY, 2008))); + } + + public void testWeekInMonthToString() throws Exception { + assertEquals("First", weekInMonthToString(FIRST_WEEK_IN_MONTH)); + assertEquals("Second", weekInMonthToString(SECOND_WEEK_IN_MONTH)); + assertEquals("Third", weekInMonthToString(THIRD_WEEK_IN_MONTH)); + assertEquals("Fourth", weekInMonthToString(FOURTH_WEEK_IN_MONTH)); + assertEquals("Last", weekInMonthToString(LAST_WEEK_IN_MONTH)); + + // todo (5): fixed below code by throwing exception instead of returning string + try { + weekInMonthToString(-1); + fail("Invalid week code should throw exception"); + } catch (IllegalArgumentException e) { + } + } + + public void testRelativeToString() throws Exception { + assertEquals("Preceding", relativeToString(PRECEDING)); + assertEquals("Nearest", relativeToString(NEAREST)); + assertEquals("Following", relativeToString(FOLLOWING)); + + // todo (6): fixed below code by throwing exception instead of returning string + try { + relativeToString(-1000); + fail("Invalid relative code should throw exception"); + } catch (IllegalArgumentException e) { + } + } + + public void testCreateInstanceFromDDMMYYY() throws Exception { + SerialDate date = createInstance(1, JANUARY, 1900); + assertEquals(1, date.getDayOfMonth()); + assertEquals(JANUARY, date.getMonth()); + assertEquals(1900, date.getYYYY()); + assertEquals(2, date.toSerial()); + } + + public void testCreateInstanceFromSerial() throws Exception { + assertEquals(d(1, JANUARY, 1900), createInstance(2)); + assertEquals(d(1, JANUARY, 1901), createInstance(367)); + } + + public void testCreateInstanceFromJavaDate() throws Exception { + assertEquals(d(1, JANUARY, 1900), createInstance(new GregorianCalendar(1900, 0, 1).getTime())); + assertEquals(d(1, JANUARY, 2006), createInstance(new GregorianCalendar(2006, 0, 1).getTime())); + } +} \ No newline at end of file diff --git a/src/test/java/chapter16/original/SerialDateTest.java b/src/test/java/chapter16/original/SerialDateTest.java new file mode 100644 index 0000000..742e0d1 --- /dev/null +++ b/src/test/java/chapter16/original/SerialDateTest.java @@ -0,0 +1,323 @@ +package chapter16.original; + +/* ======================================================================== + * JCommon : a free general purpose class library for the Java(tm) platform + * ======================================================================== + * + * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors. + * + * Project Info: http://www.jfree.org/jcommon/index.html + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * [Java is a trademark or registered trademark of Sun Microsystems, Inc. + * in the United States and other countries.] + * + * -------------------- + * SerialDateTests.java + * -------------------- + * (C) Copyright 2001-2005, by Object Refinery Limited. + * + * Original Author: David Gilbert (for Object Refinery Limited); + * Contributor(s): -; + * + * $Id: SerialDateTests.java,v 1.6 2005/11/16 15:58:40 taqua Exp $ + * + * Changes + * ------- + * 15-Nov-2001 : Version 1 (DG); + * 25-Jun-2002 : Removed unnecessary import (DG); + * 24-Oct-2002 : Fixed errors reported by Checkstyle (DG); + * 13-Mar-2003 : Added serialization test (DG); + * 05-Jan-2005 : Added test for bug report 1096282 (DG); + * + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.io.ObjectOutput; +import java.io.ObjectOutputStream; + +import clean.code.chapter16.original.MonthConstants; +import clean.code.chapter16.original.SerialDate; +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Some JUnit tests for the {@link SerialDate} class. + */ +public class SerialDateTest extends TestCase { + + /** Date representing November 9. */ + private SerialDate nov9Y2001; + + /** + * Creates a new test case. + * + * @param name the name. + */ + public SerialDateTest(final String name) { + super(name); + } + + /** + * Returns a test suite for the JUnit test runner. + * + * @return The test suite. + */ + public static Test suite() { + return new TestSuite(SerialDateTest.class); + } + + /** + * Problem set up. + */ + protected void setUp() { + this.nov9Y2001 = SerialDate.createInstance(9, MonthConstants.NOVEMBER, 2001); + } + + /** + * 9 Nov 2001 plus two months should be 9 Jan 2002. + */ + public void testAddMonthsTo9Nov2001() { + final SerialDate jan9Y2002 = SerialDate.addMonths(2, this.nov9Y2001); + final SerialDate answer = SerialDate.createInstance(9, 1, 2002); + assertEquals(answer, jan9Y2002); + } + + /** + * A test case for a reported bug, now fixed. + */ + public void testAddMonthsTo5Oct2003() { + final SerialDate d1 = SerialDate.createInstance(5, MonthConstants.OCTOBER, 2003); + final SerialDate d2 = SerialDate.addMonths(2, d1); + assertEquals(d2, SerialDate.createInstance(5, MonthConstants.DECEMBER, 2003)); + } + + /** + * A test case for a reported bug, now fixed. + */ + public void testAddMonthsTo1Jan2003() { + final SerialDate d1 = SerialDate.createInstance(1, MonthConstants.JANUARY, 2003); + final SerialDate d2 = SerialDate.addMonths(0, d1); + assertEquals(d2, d1); + } + + /** + * Monday preceding Friday 9 November 2001 should be 5 November. + */ + public void testMondayPrecedingFriday9Nov2001() { + SerialDate mondayBefore = SerialDate.getPreviousDayOfWeek( + SerialDate.MONDAY, this.nov9Y2001 + ); + assertEquals(5, mondayBefore.getDayOfMonth()); + } + + /** + * Monday following Friday 9 November 2001 should be 12 November. + */ + public void testMondayFollowingFriday9Nov2001() { + SerialDate mondayAfter = SerialDate.getFollowingDayOfWeek( + SerialDate.MONDAY, this.nov9Y2001 + ); + assertEquals(12, mondayAfter.getDayOfMonth()); + } + + /** + * Monday nearest Friday 9 November 2001 should be 12 November. + */ + public void testMondayNearestFriday9Nov2001() { + SerialDate mondayNearest = SerialDate.getNearestDayOfWeek( + SerialDate.MONDAY, this.nov9Y2001 + ); + assertEquals(12, mondayNearest.getDayOfMonth()); + } + + /** + * The Monday nearest to 22nd January 1970 falls on the 19th. + */ + public void testMondayNearest22Jan1970() { + SerialDate jan22Y1970 = SerialDate.createInstance(22, MonthConstants.JANUARY, 1970); + SerialDate mondayNearest=SerialDate.getNearestDayOfWeek(SerialDate.MONDAY, jan22Y1970); + assertEquals(19, mondayNearest.getDayOfMonth()); + } + + /** + * Problem that the conversion of days to strings returns the right result. Actually, this + * result depends on the Locale so this test needs to be modified. + */ + public void testWeekdayCodeToString() { + final String test = SerialDate.weekdayCodeToString(SerialDate.SATURDAY); + assertEquals("Saturday", test); + + // TODO: 5/1/2021 this function is case sensitive which is not our expected behavior + final String test2 = SerialDate.weekdayCodeToString(SerialDate.MONDAY); + assertEquals("monday", test2); + } + + /** + * Test the conversion of a string to a weekday. Note that this test will fail if the + * default locale doesn't use English weekday names...devise a better test! + */ + public void testStringToWeekday() { + + int weekday = SerialDate.stringToWeekdayCode("Wednesday"); + assertEquals(SerialDate.WEDNESDAY, weekday); + + weekday = SerialDate.stringToWeekdayCode(" Wednesday "); + assertEquals(SerialDate.WEDNESDAY, weekday); + + weekday = SerialDate.stringToWeekdayCode("Wed"); + assertEquals(SerialDate.WEDNESDAY, weekday); + + } + + /** + * Test the conversion of a string to a month. Note that this test will fail if the + * default locale doesn't use English month names...devise a better test! + */ + public void testStringToMonthCode() { + + int m = SerialDate.stringToMonthCode("January"); + assertEquals(MonthConstants.JANUARY, m); + + m = SerialDate.stringToMonthCode(" January "); + assertEquals(MonthConstants.JANUARY, m); + + m = SerialDate.stringToMonthCode("Jan"); + assertEquals(MonthConstants.JANUARY, m); + + } + + /** + * Tests the conversion of a month code to a string. + */ + public void testMonthCodeToStringCode() { + + final String test = SerialDate.monthCodeToString(MonthConstants.DECEMBER); + assertEquals("December", test); + + } + + /** + * 1900 is not a leap year. + */ + public void testIsNotLeapYear1900() { + assertTrue(!SerialDate.isLeapYear(1900)); + } + + /** + * 2000 is a leap year. + */ + public void testIsLeapYear2000() { + assertTrue(SerialDate.isLeapYear(2000)); + } + + /** + * The number of leap years from 1900 up-to-and-including 1899 is 0. + */ + public void testLeapYearCount1899() { + assertEquals(SerialDate.leapYearCount(1899), 0); + } + + /** + * The number of leap years from 1900 up-to-and-including 1903 is 0. + */ + public void testLeapYearCount1903() { + assertEquals(SerialDate.leapYearCount(1903), 0); + } + + /** + * The number of leap years from 1900 up-to-and-including 1904 is 1. + */ + public void testLeapYearCount1904() { + assertEquals(SerialDate.leapYearCount(1904), 1); + } + + /** + * The number of leap years from 1900 up-to-and-including 1999 is 24. + */ + public void testLeapYearCount1999() { + assertEquals(SerialDate.leapYearCount(1999), 24); + } + + /** + * The number of leap years from 1900 up-to-and-including 2000 is 25. + */ + public void testLeapYearCount2000() { + assertEquals(SerialDate.leapYearCount(2000), 25); + } + + /** + * Serialize an instance, restore it, and check for equality. + */ + public void testSerialization() { + + SerialDate d1 = SerialDate.createInstance(15, 4, 2000); + SerialDate d2 = null; + + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ObjectOutput out = new ObjectOutputStream(buffer); + out.writeObject(d1); + out.close(); + + ObjectInput in = new ObjectInputStream( new ByteArrayInputStream(buffer.toByteArray())); + d2 = (SerialDate) in.readObject(); + in.close(); + } + catch (Exception e) { + System.out.println(e.toString()); + } + assertEquals(d1, d2); + + } + + /** + * A test for bug report 1096282 (now fixed). + */ + public void test1096282() { + SerialDate d = SerialDate.createInstance(29, 2, 2004); + d = SerialDate.addYears(1, d); + SerialDate expected = SerialDate.createInstance(28, 2, 2005); + assertTrue(d.isOn(expected)); + } + + /** + * Miscellaneous tests for the addMonths() method. + */ + public void testAddMonths() { + SerialDate d1 = SerialDate.createInstance(31, 5, 2004); + + SerialDate d2 = SerialDate.addMonths(1, d1); + assertEquals(30, d2.getDayOfMonth()); + assertEquals(6, d2.getMonth()); + assertEquals(2004, d2.getYYYY()); + + SerialDate d3 = SerialDate.addMonths(2, d1); + assertEquals(31, d3.getDayOfMonth()); + assertEquals(7, d3.getMonth()); + assertEquals(2004, d3.getYYYY()); + + SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1)); + assertEquals(30, d4.getDayOfMonth()); + assertEquals(7, d4.getMonth()); + assertEquals(2004, d4.getYYYY()); + } + } \ No newline at end of file diff --git a/src/test/java/chapter16/solution/BobsSerialDateTest.java b/src/test/java/chapter16/solution/BobsSerialDateTest.java new file mode 100644 index 0000000..ee31e5a --- /dev/null +++ b/src/test/java/chapter16/solution/BobsSerialDateTest.java @@ -0,0 +1,466 @@ +package chapter16.solution; + +import clean.code.chapter16.original.SerialDate; +import clean.code.chapter16.original.SpreadsheetDate; +import junit.framework.TestCase; + +import java.util.GregorianCalendar; + +import static clean.code.chapter16.original.MonthConstants.*; +import static clean.code.chapter16.original.SerialDate.*; + +///////////////////////////////////// +// NOTE - THESE DON'T WORK YET!!! +///////////////////////////////////// + +public class BobsSerialDateTest extends TestCase { + + public void testIsValidWeekdayCode() throws Exception { + for (int day = 1; day <= 7; day++) + assertTrue(isValidWeekdayCode(day)); + assertFalse(isValidWeekdayCode(0)); + assertFalse(isValidWeekdayCode(8)); + } + + public void testStringToWeekdayCode() throws Exception { + + assertEquals(-1, stringToWeekdayCode("Hello")); + assertEquals(MONDAY, stringToWeekdayCode("Monday")); + assertEquals(MONDAY, stringToWeekdayCode("Mon")); + //todo assertEquals(MONDAY,stringToWeekdayCode("monday")); + // assertEquals(MONDAY,stringToWeekdayCode("MONDAY")); + // assertEquals(MONDAY, stringToWeekdayCode("mon")); + + assertEquals(TUESDAY, stringToWeekdayCode("Tuesday")); + assertEquals(TUESDAY, stringToWeekdayCode("Tue")); + // assertEquals(TUESDAY,stringToWeekdayCode("tuesday")); + // assertEquals(TUESDAY,stringToWeekdayCode("TUESDAY")); + // assertEquals(TUESDAY, stringToWeekdayCode("tue")); + // assertEquals(TUESDAY, stringToWeekdayCode("tues")); + + assertEquals(WEDNESDAY, stringToWeekdayCode("Wednesday")); + assertEquals(WEDNESDAY, stringToWeekdayCode("Wed")); + // assertEquals(WEDNESDAY,stringToWeekdayCode("wednesday")); + // assertEquals(WEDNESDAY,stringToWeekdayCode("WEDNESDAY")); + // assertEquals(WEDNESDAY, stringToWeekdayCode("wed")); + + assertEquals(THURSDAY, stringToWeekdayCode("Thursday")); + assertEquals(THURSDAY, stringToWeekdayCode("Thu")); + // assertEquals(THURSDAY,stringToWeekdayCode("thursday")); + // assertEquals(THURSDAY,stringToWeekdayCode("THURSDAY")); + // assertEquals(THURSDAY, stringToWeekdayCode("thu")); + // assertEquals(THURSDAY, stringToWeekdayCode("thurs")); + + assertEquals(FRIDAY, stringToWeekdayCode("Friday")); + assertEquals(FRIDAY, stringToWeekdayCode("Fri")); + // assertEquals(FRIDAY,stringToWeekdayCode("friday")); + // assertEquals(FRIDAY,stringToWeekdayCode("FRIDAY")); + // assertEquals(FRIDAY, stringToWeekdayCode("fri")); + + assertEquals(SATURDAY, stringToWeekdayCode("Saturday")); + assertEquals(SATURDAY, stringToWeekdayCode("Sat")); + // assertEquals(SATURDAY,stringToWeekdayCode("saturday")); + // assertEquals(SATURDAY,stringToWeekdayCode("SATURDAY")); + // assertEquals(SATURDAY, stringToWeekdayCode("sat")); + + assertEquals(SUNDAY, stringToWeekdayCode("Sunday")); + assertEquals(SUNDAY, stringToWeekdayCode("Sun")); + // assertEquals(SUNDAY,stringToWeekdayCode("sunday")); + // assertEquals(SUNDAY,stringToWeekdayCode("SUNDAY")); + // assertEquals(SUNDAY, stringToWeekdayCode("sun")); + } + + public void testWeekdayCodeToString() throws Exception { + assertEquals("Sunday", weekdayCodeToString(SUNDAY)); + assertEquals("Monday", weekdayCodeToString(MONDAY)); + assertEquals("Tuesday", weekdayCodeToString(TUESDAY)); + assertEquals("Wednesday", weekdayCodeToString(WEDNESDAY)); + assertEquals("Thursday", weekdayCodeToString(THURSDAY)); + assertEquals("Friday", weekdayCodeToString(FRIDAY)); + assertEquals("Saturday", weekdayCodeToString(SATURDAY)); + } + + public void testIsValidMonthCode() throws Exception { + for (int i = 1; i <= 12; i++) + assertTrue(isValidMonthCode(i)); + assertFalse(isValidMonthCode(0)); + assertFalse(isValidMonthCode(13)); + } + + public void testMonthToQuarter() throws Exception { + assertEquals(1, monthCodeToQuarter(JANUARY)); + assertEquals(1, monthCodeToQuarter(FEBRUARY)); + assertEquals(1, monthCodeToQuarter(MARCH)); + assertEquals(2, monthCodeToQuarter(APRIL)); + assertEquals(2, monthCodeToQuarter(MAY)); + assertEquals(2, monthCodeToQuarter(JUNE)); + assertEquals(3, monthCodeToQuarter(JULY)); + assertEquals(3, monthCodeToQuarter(AUGUST)); + assertEquals(3, monthCodeToQuarter(SEPTEMBER)); + assertEquals(4, monthCodeToQuarter(OCTOBER)); + assertEquals(4, monthCodeToQuarter(NOVEMBER)); + assertEquals(4, monthCodeToQuarter(DECEMBER)); + + try { + monthCodeToQuarter(-1); + fail("Invalid Month Code should throw exception"); + } catch (IllegalArgumentException e) { + } + } + + public void testMonthCodeToString() throws Exception { + assertEquals("January", monthCodeToString(JANUARY)); + assertEquals("February", monthCodeToString(FEBRUARY)); + assertEquals("March", monthCodeToString(MARCH)); + assertEquals("April", monthCodeToString(APRIL)); + assertEquals("May", monthCodeToString(MAY)); + assertEquals("June", monthCodeToString(JUNE)); + assertEquals("July", monthCodeToString(JULY)); + assertEquals("August", monthCodeToString(AUGUST)); + assertEquals("September", monthCodeToString(SEPTEMBER)); + assertEquals("October", monthCodeToString(OCTOBER)); + assertEquals("November", monthCodeToString(NOVEMBER)); + assertEquals("December", monthCodeToString(DECEMBER)); + + assertEquals("Jan", monthCodeToString(JANUARY, true)); + assertEquals("Feb", monthCodeToString(FEBRUARY, true)); + assertEquals("Mar", monthCodeToString(MARCH, true)); + assertEquals("Apr", monthCodeToString(APRIL, true)); + assertEquals("May", monthCodeToString(MAY, true)); + assertEquals("Jun", monthCodeToString(JUNE, true)); + assertEquals("Jul", monthCodeToString(JULY, true)); + assertEquals("Aug", monthCodeToString(AUGUST, true)); + assertEquals("Sep", monthCodeToString(SEPTEMBER, true)); + assertEquals("Oct", monthCodeToString(OCTOBER, true)); + assertEquals("Nov", monthCodeToString(NOVEMBER, true)); + assertEquals("Dec", monthCodeToString(DECEMBER, true)); + + try { + monthCodeToString(-1); + fail("Invalid month code should throw exception"); + } catch (IllegalArgumentException e) { + } + + } + + public void testStringToMonthCode() throws Exception { + assertEquals(JANUARY, stringToMonthCode("1")); + assertEquals(FEBRUARY, stringToMonthCode("2")); + assertEquals(MARCH, stringToMonthCode("3")); + assertEquals(APRIL, stringToMonthCode("4")); + assertEquals(MAY, stringToMonthCode("5")); + assertEquals(JUNE, stringToMonthCode("6")); + assertEquals(JULY, stringToMonthCode("7")); + assertEquals(AUGUST, stringToMonthCode("8")); + assertEquals(SEPTEMBER, stringToMonthCode("9")); + assertEquals(OCTOBER, stringToMonthCode("10")); + assertEquals(NOVEMBER, stringToMonthCode("11")); + assertEquals(DECEMBER, stringToMonthCode("12")); + + //todo assertEquals(-1, stringToMonthCode("0")); + // assertEquals(-1, stringToMonthCode("13")); + + assertEquals(-1, stringToMonthCode("Hello")); + + for (int m = 1; m <= 12; m++) { + assertEquals(m, stringToMonthCode(monthCodeToString(m, false))); + assertEquals(m, stringToMonthCode(monthCodeToString(m, true))); + } + + // assertEquals(1,stringToMonthCode("jan")); + // assertEquals(2,stringToMonthCode("feb")); + // assertEquals(3,stringToMonthCode("mar")); + // assertEquals(4,stringToMonthCode("apr")); + // assertEquals(5,stringToMonthCode("may")); + // assertEquals(6,stringToMonthCode("jun")); + // assertEquals(7,stringToMonthCode("jul")); + // assertEquals(8,stringToMonthCode("aug")); + // assertEquals(9,stringToMonthCode("sep")); + // assertEquals(10,stringToMonthCode("oct")); + // assertEquals(11,stringToMonthCode("nov")); + // assertEquals(12,stringToMonthCode("dec")); + + // assertEquals(1,stringToMonthCode("JAN")); + // assertEquals(2,stringToMonthCode("FEB")); + // assertEquals(3,stringToMonthCode("MAR")); + // assertEquals(4,stringToMonthCode("APR")); + // assertEquals(5,stringToMonthCode("MAY")); + // assertEquals(6,stringToMonthCode("JUN")); + // assertEquals(7,stringToMonthCode("JUL")); + // assertEquals(8,stringToMonthCode("AUG")); + // assertEquals(9,stringToMonthCode("SEP")); + // assertEquals(10,stringToMonthCode("OCT")); + // assertEquals(11,stringToMonthCode("NOV")); + // assertEquals(12,stringToMonthCode("DEC")); + + // assertEquals(1,stringToMonthCode("january")); + // assertEquals(2,stringToMonthCode("february")); + // assertEquals(3,stringToMonthCode("march")); + // assertEquals(4,stringToMonthCode("april")); + // assertEquals(5,stringToMonthCode("may")); + // assertEquals(6,stringToMonthCode("june")); + // assertEquals(7,stringToMonthCode("july")); + // assertEquals(8,stringToMonthCode("august")); + // assertEquals(9,stringToMonthCode("september")); + // assertEquals(10,stringToMonthCode("october")); + // assertEquals(11,stringToMonthCode("november")); + // assertEquals(12,stringToMonthCode("december")); + + // assertEquals(1,stringToMonthCode("JANUARY")); + // assertEquals(2,stringToMonthCode("FEBRUARY")); + // assertEquals(3,stringToMonthCode("MAR")); + // assertEquals(4,stringToMonthCode("APRIL")); + // assertEquals(5,stringToMonthCode("MAY")); + // assertEquals(6,stringToMonthCode("JUNE")); + // assertEquals(7,stringToMonthCode("JULY")); + // assertEquals(8,stringToMonthCode("AUGUST")); + // assertEquals(9,stringToMonthCode("SEPTEMBER")); + // assertEquals(10,stringToMonthCode("OCTOBER")); + // assertEquals(11,stringToMonthCode("NOVEMBER")); + // assertEquals(12,stringToMonthCode("DECEMBER")); + } + + public void testIsValidWeekInMonthCode() throws Exception { + for (int w = 0; w <= 4; w++) { + assertTrue(isValidWeekInMonthCode(w)); + } + assertFalse(isValidWeekInMonthCode(5)); + } + + public void testIsLeapYear() throws Exception { + assertFalse(isLeapYear(1900)); + assertFalse(isLeapYear(1901)); + assertFalse(isLeapYear(1902)); + assertFalse(isLeapYear(1903)); + assertTrue(isLeapYear(1904)); + assertTrue(isLeapYear(1908)); + assertFalse(isLeapYear(1955)); + assertTrue(isLeapYear(1964)); + assertTrue(isLeapYear(1980)); + assertTrue(isLeapYear(2000)); + assertFalse(isLeapYear(2001)); + assertFalse(isLeapYear(2100)); + } + + public void testLeapYearCount() throws Exception { + assertEquals(0, leapYearCount(1900)); + assertEquals(0, leapYearCount(1901)); + assertEquals(0, leapYearCount(1902)); + assertEquals(0, leapYearCount(1903)); + assertEquals(1, leapYearCount(1904)); + assertEquals(1, leapYearCount(1905)); + assertEquals(1, leapYearCount(1906)); + assertEquals(1, leapYearCount(1907)); + assertEquals(2, leapYearCount(1908)); + assertEquals(24, leapYearCount(1999)); + assertEquals(25, leapYearCount(2001)); + assertEquals(49, leapYearCount(2101)); + assertEquals(73, leapYearCount(2201)); + assertEquals(97, leapYearCount(2301)); + assertEquals(122, leapYearCount(2401)); + } + + public void testLastDayOfMonth() throws Exception { + assertEquals(31, lastDayOfMonth(JANUARY, 1901)); + assertEquals(28, lastDayOfMonth(FEBRUARY, 1901)); + assertEquals(31, lastDayOfMonth(MARCH, 1901)); + assertEquals(30, lastDayOfMonth(APRIL, 1901)); + assertEquals(31, lastDayOfMonth(MAY, 1901)); + assertEquals(30, lastDayOfMonth(JUNE, 1901)); + assertEquals(31, lastDayOfMonth(JULY, 1901)); + assertEquals(31, lastDayOfMonth(AUGUST, 1901)); + assertEquals(30, lastDayOfMonth(SEPTEMBER, 1901)); + assertEquals(31, lastDayOfMonth(OCTOBER, 1901)); + assertEquals(30, lastDayOfMonth(NOVEMBER, 1901)); + assertEquals(31, lastDayOfMonth(DECEMBER, 1901)); + assertEquals(29, lastDayOfMonth(FEBRUARY, 1904)); + } + + public void testAddDays() throws Exception { + SerialDate newYears = d(1, JANUARY, 1900); + assertEquals(d(2, JANUARY, 1900), addDays(1, newYears)); + assertEquals(d(1, FEBRUARY, 1900), addDays(31, newYears)); + assertEquals(d(1, JANUARY, 1901), addDays(365, newYears)); + assertEquals(d(31, DECEMBER, 1904), addDays(5 * 365, newYears)); + } + + private static clean.code.chapter16.original.SpreadsheetDate d(int day, int month, int year) { + return new SpreadsheetDate(day, month, year); + } + + public void testAddMonths() throws Exception { + assertEquals(d(1, FEBRUARY, 1900), addMonths(1, d(1, JANUARY, 1900))); + assertEquals(d(28, FEBRUARY, 1900), addMonths(1, d(31, JANUARY, 1900))); + assertEquals(d(28, FEBRUARY, 1900), addMonths(1, d(30, JANUARY, 1900))); + assertEquals(d(28, FEBRUARY, 1900), addMonths(1, d(29, JANUARY, 1900))); + assertEquals(d(28, FEBRUARY, 1900), addMonths(1, d(28, JANUARY, 1900))); + assertEquals(d(27, FEBRUARY, 1900), addMonths(1, d(27, JANUARY, 1900))); + + assertEquals(d(30, JUNE, 1900), addMonths(5, d(31, JANUARY, 1900))); + assertEquals(d(30, JUNE, 1901), addMonths(17, d(31, JANUARY, 1900))); + + assertEquals(d(29, FEBRUARY, 1904), addMonths(49, d(31, JANUARY, 1900))); + + } + + public void testAddYears() throws Exception { + assertEquals(d(1, JANUARY, 1901), addYears(1, d(1, JANUARY, 1900))); + assertEquals(d(28, FEBRUARY, 1905), addYears(1, d(29, FEBRUARY, 1904))); + assertEquals(d(28, FEBRUARY, 1905), addYears(1, d(28, FEBRUARY, 1904))); + assertEquals(d(28, FEBRUARY, 1904), addYears(1, d(28, FEBRUARY, 1903))); + } + + public void testGetPreviousDayOfWeek() throws Exception { + assertEquals(d(24, FEBRUARY, 2006), getPreviousDayOfWeek(FRIDAY, d(1, MARCH, 2006))); + assertEquals(d(22, FEBRUARY, 2006), getPreviousDayOfWeek(WEDNESDAY, d(1, MARCH, 2006))); + assertEquals(d(29, FEBRUARY, 2004), getPreviousDayOfWeek(SUNDAY, d(3, MARCH, 2004))); + assertEquals(d(29, DECEMBER, 2004), getPreviousDayOfWeek(WEDNESDAY, d(5, JANUARY, 2005))); + + try { + getPreviousDayOfWeek(-1, d(1, JANUARY, 2006)); + fail("Invalid day of week code should throw exception"); + } catch (IllegalArgumentException e) { + } + } + + public void testGetFollowingDayOfWeek() throws Exception { + // assertEquals(d(1, JANUARY, 2005),getFollowingDayOfWeek(SATURDAY, d(25, DECEMBER, 2004))); + assertEquals(d(1, JANUARY, 2005), getFollowingDayOfWeek(SATURDAY, d(26, DECEMBER, 2004))); + assertEquals(d(3, MARCH, 2004), getFollowingDayOfWeek(WEDNESDAY, d(28, FEBRUARY, 2004))); + + try { + getFollowingDayOfWeek(-1, d(1, JANUARY, 2006)); + fail("Invalid day of week code should throw exception"); + } catch (IllegalArgumentException e) { + } + } + + public void testGetNearestDayOfWeek() throws Exception { + assertEquals(d(16, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(16, APRIL, 2006))); + assertEquals(d(16, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(17, APRIL, 2006))); + assertEquals(d(16, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(18, APRIL, 2006))); + assertEquals(d(16, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(19, APRIL, 2006))); + assertEquals(d(23, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(20, APRIL, 2006))); + assertEquals(d(23, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(21, APRIL, 2006))); + assertEquals(d(23, APRIL, 2006), getNearestDayOfWeek(SUNDAY, d(22, APRIL, 2006))); + + //todo assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(16, APRIL, 2006))); + assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(17, APRIL, 2006))); + assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(18, APRIL, 2006))); + assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(19, APRIL, 2006))); + assertEquals(d(17, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(20, APRIL, 2006))); + assertEquals(d(24, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(21, APRIL, 2006))); + assertEquals(d(24, APRIL, 2006), getNearestDayOfWeek(MONDAY, d(22, APRIL, 2006))); + + // assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(16, APRIL, 2006))); + // assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(17, APRIL, 2006))); + assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(18, APRIL, 2006))); + assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(19, APRIL, 2006))); + assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(20, APRIL, 2006))); + assertEquals(d(18, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(21, APRIL, 2006))); + assertEquals(d(25, APRIL, 2006), getNearestDayOfWeek(TUESDAY, d(22, APRIL, 2006))); + + // assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(16, APRIL, 2006))); + // assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(17, APRIL, 2006))); + // assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(18, APRIL, 2006))); + assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(19, APRIL, 2006))); + assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(20, APRIL, 2006))); + assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(21, APRIL, 2006))); + assertEquals(d(19, APRIL, 2006), getNearestDayOfWeek(WEDNESDAY, d(22, APRIL, 2006))); + + // assertEquals(d(13, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(16, APRIL, 2006))); + // assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(17, APRIL, 2006))); + // assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(18, APRIL, 2006))); + // assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(19, APRIL, 2006))); + assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(20, APRIL, 2006))); + assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(21, APRIL, 2006))); + assertEquals(d(20, APRIL, 2006), getNearestDayOfWeek(THURSDAY, d(22, APRIL, 2006))); + + // assertEquals(d(14, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(16, APRIL, 2006))); + // assertEquals(d(14, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(17, APRIL, 2006))); + // assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(18, APRIL, 2006))); + // assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(19, APRIL, 2006))); + // assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(20, APRIL, 2006))); + assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(21, APRIL, 2006))); + assertEquals(d(21, APRIL, 2006), getNearestDayOfWeek(FRIDAY, d(22, APRIL, 2006))); + + // assertEquals(d(15, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(16, APRIL, 2006))); + // assertEquals(d(15, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(17, APRIL, 2006))); + // assertEquals(d(15, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(18, APRIL, 2006))); + // assertEquals(d(22, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(19, APRIL, 2006))); + // assertEquals(d(22, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(20, APRIL, 2006))); + // assertEquals(d(22, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(21, APRIL, 2006))); + assertEquals(d(22, APRIL, 2006), getNearestDayOfWeek(SATURDAY, d(22, APRIL, 2006))); + + try { + getNearestDayOfWeek(-1, d(1, JANUARY, 2006)); + fail("Invalid day of week code should throw exception"); + } catch (IllegalArgumentException e) { + } + } + + public void testEndOfCurrentMonth() throws Exception { + SerialDate d = SerialDate.createInstance(2); + assertEquals(d(31, JANUARY, 2006), d.getEndOfCurrentMonth(d(1, JANUARY, 2006))); + assertEquals(d(28, FEBRUARY, 2006), d.getEndOfCurrentMonth(d(1, FEBRUARY, 2006))); + assertEquals(d(31, MARCH, 2006), d.getEndOfCurrentMonth(d(1, MARCH, 2006))); + assertEquals(d(30, APRIL, 2006), d.getEndOfCurrentMonth(d(1, APRIL, 2006))); + assertEquals(d(31, MAY, 2006), d.getEndOfCurrentMonth(d(1, MAY, 2006))); + assertEquals(d(30, JUNE, 2006), d.getEndOfCurrentMonth(d(1, JUNE, 2006))); + assertEquals(d(31, JULY, 2006), d.getEndOfCurrentMonth(d(1, JULY, 2006))); + assertEquals(d(31, AUGUST, 2006), d.getEndOfCurrentMonth(d(1, AUGUST, 2006))); + assertEquals(d(30, SEPTEMBER, 2006), d.getEndOfCurrentMonth(d(1, SEPTEMBER, 2006))); + assertEquals(d(31, OCTOBER, 2006), d.getEndOfCurrentMonth(d(1, OCTOBER, 2006))); + assertEquals(d(30, NOVEMBER, 2006), d.getEndOfCurrentMonth(d(1, NOVEMBER, 2006))); + assertEquals(d(31, DECEMBER, 2006), d.getEndOfCurrentMonth(d(1, DECEMBER, 2006))); + assertEquals(d(29, FEBRUARY, 2008), d.getEndOfCurrentMonth(d(1, FEBRUARY, 2008))); + } + + public void testWeekInMonthToString() throws Exception { + assertEquals("First", weekInMonthToString(FIRST_WEEK_IN_MONTH)); + assertEquals("Second", weekInMonthToString(SECOND_WEEK_IN_MONTH)); + assertEquals("Third", weekInMonthToString(THIRD_WEEK_IN_MONTH)); + assertEquals("Fourth", weekInMonthToString(FOURTH_WEEK_IN_MONTH)); + assertEquals("Last", weekInMonthToString(LAST_WEEK_IN_MONTH)); + + //todo try { + // weekInMonthToString(-1); + // fail("Invalid week code should throw exception"); + // } catch (IllegalArgumentException e) { + // } + } + + public void testRelativeToString() throws Exception { + assertEquals("Preceding", relativeToString(PRECEDING)); + assertEquals("Nearest", relativeToString(NEAREST)); + assertEquals("Following", relativeToString(FOLLOWING)); + + //todo try { + // relativeToString(-1000); + // fail("Invalid relative code should throw exception"); + // } catch (IllegalArgumentException e) { + // } + } + + public void testCreateInstanceFromDDMMYYY() throws Exception { + SerialDate date = createInstance(1, JANUARY, 1900); + assertEquals(1, date.getDayOfMonth()); + assertEquals(JANUARY, date.getMonth()); + assertEquals(1900, date.getYYYY()); + assertEquals(2, date.toSerial()); + } + + public void testCreateInstanceFromSerial() throws Exception { + assertEquals(d(1, JANUARY, 1900), createInstance(2)); + assertEquals(d(1, JANUARY, 1901), createInstance(367)); + } + + public void testCreateInstanceFromJavaDate() throws Exception { + assertEquals(d(1, JANUARY, 1900), createInstance(new GregorianCalendar(1900, 0, 1).getTime())); + assertEquals(d(1, JANUARY, 2006), createInstance(new GregorianCalendar(2006, 0, 1).getTime())); + } + +// public static void main(String[] args) { +// junit.textui.TestRunner.run(BobsSerialDateTest.class); +// } +} \ No newline at end of file diff --git a/src/test/java/chapter16/solution/SerialDateTest.java b/src/test/java/chapter16/solution/SerialDateTest.java new file mode 100644 index 0000000..faefa99 --- /dev/null +++ b/src/test/java/chapter16/solution/SerialDateTest.java @@ -0,0 +1,320 @@ +package chapter16.solution; + +/* ======================================================================== + * JCommon : a free general purpose class library for the Java(tm) platform + * ======================================================================== + * + * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors. + * + * Project Info: http://www.jfree.org/jcommon/index.html + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * [Java is a trademark or registered trademark of Sun Microsystems, Inc. + * in the United States and other countries.] + * + * -------------------- + * SerialDateTests.java + * -------------------- + * (C) Copyright 2001-2005, by Object Refinery Limited. + * + * Original Author: David Gilbert (for Object Refinery Limited); + * Contributor(s): -; + * + * $Id: SerialDateTests.java,v 1.6 2005/11/16 15:58:40 taqua Exp $ + * + * Changes + * ------- + * 15-Nov-2001 : Version 1 (DG); + * 25-Jun-2002 : Removed unnecessary import (DG); + * 24-Oct-2002 : Fixed errors reported by Checkstyle (DG); + * 13-Mar-2003 : Added serialization test (DG); + * 05-Jan-2005 : Added test for bug report 1096282 (DG); + * + */ + +import clean.code.chapter16.original.MonthConstants; +import clean.code.chapter16.original.SerialDate; +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import java.io.*; + +///////////////////////////////////// +// NOTE - THESE DON'T WORK YET!!! +///////////////////////////////////// + +/** + * Some JUnit tests for the {@link clean.code.chapter16.original.SerialDate} class. + */ +public class SerialDateTest extends TestCase { + + /** Date representing November 9. */ + private SerialDate nov9Y2001; + + /** + * Creates a new test case. + * + * @param name the name. + */ + public SerialDateTest(final String name) { + super(name); + } + + /** + * Returns a test suite for the JUnit test runner. + * + * @return The test suite. + */ + public static Test suite() { + return new TestSuite(SerialDateTest.class); + } + + /** + * Problem set up. + */ + protected void setUp() { + this.nov9Y2001 = SerialDate.createInstance(9, MonthConstants.NOVEMBER, 2001); + } + + /** + * 9 Nov 2001 plus two months should be 9 Jan 2002. + */ + public void testAddMonthsTo9Nov2001() { + final SerialDate jan9Y2002 = SerialDate.addMonths(2, this.nov9Y2001); + final SerialDate answer = SerialDate.createInstance(9, 1, 2002); + assertEquals(answer, jan9Y2002); + } + + /** + * A test case for a reported bug, now fixed. + */ + public void testAddMonthsTo5Oct2003() { + final SerialDate d1 = SerialDate.createInstance(5, MonthConstants.OCTOBER, 2003); + final SerialDate d2 = SerialDate.addMonths(2, d1); + assertEquals(d2, SerialDate.createInstance(5, MonthConstants.DECEMBER, 2003)); + } + + /** + * A test case for a reported bug, now fixed. + */ + public void testAddMonthsTo1Jan2003() { + final SerialDate d1 = SerialDate.createInstance(1, MonthConstants.JANUARY, 2003); + final SerialDate d2 = SerialDate.addMonths(0, d1); + assertEquals(d2, d1); + } + + /** + * Monday preceding Friday 9 November 2001 should be 5 November. + */ + public void testMondayPrecedingFriday9Nov2001() { + SerialDate mondayBefore = SerialDate.getPreviousDayOfWeek( + SerialDate.MONDAY, this.nov9Y2001 + ); + assertEquals(5, mondayBefore.getDayOfMonth()); + } + + /** + * Monday following Friday 9 November 2001 should be 12 November. + */ + public void testMondayFollowingFriday9Nov2001() { + SerialDate mondayAfter = SerialDate.getFollowingDayOfWeek( + SerialDate.MONDAY, this.nov9Y2001 + ); + assertEquals(12, mondayAfter.getDayOfMonth()); + } + + /** + * Monday nearest Friday 9 November 2001 should be 12 November. + */ + public void testMondayNearestFriday9Nov2001() { + SerialDate mondayNearest = SerialDate.getNearestDayOfWeek( + SerialDate.MONDAY, this.nov9Y2001 + ); + assertEquals(12, mondayNearest.getDayOfMonth()); + } + + /** + * The Monday nearest to 22nd January 1970 falls on the 19th. + */ + public void testMondayNearest22Jan1970() { + SerialDate jan22Y1970 = SerialDate.createInstance(22, MonthConstants.JANUARY, 1970); + SerialDate mondayNearest=SerialDate.getNearestDayOfWeek(SerialDate.MONDAY, jan22Y1970); + assertEquals(19, mondayNearest.getDayOfMonth()); + } + + /** + * Problem that the conversion of days to strings returns the right result. Actually, this + * result depends on the Locale so this test needs to be modified. + */ + public void testWeekdayCodeToString() { + + final String test = SerialDate.weekdayCodeToString(SerialDate.SATURDAY); + assertEquals("Saturday", test); + + } + + /** + * Test the conversion of a string to a weekday. Note that this test will fail if the + * default locale doesn't use English weekday names...devise a better test! + */ + public void testStringToWeekday() { + + int weekday = SerialDate.stringToWeekdayCode("Wednesday"); + assertEquals(SerialDate.WEDNESDAY, weekday); + + weekday = SerialDate.stringToWeekdayCode(" Wednesday "); + assertEquals(SerialDate.WEDNESDAY, weekday); + + weekday = SerialDate.stringToWeekdayCode("Wed"); + assertEquals(SerialDate.WEDNESDAY, weekday); + + } + + /** + * Test the conversion of a string to a month. Note that this test will fail if the + * default locale doesn't use English month names...devise a better test! + */ + public void testStringToMonthCode() { + + int m = SerialDate.stringToMonthCode("January"); + assertEquals(MonthConstants.JANUARY, m); + + m = SerialDate.stringToMonthCode(" January "); + assertEquals(MonthConstants.JANUARY, m); + + m = SerialDate.stringToMonthCode("Jan"); + assertEquals(MonthConstants.JANUARY, m); + + } + + /** + * Tests the conversion of a month code to a string. + */ + public void testMonthCodeToStringCode() { + + final String test = SerialDate.monthCodeToString(MonthConstants.DECEMBER); + assertEquals("December", test); + + } + + /** + * 1900 is not a leap year. + */ + public void testIsNotLeapYear1900() { + assertTrue(!SerialDate.isLeapYear(1900)); + } + + /** + * 2000 is a leap year. + */ + public void testIsLeapYear2000() { + assertTrue(SerialDate.isLeapYear(2000)); + } + + /** + * The number of leap years from 1900 up-to-and-including 1899 is 0. + */ + public void testLeapYearCount1899() { + assertEquals(SerialDate.leapYearCount(1899), 0); + } + + /** + * The number of leap years from 1900 up-to-and-including 1903 is 0. + */ + public void testLeapYearCount1903() { + assertEquals(SerialDate.leapYearCount(1903), 0); + } + + /** + * The number of leap years from 1900 up-to-and-including 1904 is 1. + */ + public void testLeapYearCount1904() { + assertEquals(SerialDate.leapYearCount(1904), 1); + } + + /** + * The number of leap years from 1900 up-to-and-including 1999 is 24. + */ + public void testLeapYearCount1999() { + assertEquals(SerialDate.leapYearCount(1999), 24); + } + + /** + * The number of leap years from 1900 up-to-and-including 2000 is 25. + */ + public void testLeapYearCount2000() { + assertEquals(SerialDate.leapYearCount(2000), 25); + } + + /** + * Serialize an instance, restore it, and check for equality. + */ + public void testSerialization() { + + SerialDate d1 = SerialDate.createInstance(15, 4, 2000); + SerialDate d2 = null; + + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ObjectOutput out = new ObjectOutputStream(buffer); + out.writeObject(d1); + out.close(); + + ObjectInput in = new ObjectInputStream( new ByteArrayInputStream(buffer.toByteArray())); + d2 = (SerialDate) in.readObject(); + in.close(); + } + catch (Exception e) { + System.out.println(e.toString()); + } + assertEquals(d1, d2); + + } + + /** + * A test for bug report 1096282 (now fixed). + */ + public void test1096282() { + SerialDate d = SerialDate.createInstance(29, 2, 2004); + d = SerialDate.addYears(1, d); + SerialDate expected = SerialDate.createInstance(28, 2, 2005); + assertTrue(d.isOn(expected)); + } + + /** + * Miscellaneous tests for the addMonths() method. + */ + public void testAddMonths() { + SerialDate d1 = SerialDate.createInstance(31, 5, 2004); + + SerialDate d2 = SerialDate.addMonths(1, d1); + assertEquals(30, d2.getDayOfMonth()); + assertEquals(6, d2.getMonth()); + assertEquals(2004, d2.getYYYY()); + + SerialDate d3 = SerialDate.addMonths(2, d1); + assertEquals(31, d3.getDayOfMonth()); + assertEquals(7, d3.getMonth()); + assertEquals(2004, d3.getYYYY()); + + SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1)); + assertEquals(30, d4.getDayOfMonth()); + assertEquals(7, d4.getMonth()); + assertEquals(2004, d4.getYYYY()); + } + } \ No newline at end of file