diff --git a/.gitignore b/.gitignore index a95aedad..a937b7ae 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,9 @@ target/ /.scannerwork/ /.bsp/sbt.json /src/it/resources/AIRBNB.Listing.csv +/src/it/resources/com/phasmidsoftware/examples/CSYE7200-FALL2021-Project.csv +/output.csv + +src/it/resources/2020VAERSData/ + +TeamProjectOutput.csv diff --git a/README.md b/README.md index 40ac7bde..589f497f 100644 --- a/README.md +++ b/README.md @@ -141,18 +141,29 @@ See section on _CellParsers_ below. ## Table -The _Table_ class has several methods for manipulation: -* def iterator: Iterator\[Row] -* def rows: Seq\[Row] -* def maybeHeader: Option\[Header] -* def map\[S](f: Row => S): Table\[S] -* def flatMap\[U](f: Row => Table\[U]): Table\[U] -* def unit\[S](rows: Seq\[S], maybeHeader: Option\[Header]): Table\[S] -* def ++\[U >: Row](table: Table\[U]): Table\[U] - -It is to be expected that _join_ methods will be added later. - -The following object methods are available for parsing text: +The _Table_ class, which implements _Iterable\[Row]_, also has several methods for manipulation: +### query methods +* def rows: Seq\[Row] +* def maybeHeader: Option\[Header] +* def toCSV(implicit renderer: CsvRenderer\[Row], generator: CsvProductGenerator\[Row], csvAttributes: CsvAttributes): Iterable\[String] +* def maybeColumnNames: Option\[Seq\[String]] +* def column(name: String): Iterator\[Option\[String]] + +### transformation methods +* def flatMap\[U](f: Row => Iterable\[U]): Table\[U] +* def unit\[S](rows: Iterable\[S], maybeHeader: Option\[Header]): Table\[S] +* def ++\[U >: Row](table: Table\[U]): Table\[U] +* def processRows\[S](f: Iterable\[Row] => Iterable\[S]): Table\[S] +* def processRows\[R, S](f: (Iterable\[Row], Iterable\[R]) => Iterable\[S])(other: Table\[R]): Table\[S] +* def sort\[S >: Row : Ordering]: Table\[S] +* def select(range: Range): Table\[Row] +* def select(n: Int): Table\[Row] +* lazy val shuffle: Table\[Row] + + +It is to be expected that _join_ methods will be added later (based upon the second signature of processRows). + +The following **object** methods are available for parsing text: * def parse\[T: TableParser](ws: Seq\[String]): Try\[T] * def parse\[T: TableParser](ws: Iterator\[String]): Try\[T] * def parse\[T: TableParser](x: => Source): Try\[T] @@ -331,19 +342,19 @@ The other case classes look like this: The _MovieParser_ object looks like this: object MovieParser extends CellParsers { - def camelCaseColumnNameMapper(w: String): String = w.replaceAll("([A-Z0-9])", "_$1") - implicit val movieColumnHelper: ColumnHelper[Movie] = columnHelper(camelCaseColumnNameMapper _, + def camelToSnakeCaseColumnNameMapper(w: String): String = w.replaceAll("([A-Z0-9])", "_$1") + implicit val movieColumnHelper: ColumnHelper[Movie] = columnHelper(camelToSnakeCaseColumnNameMapper _, "title" -> "movie_title", "imdb" -> "movie_imdb_link") - implicit val reviewsColumnHelper: ColumnHelper[Reviews] = columnHelper(camelCaseColumnNameMapper _, + implicit val reviewsColumnHelper: ColumnHelper[Reviews] = columnHelper(camelToSnakeCaseColumnNameMapper _, "facebookLikes" -> "movie_facebook_likes", "numUsersReview" -> "num_user_for_reviews", "numUsersVoted" -> "num_voted_users", "numCriticReviews" -> "num_critic_for_reviews", "totalFacebookLikes" -> "cast_total_facebook_likes") - implicit val formatColumnHelper: ColumnHelper[Format] = columnHelper(camelCaseColumnNameMapper _) - implicit val productionColumnHelper: ColumnHelper[Production] = columnHelper(camelCaseColumnNameMapper _) - implicit val principalColumnHelper: ColumnHelper[Principal] = columnHelper(camelCaseColumnNameMapper _, Some("$x_$c")) + implicit val formatColumnHelper: ColumnHelper[Format] = columnHelper(camelToSnakeCaseColumnNameMapper _) + implicit val productionColumnHelper: ColumnHelper[Production] = columnHelper(camelToSnakeCaseColumnNameMapper _) + implicit val principalColumnHelper: ColumnHelper[Principal] = columnHelper(camelToSnakeCaseColumnNameMapper _, Some("$x_$c")) implicit val ratingParser: CellParser[Rating] = cellParser(Rating.apply: String => Rating) implicit val formatParser: CellParser[Format] = cellParser4(Format) implicit val productionParser: CellParser[Production] = cellParser4(Production) @@ -402,6 +413,8 @@ A parameter can be optional, for example, in the _Movie_ example, the _Productio In this example, some movies do not have a budget provided. All you have to do is declare it optional in the case class and _TableParser_ will specify it as _Some(x)_ if valid, else _None_. +Note that there is a default, implicit _RowConfig_ object defined in the object _RowConfig_. + ## Example: Submissions This example has two variations on the earlier theme of the _Movies_ example: @@ -530,16 +543,55 @@ As with the parsing methods, the conversion between instances of types (especial If you need to set HTML attributes for a specific type, for example a row in the above example, then an attribute map can be defined for the _renderer2_ method. -## String Rendering +## CSV Rendering + +If you simply need to write a table to CSV (comma-separated value) format as a _String_, then use the _toCsv_ method of _Table\[T]_. +Note that there is also an object method of _Table_ called _toCsvRow_ which can be used for instances of _Table\[Row]_. +More control can be gained by using _CsvTableStringRenderer\[T]_ or _CsvTableFileRenderer\[T]_ for a particular type _T_. + +These require customizable (implicit) evidence parameters and are defined as follows: + + case class CsvTableStringRenderer[T: CsvRenderer : CsvGenerator]()(implicit csvAttributes: CsvAttributes) + extends CsvTableRenderer[T, StringBuilder]()(implicitly[CsvRenderer[T]], implicitly[CsvGenerator[T]], Writable.stringBuilderWritable(csvAttributes.delimiter, csvAttributes.quote), csvAttributes) + case class CsvTableFileRenderer[T: CsvRenderer : CsvGenerator](file: File)(implicit csvAttributes: CsvAttributes) + extends CsvTableRenderer[T, FileWriter]()(implicitly[CsvRenderer[T]], implicitly[CsvGenerator[T]], Writable.fileWritable(file), csvAttributes) + abstract class CsvTableRenderer[T: CsvRenderer : CsvGenerator, O: Writable]()(implicit csvAttributes: CsvAttributes) extends Renderer[Table[T], O] {...} + +_CsvRenderer\[T]_ determines the layout of the rows, while _CsvGenerator\[T]_ determines the header. +_CsvAttributes_ specify the delimiter and quote characters for the output. +Instances of each can be created using methods in _CsvRenderers_ and _CsvGenerators_ respectively. +Appropriate methods are: +* sequenceRenderer, optionRenderer, renderer1, renderer2, renderer3, etc. up to renderer12. +* sequenceGenerator, optionGenerator, generator1, generator2, generator3, etc. up to generator12. + +In some situations, you will want to omit values (and corresponding header columns) when outputting a CSV file. +You may use the following methods (from the same types as above): + + def skipRenderer[T](alignment: Int = 1)(implicit ca: CsvAttributes): CsvRenderer[T] + def skipGenerator[T](implicit ca: CsvAttributes): CsvGenerator[T] + +Note that, when rendering a CSV row, you may want to simply render some number of delimiters +(this would be in the case where you have a fixed header). +You can use the _alignment_ parameter of _skipRenderer_ to ensure alignment is correct. -There is currently only one implementation of String rendering, and that is Json rendering. +As usual, the standard types are pre-defined for both _CsvRenderer\[T]_ and _CsvGenerator\[T]_ (for Int, Double, etc.). + +The methods mentioned above render tables in the form of CSV Strings. +However, there are also methods available to render tables as a _File_: _writeCSVFile_ and _writeCSVFileRow_. +These utilize the type _CsvTableFileRenderer\[T]_ mentioned above. + +If you wish to output only a subset of rows, then you should use one of the methods defined in _Table_ such as _take_. + +## Other String Rendering + +Apart from CSV, there is currently only one implementation of _String_ rendering, and that is _Json_ rendering. Although Json is indeed a hierarchical serialization format, the manner of creating a Json string masks the hierarchical aspects. The implemented Json reader/writer is Spray Json but that could easily be changed in the future. Although this section is concerned with rendering, it is also true of course to say that tables can be read from Json strings. The following example from _JsonRendererSpec.scala_ shows how we can take the following steps -(for the definitions of Player, Partnership, please see the spec file itself): +(for the definitions of _Player_, _Partnership_, please see the spec file itself): * read a table of players from a list of Strings (there are, as shown above, other signatures of parse for files, URLs, etc.); * convert to a table of partnerships; * write the resulting table to a Json string; @@ -556,6 +608,15 @@ The following example from _JsonRendererSpec.scala_ shows how we can take the fo Release Notes ============= +V1.1.0 -> V1.1.1 +* Enable cryptographic capabilities + +V1.0.15 -> V1.1.0 +* Enable CSV-rendering and selection of table rows. + +V1.0.14 -> V1.0.15 +* Minor changes + V1.0.13 -> V1.0.14 * Enabled multi-line quoted strings: if a quoted string spans more than one line, this is acceptable. * Implemented analysis of raw-row tables. diff --git a/build.sbt b/build.sbt index 1f1a49cb..1a4e2d13 100755 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ organization := "com.phasmidsoftware" name := "TableParser" -version := "1.0.15" +version := "1.1.0" scalaVersion := "2.13.7" diff --git a/src/it/resources/airbnb2.csv b/src/it/resources/airbnb2.csv index ebdc25a3..71eafc98 100644 --- a/src/it/resources/airbnb2.csv +++ b/src/it/resources/airbnb2.csv @@ -990,11 +990,4 @@ Looking forward to meeting you!","within an hour","100%",NA,"1","https://a0.musc "","TAKE ADVANTAGE OF OUR LOWER RATES FOR JANUARY AND FEBRUARY FOR OUR LOVELY HOME WITH A SUPER HOST RATING!! FREE OFF STREET PARKING DURING THE WINTER MONTHS (to avoid dealing with digging out). :) Our lovely and spacious Victorian home is located on a quiet one way street between Harvard and Inman Squares. A fifteen minute walk will bring you to historic Harvard Square where the subway can transport you to downtown Boston in under 10 minutes.","Vibrant Inman Square is a 5 minute walk with a large selection of fabulous restaurants for all budgets. I'm a foodie so ask me for recommendations! Free off street parking is available. A perfect location for visiting the many colleges in the area including Harvard, MIT, BU, BC, Tufts, Emerson, and Lesley College. High Tech Kendall Square is also nearby for business travelers and conference attendees.","TAKE ADVANTAGE OF OUR LOWER RATES FOR JANUARY AND FEBRUARY FOR OUR LOVELY HOME WITH A SUPER HOST RATING!! FREE OFF STREET PARKING DURING THE WINTER MONTHS (to avoid dealing with digging out). :) Our lovely and spacious Victorian home is located on a quiet one way street between Harvard and Inman Squares. A fifteen minute walk will bring you to historic Harvard Square where the subway can transport you to downtown Boston in under 10 minutes. Vibrant Inman Square is a 5 minute walk with a large selection of fabulous restaurants for all budgets. I'm a foodie so ask me for recommendations! Free off street parking is available. A perfect location for visiting the many colleges in the area including Harvard, MIT, BU, BC, Tufts, Emerson, and Lesley College. High Tech Kendall Square is also nearby for business travelers and conference attendees. For families/groups of 3 or 4 people, we offer 2 ROOMS (the listing of 1 bedroom in the space above is an airbnb computer glitch) in their own ""wi","Proximity to Harvard (0.8 miles) and Inman Squares, great restaurants within walking distance, interesting architecture.","For stays over 1 week a $50 weekly cleaning fee applies and breakfast other than coffee and tea is not provided. I do not generally book more than 6 weeks out but if you have a special event you want to reserve for, send me a message.","The ""T"" is a 12-15 minute walk to get you to downtown Boston within 8 minutes. A bus stop is 2 blocks away. You can even park in our driveway!","For families/groups of 3 or 4 people, we offer 2 ROOMS (the listing of 1 bedroom in the space above is an airbnb computer glitch) in their own ""wing"" with their own private bathroom. The first bright and comfortable room has a full bed, TV, closet and bureau space with it's own AC and heat controls. The 2nd room has a super comfy single bed and a futon can be added to this room (or your own portable crib) for a 4th person. This room also has it's own AC and heat controls. This space is ideally suited for families or a couple and a friend/friends. It is not ideal for 3 or 4 single adults unless 2 of you are comfortable sharing a full size bed. For Families with children 10 years of age and under the $25/per night extra fee for over 2 people does not apply. Nor does it apply during certain peak times when a flat fee for both rooms are set. I will send you a special offer to adjust the rate to address those situations. You will have full shared use of our beautiful, well stocked custom k","We are working most days but when around would love to share our extensive knowledge of the Boston area to make your stay as enjoyable as possible. However we also are happy to give you as much space and privacy as you like. Look forward to meeting you!","No pets or smoking allowed. No red wine or other stainable foods consumed in the rooms please. We have year round directly below our guest rooms so please be considerate with noise volume especially after 11 pm. See the house manual once you are booked for more details.",1632003,2019,"January","https://www.airbnb.com/rooms/1632003","https://a0.muscache.com/im/pictures/5576faab-99e6-47bc-a160-c2f74e0d5c1a.jpg?aki_policy=large","HSE","PR",4,1,2,3,"RB","{TV,""Cable TV"",Internet,Wifi,""Air conditioning"",Kitchen,Breakfast,""Free street parking"",""Indoor fireplace"",Heating,""Family/kid friendly"",Washer,Dryer,""Smoke detector"",""Carbon monoxide detector"",""First aid kit"",""Fire extinguisher"",Essentials,Shampoo,""Lock on bedroom door"",Hangers,""Hair dryer"",Iron,""Laptop friendly workspace"",""translation missing: en.hosting_amenity_49"",""Self check-in"",Keypad,""Children’s books and toys"",""Hot water"",""Bed linens"",""Extra pillows and blankets"",""Ethernet connection"",Microwave,""Coffee maker"",Refrigerator,Dishwasher,""Dishes and silverware"",""Cooking basics"",Oven,Stove,""Luggage dropoff allowed"",""Long term stays allowed"",""Paid parking on premises""}",NA,"$170.00 ","$1,200.00 ","$4,000.00 ","$300.00 ","$50.00 ",3,"$10.00 ",2,45,"today",27,57,80,164,"1/22/19",0,"",0,"MOD",0,1,0,8530114,"https://www.airbnb.com/users/show/8530114","Colleen","8/30/13","Cambridge, Massachusetts, United States","I am a pediatric nurse practitioner married to a cabinetmaker who loves to travel (especially to Italy)! I love meeting new people from around the world and helping them to fully experience my wonderful home town of Cambridge/Boston. I am a foodie and can steer you to fabulous restaurants (many of them walking distance from my home). My kids are grown so I now have 2 comfortable, peaceful rooms available to guests. Looking forward to meeting you!","within an hour","100%",NA,"1","https://a0.muscache.com/im/users/8530114/profile_pic/1378248670/original.jpg?aki_policy=profile_small","https://a0.muscache.com/im/users/8530114/profile_pic/1378248670/original.jpg?aki_policy=profile_x_medium",3,"['email', 'phone', 'reviews', 'kba']","1","1",3,1,2,0,33,7,0.51,"10/8/13","11/13/18",99,10,10,10,10,10,10,42.37209473,-71.10327834,"Cambridge, MA, United States",1,"Cambridge",2.50174e+14,25017352900,"Cambridge","Middlesex County" -,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"#17-3 Cozy Apartment in Harvard Sq!","This apartment is on the 3rd floor of a Victorian house on a quiet street in Cambridge. Street permit parking may be reserved upon request; however, permits are limited in number.","This is a two bedroom apartment right in Harvard Sq (**limited parking**— please see details below)! One bedroom has a queen bed, dresser, a fold out couch, desk, TV and closet. The other bedroom has a queen bed, dresser, desk, reading chair with foot stool, closet. There is a full bathroom and fully equipped kitchen for cooking. Harvard Square is a great neighborhood- lots of cafes, shops, restaurant and night life. Luckily my house is just outside of Harvard Square on a quiet street in Cambridge. The apartment is very close to the Charles River, which is a great place to jog or bike ride. Cambridge is a wonderful place to visit, I'm sure you'll have an amazing time!","This apartment is on the 3rd floor of a Victorian house on a quiet street in Cambridge. Street permit parking may be reserved upon request; however, permits are limited in number. This is a two bedroom apartment right in Harvard Sq (**limited parking**— please see details below)! One bedroom has a queen bed, dresser, a fold out couch, desk, TV and closet. The other bedroom has a queen bed, dresser, desk, reading chair with foot stool, closet. There is a full bathroom and fully equipped kitchen for cooking. Harvard Square is a great neighborhood- lots of cafes, shops, restaurant and night life. Luckily my house is just outside of Harvard Square on a quiet street in Cambridge. The apartment is very close to the Charles River, which is a great place to jog or bike ride. Cambridge is a wonderful place to visit, I'm sure you'll have an amazing time! The whole apartment, deck and back yard. I am available if you need me, but I also like to give guests their space. **Parking: Please note tha","","All utilities included in both short-term & long-term stays.","**Parking: Please note that parking in this area is VERY LIMITED, unfortunately! RESIDENT/GUEST PARKING: We only have two visitor parking pass for all 6 apartments, available at a first-ask, first-receive basis. If you want a pass you **must** confirm that one is available for you (you may park for up to 3 days at a time; the zone in which you can park is on the back of the pass which you should put on your dashboard, face up). METERS: There is *2 hour* metered parking at the end of the street (Mt. Auburn St.), and several garages in the Harvard Square area if you have a car (for example, the (Charles Square Garage at One Bennett Street, Cambridge, MA (Phone number hidden by Airbnb) ,  (Email hidden by Airbnb) , (Website hidden by Airbnb) PUBLIC TRANSPORTATION: We strongly urge you to use the Boston Area's public transportation (the subway is called the ""T"" :-) ) and the Harvard Square Station is a 7 minute walk away (address: (Phone number hidden by Airbnb) Chester Square, Cambridge, ","The whole apartment, deck and back yard. ","I am available if you need me, but I also like to give guests their space. ","Check-in = no earlier than 3pm* Check-Out = no later than noon (& we ask that you leave your used bedding and towels outside your door by 11am).* *unless you arrange with us otherwise Food Storage: Please make sure to not leave any food out (either put it in the refrigerator or make sure it is in an air tight container or some sort) we do not want to attract mice or other pests! Cleanliness and Tidiness: Please leave everything clean, organized and tidy in the common areas and when you check out. Quiet hours: Starting at 11pm please do not have any loud music, TV playing, etc.",1644730,2019,"January","https://www.airbnb.com/rooms/1644730","https://a0.muscache.com/im/pictures/23661727/b3f327d2_original.jpg?aki_policy=large","APT","HA",4,1,2,2,"RB","{TV,Internet,Wifi,""Air conditioning"",Kitchen,Heating,Washer,Dryer,""24-hour check-in"",""translation missing: en.hosting_amenity_49"",""translation missing: en.hosting_amenity_50""}",NA,"$240.00 ","$1,440.00 ","$4,000.00 ","$400.00 ","$85.00 ",4,"$25.00 ",3,1125,"a week ago",11,24,43,265,"1/22/19",0,"",0,"STR_G",0,0,0,1461240,"https://www.airbnb.com/users/show/1461240","Monica","12/1/11","Cambridge, Massachusetts, United States","Welcome to The Cambridge Karma House! - -¡Bienvenidos a La Casa de Karma! - -We wanted a name that embodies our feelings and philosophy around hosting, and essentially it is about karma- the notion that our actions and intentions will come back to us and influence our future, whether they be positive or negative. Karma is similar to the golden rule of “treat others the way you want to be treated."" We believe that this is a good way to go through the world and attempt to always practice this with our guests. We will try to always treat you with loving kindness and respect, and we expect the same from our guests--that they treat the folks running the house, other guests in the house, and the space itself, with respect and kindness. - -This Victorian house built in the 1880s and still has many of the quirks from that era. Around 30 years ago our family bought the property, and in 2013 we started managing and renovating its 6 apartments and established the Cambridge Karma House LLC! diff --git a/src/it/resources/com/phasmidsoftware/examples/TeamProject.csv b/src/it/resources/com/phasmidsoftware/examples/TeamProject.csv new file mode 100644 index 00000000..80811824 --- /dev/null +++ b/src/it/resources/com/phasmidsoftware/examples/TeamProject.csv @@ -0,0 +1,7 @@ +Team Number,Team Member 1,Team Member 2,Team Member 3,Team Member 4,Total Score,On Time,Scope Scale,Planning Presentation,Presentation,Idea,Use Cases,Acceptance Criteria,Team Execution,Code,Unit Tests,Repo,Remarks,Repository +,,,,,100,11,10,6,12,5,4,8,5,23,11,5,, +1,Leonhard Euler,Daniel Bernoulli,Isaac Newton,Srinivas Ramanujan,92,8.5,8,5,10.5,5,4,8,5,23,10,5,Presentation long and detailed. Project excellent overall. Need to actually run UI myself.,https://github.com/youngbai/CSYE7200-MovieRecommendation,,, +2,Ringo Starr,George Harrison,Paul McCartney,,83.5,8.5,10,5,9,5,4,6,3,22,7,4,Presentation quite confused. Part of project not achieved. ,https://github.com/PhoenixMay/MovieRecommendation,,, +3,Pablo Picasso,Joan Miro,,,92,11,10,5,10,5,4,7,4,23,9,4,Presentation OK. What did they do about these challenges.,https://github.com/jianghanw/CSYE7200_Team_Project_Team3,,, +4,Ludwig van Beethoven,Wolfgang Amadeus Mozart,,,88.5,11,10,6,10.5,5,3.5,6,4,21.5,7,4,A/C not clearly defined (planning pres?),https://github.com/Essexwwz/heart-health-indicator,,, +5,Winston Churchill,Clement Attlee,Harold McMillan,,88.5,11,9.5,5,11,5,4,7,2,22,7,5,Presentation long and detailed. Project excellent overall. A/C not shown clearly.,https://github.com/CSYE7200-21FALL-TEAM6,,, \ No newline at end of file diff --git a/src/it/scala/com/phasmidsoftware/examples/ProjectsFuncSpec.scala b/src/it/scala/com/phasmidsoftware/examples/ProjectsFuncSpec.scala new file mode 100644 index 00000000..134b8382 --- /dev/null +++ b/src/it/scala/com/phasmidsoftware/examples/ProjectsFuncSpec.scala @@ -0,0 +1,195 @@ +package com.phasmidsoftware.examples + +import com.phasmidsoftware.parse.TableParser +import com.phasmidsoftware.render.{CsvGenerators, CsvRenderer, CsvRenderers} +import com.phasmidsoftware.table.Table.parse +import com.phasmidsoftware.table.{CsvGenerator, HeadedTable, Row, Table} +import com.phasmidsoftware.util.TryUsing +import java.io.File +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import scala.io.Source +import scala.util._ + +class ProjectsFuncSpec extends AnyFlatSpec with Matchers { + + behavior of "TeamProject table" + + /** + * NOTE: it is perfectly proper for there to be a number of parsing problems. + * These are application-specific and are not indicative of any bugs in the + * TableParser library itself. + */ + it should "be ingested properly" in { + import TeamProjectParser._ + + val filename = "TeamProject.csv" + val mty: Try[Table[TeamProject]] = Table.parseResource(filename, classOf[ProjectsFuncSpec]) + mty should matchPattern { case Success(HeadedTable(_, _)) => } + for (mt <- mty) { + println(s"TeamProject: successfully read ${mt.size} rows") + mt.size shouldBe 5 + mt foreach println + } + } + + it should "be ingested and written out to file using the given header" in { + import TeamProjectParser._ + + implicit val parser: TableParser[Table[TeamProject]] = implicitly[TableParser[Table[TeamProject]]] + val mty: Try[Table[TeamProject]] = TryUsing(Source.fromURL(classOf[TeamProject].getResource("TeamProject.csv")))(parse(_)) + mty should matchPattern { case Success(HeadedTable(_, _)) => } + for (mt <- mty) { + import CsvGenerators._ + implicit val csvGenerator: CsvGenerator[TeamProject] = mt.maybeHeader match { + case Some(h) => Row.csvGenerator(h) + case None => createCsvGeneratorFromTeamProject(_.generator12(Grade)) + } + import CsvRenderers._ + implicit val csvRenderer: CsvRenderer[TeamProject] = createCsvRendererForTeamProject(_.renderer12(Grade)) + mt.writeCSVFile(new File("TeamProjectOutput.csv")) + } + } + + it should "be ingested and written out properly using the given header" in { + import TeamProjectParser._ + + implicit val parser: TableParser[Table[TeamProject]] = implicitly[TableParser[Table[TeamProject]]] + val mty: Try[Table[TeamProject]] = TryUsing(Source.fromURL(classOf[TeamProject].getResource("TeamProject.csv")))(parse(_)) + mty should matchPattern { case Success(HeadedTable(_, _)) => } + for (mt <- mty) { + import CsvGenerators._ + implicit val csvGenerator: CsvGenerator[TeamProject] = mt.maybeHeader match { + case Some(h) => Row.csvGenerator(h) + case None => createCsvGeneratorFromTeamProject(_.generator12(Grade)) + } + import CsvRenderers._ + implicit val csvRenderer: CsvRenderer[TeamProject] = createCsvRendererForTeamProject(_.renderer12(Grade)) + mt.toCSV foreach println + } + } + + it should "be ingested and written out properly for team 1 using the given header" in { + import TeamProjectParser._ + + implicit val parser: TableParser[Table[TeamProject]] = implicitly[TableParser[Table[TeamProject]]] + val mty: Try[Table[TeamProject]] = TryUsing(Source.fromURL(classOf[TeamProject].getResource("TeamProject.csv")))(parse(_)) + mty should matchPattern { case Success(HeadedTable(_, _)) => } + for (mt <- mty) { + import CsvGenerators._ + implicit val csvGenerator: CsvGenerator[TeamProject] = mt.maybeHeader match { + case Some(h) => Row.csvGenerator(h) + case None => createCsvGeneratorFromTeamProject(_.generator12(Grade)) + } + import CsvRenderers._ + implicit val csvRenderer: CsvRenderer[TeamProject] = createCsvRendererForTeamProject(_.renderer12(Grade)) + mt.take(1).toCSV shouldBe + """Team Number,Team Member 1,Team Member 2,Team Member 3,Team Member 4,Total Score,On Time,Scope Scale,Planning Presentation,Presentation,Idea,Use Cases,Acceptance Criteria,Team Execution,Code,Unit Tests,Repo,Remarks,Repository + |,,,,,100,11,10,6,12,5,4,8,5,23,11,5,, + |1,Leonhard Euler,Daniel Bernoulli,Isaac Newton,Srinivas Ramanujan,92.0,8.5,8.0,5.0,10.5,5.0,4.0,8.0,5.0,23.0,10.0,5.0,Presentation long and detailed. Project excellent overall. Need to actually run UI myself.,https://github.com/youngbai/CSYE7200-MovieRecommendation + |""".stripMargin + } + } + + it should "be ingested and written out properly for team 1 using the given header but skipping grades" in { + import TeamProjectParser._ + + implicit val parser: TableParser[Table[TeamProject]] = implicitly[TableParser[Table[TeamProject]]] + val mty: Try[Table[TeamProject]] = TryUsing(Source.fromURL(classOf[TeamProject].getResource("TeamProject.csv")))(parse(_)) + mty should matchPattern { case Success(HeadedTable(_, _)) => } + for (mt <- mty) { + import CsvGenerators._ + implicit val csvGenerator: CsvGenerator[TeamProject] = mt.maybeHeader match { + case Some(h) => Row.csvGenerator(h) + case None => createCsvGeneratorFromTeamProject(_.generator12(Grade)) + } + implicit val csvRenderer: CsvRenderer[TeamProject] = createCsvRendererForTeamProject(_.skipRenderer(12)) + mt.take(1).toCSV shouldBe + """Team Number,Team Member 1,Team Member 2,Team Member 3,Team Member 4,Total Score,On Time,Scope Scale,Planning Presentation,Presentation,Idea,Use Cases,Acceptance Criteria,Team Execution,Code,Unit Tests,Repo,Remarks,Repository + |,,,,,100,11,10,6,12,5,4,8,5,23,11,5,, + |1,Leonhard Euler,Daniel Bernoulli,Isaac Newton,Srinivas Ramanujan,,,,,,,,,,,,,Presentation long and detailed. Project excellent overall. Need to actually run UI myself.,https://github.com/youngbai/CSYE7200-MovieRecommendation + |""".stripMargin + } + } + + it should "be ingested and written out properly using fabricated header" in { + import TeamProjectParser._ + + implicit val parser: TableParser[Table[TeamProject]] = implicitly[TableParser[Table[TeamProject]]] + val mty: Try[Table[TeamProject]] = TryUsing(Source.fromURL(classOf[TeamProject].getResource("TeamProject.csv")))(parse(_)) + mty should matchPattern { case Success(HeadedTable(_, _)) => } + for (mt <- mty) { + import CsvGenerators._ + implicit val csvGenerator: CsvGenerator[TeamProject] = mt.maybeHeader match { + case _ => createCsvGeneratorFromTeamProject(_.generator12(Grade)) + } + import CsvRenderers._ + implicit val csvRenderer: CsvRenderer[TeamProject] = createCsvRendererForTeamProject(_.renderer12(Grade)) + mt.toCSV shouldBe + """team.number,team.member_1,team.member_2,team.member_3,team.member_4,grade.totalScore,grade.onTime,grade.scopeScale,grade.planningPresentation,grade.presentation,grade.idea,grade.useCases,grade.acceptanceCriteria,grade.teamExecution,grade.code,grade.unitTests,grade.repo,remarks,repository + |1,Leonhard Euler,Daniel Bernoulli,Isaac Newton,Srinivas Ramanujan,92.0,8.5,8.0,5.0,10.5,5.0,4.0,8.0,5.0,23.0,10.0,5.0,Presentation long and detailed. Project excellent overall. Need to actually run UI myself.,https://github.com/youngbai/CSYE7200-MovieRecommendation + |2,Ringo Starr,George Harrison,Paul McCartney,,83.5,8.5,10.0,5.0,9.0,5.0,4.0,6.0,3.0,22.0,7.0,4.0,Presentation quite confused. Part of project not achieved. ,https://github.com/PhoenixMay/MovieRecommendation + |3,Pablo Picasso,Joan Miro,,,92.0,11.0,10.0,5.0,10.0,5.0,4.0,7.0,4.0,23.0,9.0,4.0,Presentation OK. What did they do about these challenges.,https://github.com/jianghanw/CSYE7200_Team_Project_Team3 + |4,Ludwig van Beethoven,Wolfgang Amadeus Mozart,,,88.5,11.0,10.0,6.0,10.5,5.0,3.5,6.0,4.0,21.5,7.0,4.0,A/C not clearly defined (planning pres?),https://github.com/Essexwwz/heart-health-indicator + |5,Winston Churchill,Clement Attlee,Harold McMillan,,88.5,11.0,9.5,5.0,11.0,5.0,4.0,7.0,2.0,22.0,7.0,5.0,Presentation long and detailed. Project excellent overall. A/C not shown clearly.,https://github.com/CSYE7200-21FALL-TEAM6 + |""".stripMargin + } + } + + it should "be ingested and written out properly for team 1 using fabricated header" in { + import TeamProjectParser._ + + implicit val parser: TableParser[Table[TeamProject]] = implicitly[TableParser[Table[TeamProject]]] + val mty: Try[Table[TeamProject]] = TryUsing(Source.fromURL(classOf[TeamProject].getResource("TeamProject.csv")))(parse(_)) + mty should matchPattern { case Success(HeadedTable(_, _)) => } + for (mt <- mty) { + import CsvGenerators._ + implicit val csvGenerator: CsvGenerator[TeamProject] = mt.maybeHeader match { + case _ => createCsvGeneratorFromTeamProject(_.generator12(Grade)) + } + import CsvRenderers._ + implicit val csvRenderer: CsvRenderer[TeamProject] = createCsvRendererForTeamProject(_.renderer12(Grade)) + mt.take(1).toCSV shouldBe + """team.number,team.member_1,team.member_2,team.member_3,team.member_4,grade.totalScore,grade.onTime,grade.scopeScale,grade.planningPresentation,grade.presentation,grade.idea,grade.useCases,grade.acceptanceCriteria,grade.teamExecution,grade.code,grade.unitTests,grade.repo,remarks,repository + |1,Leonhard Euler,Daniel Bernoulli,Isaac Newton,Srinivas Ramanujan,92.0,8.5,8.0,5.0,10.5,5.0,4.0,8.0,5.0,23.0,10.0,5.0,Presentation long and detailed. Project excellent overall. Need to actually run UI myself.,https://github.com/youngbai/CSYE7200-MovieRecommendation + |""".stripMargin + } + } + + it should "be ingested and written out properly for team 1 using fabricated header but skipping grades" in { + import TeamProjectParser._ + + implicit val parser: TableParser[Table[TeamProject]] = implicitly[TableParser[Table[TeamProject]]] + val mty: Try[Table[TeamProject]] = TryUsing(Source.fromURL(classOf[TeamProject].getResource("TeamProject.csv")))(parse(_)) + mty should matchPattern { case Success(HeadedTable(_, _)) => } + for (mt <- mty) { + implicit val csvGenerator: CsvGenerator[TeamProject] = mt.maybeHeader match { + case _ => createCsvGeneratorFromTeamProject(_.skipGenerator) + } + implicit val csvRenderer: CsvRenderer[TeamProject] = createCsvRendererForTeamProject(_.skipRenderer()) + mt.take(1).toCSV shouldBe + """team.number,team.member_1,team.member_2,team.member_3,team.member_4,,remarks,repository + |1,Leonhard Euler,Daniel Bernoulli,Isaac Newton,Srinivas Ramanujan,,Presentation long and detailed. Project excellent overall. Need to actually run UI myself.,https://github.com/youngbai/CSYE7200-MovieRecommendation + |""".stripMargin + } + } + + private def createCsvGeneratorFromTeamProject(function: CsvGenerators => CsvGenerator[Grade]): CsvGenerator[TeamProject] = { + val csvGenerators = new CsvGenerators {} + import CsvGenerators._ + implicit val optionStringGenerator: CsvGenerator[Option[String]] = csvGenerators.optionGenerator[String] + implicit val teamGenerator: CsvGenerator[Team] = csvGenerators.generator5(Team) + implicit val gradeGenerator: CsvGenerator[Grade] = function(csvGenerators) + csvGenerators.generator4(TeamProject) + } + + private def createCsvRendererForTeamProject(function: CsvRenderers => CsvRenderer[Grade]): CsvRenderer[TeamProject] = { + val csvRenderers = new CsvRenderers {} + import CsvRenderers._ + implicit val optionStringGenerator: CsvRenderer[Option[String]] = csvRenderers.optionRenderer[String] + implicit val teamGenerator: CsvRenderer[Team] = csvRenderers.renderer5(Team) + implicit val gradeGenerator: CsvRenderer[Grade] = function(csvRenderers) + csvRenderers.renderer4(TeamProject) + } + +} diff --git a/src/it/scala/com/phasmidsoftware/examples/TeamProject.scala b/src/it/scala/com/phasmidsoftware/examples/TeamProject.scala new file mode 100644 index 00000000..5bc927f1 --- /dev/null +++ b/src/it/scala/com/phasmidsoftware/examples/TeamProject.scala @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019. Phasmid Software + */ + +package com.phasmidsoftware.examples + +import com.phasmidsoftware.parse._ +import com.phasmidsoftware.table.{HeadedTable, Header, Table} +import java.net.URL + +/** + * This class represents a TeamProject from the IMDB data file on Kaggle. + * Although the limitation on 22 fields in a case class has partially gone away, it's still convenient to group the different attributes together into logical classes. + * + * Created by scalaprof on 9/12/16. + * + * Common questions in this assignment: + * 1. Where is main method? + * In most case, you don't need to run main method for assignments. + * Unit tests are provided to test your implementation. + * In this assignment, you will find the `object TeamProject extends App`, + * the `App` trait can be used to quickly turn objects into executable programs. + * You can read the official doc of Scala for more details. + * + * 2. How to understand the whole program in this assignment? + * I won't suggest you to understand the whole program in this assignment, + * there are some advanced features like `implicit` which hasn't been covered in class. + * You should be able to understand it before midterm. + * I will suggest you only focus on each TO BE IMPLEMENTED in the assignments. + * + */ +case class TeamProject(team: Team, grade: Grade, remarks: String, repository: URL) + +case class Team(number: Int, member_1: String, member_2: Option[String], member_3: Option[String], member_4: Option[String]) + +case class Grade(totalScore: Double, onTime: Double, scopeScale: Double, planningPresentation: Double, presentation: Double, idea: Double, useCases: Double, acceptanceCriteria: Double, teamExecution: Double, code: Double, unitTests: Double, repo: Double) + +object TeamProjectParser extends CellParsers { + + def camelToSnakeCaseColumnNameMapper(w: String): String = w.replaceAll("([A-Z0-9])", "_$1") + + def identifierToCapitalizedWordColumnNameMapper(w: String): String = w.replaceAll("([A-Z0-9])", " $1").replaceAll("_", "") + + implicit val teamProjectColumnHelper: ColumnHelper[TeamProject] = columnHelper(camelToSnakeCaseColumnNameMapper _) + implicit val gradeColumnHelper: ColumnHelper[Grade] = columnHelper(identifierToCapitalizedWordColumnNameMapper _) + implicit val teamColumnHelper: ColumnHelper[Team] = columnHelper(identifierToCapitalizedWordColumnNameMapper _, Some("$x $c")) + implicit val optionalStringParser: CellParser[Option[String]] = cellParserOption + implicit val teamParser: CellParser[Team] = cellParser5(Team) + implicit val gradeParser: CellParser[Grade] = cellParser12(Grade) + implicit val attributesParser: CellParser[AttributeSet] = cellParser(AttributeSet.apply: String => AttributeSet) + implicit val teamProjectParser: CellParser[TeamProject] = cellParser4(TeamProject) + + implicit object TeamProjectConfig extends DefaultRowConfig { + override val listEnclosure: String = "" + } + + implicit val parser: StandardRowParser[TeamProject] = StandardRowParser[TeamProject] + + trait TeamProjectTableParser extends StringTableParser[Table[TeamProject]] { + type Row = TeamProject + + val maybeFixedHeader: Option[Header] = None + + val headerRowsToRead: Int = 2 + + override val forgiving: Boolean = true + + val rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] + + protected def builder(rows: Iterable[TeamProject], header: Header): Table[Row] = HeadedTable(rows, header) + } + + implicit object TeamProjectTableParser extends TeamProjectTableParser +} diff --git a/src/it/scala/com/phasmidsoftware/table/AirBNBFuncSpec.scala b/src/it/scala/com/phasmidsoftware/table/AirBNBFuncSpec.scala index 22ec0e8c..16ff894a 100644 --- a/src/it/scala/com/phasmidsoftware/table/AirBNBFuncSpec.scala +++ b/src/it/scala/com/phasmidsoftware/table/AirBNBFuncSpec.scala @@ -4,7 +4,6 @@ import com.phasmidsoftware.parse.TableParser.sampler import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import org.scalatest.tagobjects.Slow - import scala.util._ class AirBNBFuncSpec extends AnyFlatSpec with Matchers { @@ -12,11 +11,11 @@ class AirBNBFuncSpec extends AnyFlatSpec with Matchers { behavior of "AirBNB table" /** - * This test must be ignored when pushing to github. - * This is because: - * (1) it takes a long time so we generally ignore it; - * (2) but, more importantly, we do not store the source file in git! - */ + * This test must be ignored when pushing to github. + * This is because: + * (1) it takes a long time so we generally ignore it; + * (2) but, more importantly, we do not store the source file in git! + */ ignore should "be ingested properly" taggedAs Slow in { val airBNBFile = "/AIRBNB.Listing.csv" val linesInFile = 104999 diff --git a/src/it/scala/com/phasmidsoftware/table/MovieFuncSpec.scala b/src/it/scala/com/phasmidsoftware/table/MovieFuncSpec.scala index 0cd3aad9..56a1c4fc 100644 --- a/src/it/scala/com/phasmidsoftware/table/MovieFuncSpec.scala +++ b/src/it/scala/com/phasmidsoftware/table/MovieFuncSpec.scala @@ -1,7 +1,11 @@ package com.phasmidsoftware.table +import com.phasmidsoftware.parse.TableParser +import com.phasmidsoftware.table.Table.parse +import com.phasmidsoftware.util.TryUsing import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers +import scala.io.Source import scala.util._ class MovieFuncSpec extends AnyFlatSpec with Matchers { @@ -9,10 +13,10 @@ class MovieFuncSpec extends AnyFlatSpec with Matchers { behavior of "Movie table" /** - * NOTE: it is perfectly proper for there to be a number of parsing problems. - * These are application-specific and are not indicative of any bugs in the - * TableParser library itself. - */ + * NOTE: it is perfectly proper for there to be a number of parsing problems. + * These are application-specific and are not indicative of any bugs in the + * TableParser library itself. + */ it should "be ingested properly" in { import MovieParser._ @@ -25,4 +29,22 @@ class MovieFuncSpec extends AnyFlatSpec with Matchers { } } + /** + * NOTE: it is perfectly proper for there to be a number of parsing problems. + * These are application-specific and are not indicative of any bugs in the + * TableParser library itself. + */ + it should "be ingested and written out properly" in { + import MovieParser._ + + implicit val parser: TableParser[Table[Movie]] = implicitly[TableParser[Table[Movie]]] + val mty: Try[Table[Movie]] = TryUsing(Source.fromURL(classOf[Movie].getResource("movie_metadata.csv")))(parse(_)) + mty should matchPattern { case Success(HeadedTable(_, _)) => } + for (mt <- mty) { + // TODO implement this part +// mt.generateCsvHeader +// mt.toCSV + } + } + } diff --git a/src/main/scala/com/phasmidsoftware/parse/CellParser.scala b/src/main/scala/com/phasmidsoftware/parse/CellParser.scala index 252a5cb3..435ec5fc 100644 --- a/src/main/scala/com/phasmidsoftware/parse/CellParser.scala +++ b/src/main/scala/com/phasmidsoftware/parse/CellParser.scala @@ -4,39 +4,37 @@ package com.phasmidsoftware.parse +import com.phasmidsoftware.table.{Header, Row} import java.io.File import java.net.URL - -import com.phasmidsoftware.table.{Header, Row} import org.joda.time.LocalDate - import scala.annotation.implicitNotFound import scala.util.{Failure, Success, Try} /** - * Type class trait CellParser[T]. - * This trait has methods to parse and to convert from String to T. - * - * TODO Need to define this better so that we don't have any non-implemented methods. - * - * @tparam T the type of the resulting object. - */ + * Type class trait CellParser[T]. + * This trait has methods to parse and to convert from String to T. + * + * TODO Need to define this better so that we don't have any non-implemented methods. + * + * @tparam T the type of the resulting object. + */ @implicitNotFound(msg = "Cannot find an implicit instance of CellParser[${T}]. Typically, you should invoke a suitable method from CellParsers.") trait CellParser[+T] { /** - * Convert a String into a T. - * - * @param w the String to be converted. - * @return a new instance of T wrapped in Try - */ + * Convert a String into a T. + * + * @param w the String to be converted. + * @return a new instance of T wrapped in Try + */ def convertString(w: String): Try[T] /** - * Parse a Convertible value into a T. - * - * @param value the Convertible value. - * @return a new instance of T wrapped in Try. - */ + * Parse a Convertible value into a T. + * + * @param value the Convertible value. + * @return a new instance of T wrapped in Try. + */ def parse(value: Convertible): Try[T] = value match { case CellValue(w) => convertString(w) case RowValues(row, columns) => parse(None, row, columns) @@ -44,62 +42,65 @@ trait CellParser[+T] { } /** - * Method to parse an Option[String] to a T. - * - * @param wo an optional String. - * @param row a Row of values. - * @param columns the column names. - * CONSIDER do we actually need the Header parameter here? - * @return a new instance of T. - */ + * Method to parse an Option[String] to a T. + * + * @param wo an optional String. + * @param row a Row of values. + * @param columns the column names. + * CONSIDER do we actually need the Header parameter here? + * @return a new instance of T. + */ def parse(wo: Option[String], row: Row, columns: Header): Try[T] } /** - * Trait SingleCellParser, which extends CellParser[T]. - * - * @tparam T the type of the resulting object. - */ + * Trait SingleCellParser, which extends CellParser[T]. + * + * @tparam T the type of the resulting object. + */ trait SingleCellParser[T] extends CellParser[T] { def parse(w: Option[String], row: Row, columns: Header): Try[T] = Failure(new UnsupportedOperationException) + // NOTE not used override def toString: String = "SingleCellParser" } /** - * CONSIDER renaming this to something like RowParser, or RawRowParser. - * - * @tparam T the type of the result. - */ + * CONSIDER renaming this to something like RowParser, or RawRowParser. + * + * @tparam T the type of the result. + */ trait MultiCellParser[T] extends CellParser[T] { + // NOTE not used //noinspection NotImplementedCode def convertString(w: String): Try[T] = Failure(new UnsupportedOperationException) + // NOTE not used override def toString: String = "MultiCellParser" } /** - * Abstract class Convertible. - * - * CONSIDER making this a trait since it takes no value parameters. - */ + * Abstract class Convertible. + * + * CONSIDER making this a trait since it takes no value parameters. + */ sealed abstract class Convertible { def convertTo[T: CellParser]: Try[T] = cellReader.parse(this) } /** - * A concrete Convertible corresponding to a cell value. - * - * @param w the String contained by a cell. - */ + * A concrete Convertible corresponding to a cell value. + * + * @param w the String contained by a cell. + */ case class CellValue(w: String) extends Convertible /** - * A concrete Convertible corresponding to a row. - * - * @param row the Row containing several values. - * @param header the Header. - */ + * A concrete Convertible corresponding to a row. + * + * @param row the Row containing several values. + * @param header the Header. + */ case class RowValues(row: Row, header: Header) extends Convertible object RowValues { @@ -169,8 +170,8 @@ object CellParser { } /** - * This parser succeeds on empty strings. - */ + * This parser succeeds on empty strings. + */ implicit object StringCellParser$ extends SingleCellParser[String] { def convertString(w: String): Try[String] = Success(w) diff --git a/src/main/scala/com/phasmidsoftware/parse/CellParsers.scala b/src/main/scala/com/phasmidsoftware/parse/CellParsers.scala index 4a37a272..719181ab 100644 --- a/src/main/scala/com/phasmidsoftware/parse/CellParsers.scala +++ b/src/main/scala/com/phasmidsoftware/parse/CellParsers.scala @@ -6,26 +6,25 @@ package com.phasmidsoftware.parse import com.phasmidsoftware.table._ import com.phasmidsoftware.util.{FP, Reflection} - import scala.reflect.ClassTag import scala.util.control.NonFatal import scala.util.{Failure, Success, Try} /** - * Trait to define the various parsers for reading case classes and their parameters from table rows. - * - * NOTE: In each of these cellParser methods, the CellParser has a parse method which ignores the columns. - * - */ + * Trait to define the various parsers for reading case classes and their parameters from table rows. + * + * NOTE: In each of these cellParser methods, the CellParser has a parse method which ignores the columns. + * + */ trait CellParsers { /** - * Method to return a CellParser[Seq[P] from a potentially unlimited set of P objects. - * The counting of the elements starts at start (defaults to 1). - * - * @tparam P the underlying type of the result - * @return a MultiCellParser[Seq[P] - */ + * Method to return a CellParser[Seq[P] from a potentially unlimited set of P objects. + * The counting of the elements starts at start (defaults to 1). + * + * @tparam P the underlying type of the result + * @return a MultiCellParser[Seq[P] + */ def cellParserRepetition[P: CellParser : ColumnHelper](start: Int = 1): CellParser[Seq[P]] = new MultiCellParser[Seq[P]] { override def toString: String = "MultiCellParser: cellParserRepetition" @@ -37,13 +36,13 @@ trait CellParsers { } /** - * Method to return a CellParser[Seq[P]. - * This is used only by unit tests. - * CONSIDER eliminating this; only used in unit tests - * - * @tparam P the underlying type of the result - * @return a MultiCellParser[Seq[P] - */ + * Method to return a CellParser[Seq[P]. + * This is used only by unit tests. + * CONSIDER eliminating this; only used in unit tests + * + * @tparam P the underlying type of the result + * @return a MultiCellParser[Seq[P] + */ def cellParserSeq[P: CellParser]: CellParser[Seq[P]] = new MultiCellParser[Seq[P]] { override def toString: String = "MultiCellParser: cellParserSeq" @@ -51,13 +50,13 @@ trait CellParsers { } /** - * Method to return a CellParser[Option[P]. - * - * This class is used for optional types which are non-scalar, i.e. there are no implicitly-defined parsers for the Option[P]. - * - * @tparam P the underlying type of the result - * @return a SingleCellParser[Option[P] - */ + * Method to return a CellParser[Option[P]. + * + * This class is used for optional types which are non-scalar, i.e. there are no implicitly-defined parsers for the Option[P]. + * + * @tparam P the underlying type of the result + * @return a SingleCellParser[Option[P] + */ def cellParserOption[P: CellParser]: CellParser[Option[P]] = new SingleCellParser[Option[P]] { override def toString: String = s"cellParserOption with $cp" @@ -73,14 +72,14 @@ trait CellParsers { } /** - * Method to return a CellParser[Option[String]. - * - * TODO: why do we need this in addition to cellParserOption? - * - * CONSIDER: using BlankException as above... - * - * @return a SingleCellParser[Option[String] - */ + * Method to return a CellParser[Option[String]. + * + * TODO: why do we need this in addition to cellParserOption? + * + * CONSIDER: using BlankException as above... + * + * @return a SingleCellParser[Option[String] + */ lazy val cellParserOptionNonEmptyString: CellParser[Option[String]] = new SingleCellParser[Option[String]] { override def toString: String = s"cellParserOptionNonEmptyString" @@ -96,13 +95,13 @@ trait CellParsers { } /** - * Method to return a CellParser[T] based on a function to convert a P into a T - * - * @param construct a function P => T. - * @tparam P the type of the intermediate type. - * @tparam T the underlying type of the result. - * @return a SingleCellParser which converts a String into the intermediate type P and thence into a T - */ + * Method to return a CellParser[T] based on a function to convert a P into a T + * + * @param construct a function P => T. + * @tparam P the type of the intermediate type. + * @tparam T the underlying type of the result. + * @return a SingleCellParser which converts a String into the intermediate type P and thence into a T + */ def cellParser[P: CellParser, T: ClassTag](construct: P => T): CellParser[T] = new SingleCellParser[T] { override def toString: String = s"SingleCellParser for ${implicitly[ClassTag[T]]}" @@ -110,17 +109,17 @@ trait CellParsers { } /** - * Method to return a CellParser[T] where T is a 1-ary Product and which is based on a function to convert a P into a T. - * - * NOTE: be careful using this method it only applies where T is a 1-tuple (e.g. a case class with one field). - * It probably shouldn't ever be used in practice. It can cause strange initialization errors! - * This note may be irrelevant now that we have overridden convertString to fix issue #1. - * - * @param construct a function P => T, usually the apply method of a case class. - * @tparam P1 the type of the (single) field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a MultiCellParser which converts a String from a Row into the field type P and thence into a T - */ + * Method to return a CellParser[T] where T is a 1-ary Product and which is based on a function to convert a P into a T. + * + * NOTE: be careful using this method it only applies where T is a 1-tuple (e.g. a case class with one field). + * It probably shouldn't ever be used in practice. It can cause strange initialization errors! + * This note may be irrelevant now that we have overridden convertString to fix issue #1. + * + * @param construct a function P => T, usually the apply method of a case class. + * @tparam P1 the type of the (single) field of the Product type T. + * @tparam T the underlying type of the result, a Product. + * @return a MultiCellParser which converts a String from a Row into the field type P and thence into a T + */ def cellParser1[P1: CellParser, T <: Product : ClassTag : ColumnHelper](construct: P1 => T, fields: Seq[String] = Nil): CellParser[T] = { new MultiCellParser[T] { @@ -138,14 +137,14 @@ trait CellParsers { } /** - * Method to return a CellParser[T] where T is a 2-ary Product and which is based on a function to convert a (P1,P2) into a T. - * - * @param construct a function (P1,P2) => T, usually the apply method of a case class. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a MultiCellParser which converts Strings from a Row into the field types P1 and P2 and thence into a T - */ + * Method to return a CellParser[T] where T is a 2-ary Product and which is based on a function to convert a (P1,P2) into a T. + * + * @param construct a function (P1,P2) => T, usually the apply method of a case class. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam T the underlying type of the result, a Product. + * @return a MultiCellParser which converts Strings from a Row into the field types P1 and P2 and thence into a T + */ def cellParser2[P1: CellParser, P2: CellParser, T <: Product : ClassTag : ColumnHelper](construct: (P1, P2) => T, fields: Seq[String] = Nil): CellParser[T] = { new MultiCellParser[T] { override def toString: String = s"MultiCellParser: cellParser2 for ${implicitly[ClassTag[T]]}" @@ -163,19 +162,19 @@ trait CellParsers { } /** - * Method to return a CellParser[T] where T is a 2-ary Product and which is based on a function to convert a (K,P) into a T. - * This method differs from cellParser2 in that the parser of P (a CellParser[P]) is not found implicitly, but rather is looked up - * dynamically depending on the value of the first parameter (of type K). - * - * @param construct a function (K,P) => T, usually the apply method of a case class. - * @param parsers a Map[K, CellParser[P] ] which determines which particular parser of P will be used. - * The key value looked up is the value of the first (K) field. - * @tparam K the type of the conditional lookup key, which is also the type of the first field of T. - * Typically, this will be a String, but it could also be an Int or something more exotic. - * @tparam P the type of the second field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a MultiCellParser which converts Strings from a Row into the field types K and P and thence into a T. - */ + * Method to return a CellParser[T] where T is a 2-ary Product and which is based on a function to convert a (K,P) into a T. + * This method differs from cellParser2 in that the parser of P (a CellParser[P]) is not found implicitly, but rather is looked up + * dynamically depending on the value of the first parameter (of type K). + * + * @param construct a function (K,P) => T, usually the apply method of a case class. + * @param parsers a Map[K, CellParser[P] ] which determines which particular parser of P will be used. + * The key value looked up is the value of the first (K) field. + * @tparam K the type of the conditional lookup key, which is also the type of the first field of T. + * Typically, this will be a String, but it could also be an Int or something more exotic. + * @tparam P the type of the second field of the Product type T. + * @tparam T the underlying type of the result, a Product. + * @return a MultiCellParser which converts Strings from a Row into the field types K and P and thence into a T. + */ def cellParser2Conditional[K: CellParser, P, T <: Product : ClassTag : ColumnHelper](construct: (K, P) => T, parsers: Map[K, CellParser[P]], fields: Seq[String] = Nil): CellParser[T] = { new MultiCellParser[T] { override def toString: String = s"MultiCellParser: cellParser2 for ${implicitly[ClassTag[T]]}" @@ -193,15 +192,15 @@ trait CellParsers { } /** - * Method to return a CellParser[T] where T is a 3-ary Product and which is based on a function to convert a (P1,P2,P3) into a T. - * - * @param construct a function (P1,P2,P3) => T, usually the apply method of a case class. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the third field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2 and P3 and thence into a T - */ + * Method to return a CellParser[T] where T is a 3-ary Product and which is based on a function to convert a (P1,P2,P3) into a T. + * + * @param construct a function (P1,P2,P3) => T, usually the apply method of a case class. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam T the underlying type of the result, a Product. + * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2 and P3 and thence into a T + */ def cellParser3[P1: CellParser, P2: CellParser, P3: CellParser, T <: Product : ClassTag : ColumnHelper](construct: (P1, P2, P3) => T, fields: Seq[String] = Nil): CellParser[T] = { new MultiCellParser[T] { override def toString: String = s"MultiCellParser: cellParser3 for ${implicitly[ClassTag[T]]}" @@ -219,16 +218,16 @@ trait CellParsers { } /** - * Method to return a CellParser[T] where T is a 4-ary Product and which is based on a function to convert a (P1,P2,P3,P4) into a T. - * - * @param construct a function (P1,P2,P3,P4) => T, usually the apply method of a case class. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3 and P4 and thence into a T - */ + * Method to return a CellParser[T] where T is a 4-ary Product and which is based on a function to convert a (P1,P2,P3,P4) into a T. + * + * @param construct a function (P1,P2,P3,P4) => T, usually the apply method of a case class. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam T the underlying type of the result, a Product. + * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3 and P4 and thence into a T + */ def cellParser4[P1: CellParser, P2: CellParser, P3: CellParser, P4: CellParser, T <: Product : ClassTag : ColumnHelper](construct: (P1, P2, P3, P4) => T, fields: Seq[String] = Nil): CellParser[T] = { new MultiCellParser[T] { override def toString: String = s"MultiCellParser: cellParser4 for ${implicitly[ClassTag[T]]}" @@ -246,17 +245,17 @@ trait CellParsers { } /** - * Method to return a CellParser[T] where T is a 5-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5) => T, usually the apply method of a case class. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4 and P5 and thence into a T - */ + * Method to return a CellParser[T] where T is a 5-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5) => T, usually the apply method of a case class. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam T the underlying type of the result, a Product. + * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4 and P5 and thence into a T + */ def cellParser5[P1: CellParser, P2: CellParser, P3: CellParser, P4: CellParser, P5: CellParser, T <: Product : ClassTag : ColumnHelper](construct: (P1, P2, P3, P4, P5) => T, fields: Seq[String] = Nil): CellParser[T] = { new MultiCellParser[T] { override def toString: String = s"MultiCellParser: cellParser5 for ${implicitly[ClassTag[T]]}" @@ -274,18 +273,18 @@ trait CellParsers { } /** - * Method to return a CellParser[T] where T is a 6-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5,P6) => T, usually the apply method of a case class. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam P6 the type of the sixth field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4, P5 and P6 and thence into a T - */ + * Method to return a CellParser[T] where T is a 6-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6) => T, usually the apply method of a case class. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam T the underlying type of the result, a Product. + * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4, P5 and P6 and thence into a T + */ def cellParser6[P1: CellParser, P2: CellParser, P3: CellParser, P4: CellParser, P5: CellParser, P6: CellParser, T <: Product : ClassTag : ColumnHelper](construct: (P1, P2, P3, P4, P5, P6) => T, fields: Seq[String] = Nil): CellParser[T] = { new MultiCellParser[T] { override def toString: String = s"MultiCellParser: cellParser6 for ${implicitly[ClassTag[T]]}" @@ -303,19 +302,19 @@ trait CellParsers { } /** - * Method to return a CellParser[T] where T is a 7-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5,P6,P7) => T, usually the apply method of a case class. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam P6 the type of the sixth field of the Product type T. - * @tparam P7 the type of the seventh field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6 and P7 and thence into a T - */ + * Method to return a CellParser[T] where T is a 7-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7) => T, usually the apply method of a case class. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam T the underlying type of the result, a Product. + * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6 and P7 and thence into a T + */ def cellParser7[P1: CellParser, P2: CellParser, P3: CellParser, P4: CellParser, P5: CellParser, P6: CellParser, P7: CellParser, T <: Product : ClassTag : ColumnHelper](construct: (P1, P2, P3, P4, P5, P6, P7) => T, fields: Seq[String] = Nil): CellParser[T] = { new MultiCellParser[T] { override def toString: String = s"MultiCellParser: cellParser7 for ${implicitly[ClassTag[T]]}" @@ -333,20 +332,20 @@ trait CellParsers { } /** - * Method to return a CellParser[T] where T is a 8-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8) => T, usually the apply method of a case class. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam P6 the type of the sixth field of the Product type T. - * @tparam P7 the type of the seventh field of the Product type T. - * @tparam P8 the type of the eighth field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7 and P8 and thence into a T - */ + * Method to return a CellParser[T] where T is a 8-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8) => T, usually the apply method of a case class. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam T the underlying type of the result, a Product. + * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7 and P8 and thence into a T + */ def cellParser8[P1: CellParser, P2: CellParser, P3: CellParser, P4: CellParser, P5: CellParser, P6: CellParser, P7: CellParser, P8: CellParser, T <: Product : ClassTag : ColumnHelper](construct: (P1, P2, P3, P4, P5, P6, P7, P8) => T, fields: Seq[String] = Nil): CellParser[T] = { new MultiCellParser[T] { override def toString: String = s"MultiCellParser: cellParser8 for ${implicitly[ClassTag[T]]}" @@ -364,21 +363,21 @@ trait CellParsers { } /** - * Method to return a CellParser[T] where T is a 9-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9) => T, usually the apply method of a case class. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam P6 the type of the sixth field of the Product type T. - * @tparam P7 the type of the seventh field of the Product type T. - * @tparam P8 the type of the eighth field of the Product type T. - * @tparam P9 the type of the ninth field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7, P8 and P9 and thence into a T - */ + * Method to return a CellParser[T] where T is a 9-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9) => T, usually the apply method of a case class. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam T the underlying type of the result, a Product. + * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7, P8 and P9 and thence into a T + */ def cellParser9[P1: CellParser, P2: CellParser, P3: CellParser, P4: CellParser, P5: CellParser, P6: CellParser, P7: CellParser, P8: CellParser, P9: CellParser, T <: Product : ClassTag : ColumnHelper](construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9) => T, fields: Seq[String] = Nil): CellParser[T] = { new MultiCellParser[T] { override def toString: String = s"MultiCellParser: cellParser9 for ${implicitly[ClassTag[T]]}" @@ -396,22 +395,22 @@ trait CellParsers { } /** - * Method to return a CellParser[T] where T is a 10-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10) => T, usually the apply method of a case class. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam P6 the type of the sixth field of the Product type T. - * @tparam P7 the type of the seventh field of the Product type T. - * @tparam P8 the type of the eighth field of the Product type T. - * @tparam P9 the type of the ninth field of the Product type T. - * @tparam P10 the type of the tenth field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7, P8, P9 and P10 and thence into a T - */ + * Method to return a CellParser[T] where T is a 10-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10) => T, usually the apply method of a case class. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam P10 the type of the tenth field of the Product type T. + * @tparam T the underlying type of the result, a Product. + * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7, P8, P9 and P10 and thence into a T + */ def cellParser10[P1: CellParser, P2: CellParser, P3: CellParser, P4: CellParser, P5: CellParser, P6: CellParser, P7: CellParser, P8: CellParser, P9: CellParser, P10: CellParser, T <: Product : ClassTag : ColumnHelper](construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10) => T, fields: Seq[String] = Nil): CellParser[T] = { new MultiCellParser[T] { override def toString: String = s"MultiCellParser: cellParser10 for ${implicitly[ClassTag[T]]}" @@ -429,23 +428,23 @@ trait CellParsers { } /** - * Method to return a CellParser[T] where T is a 11-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11) => T, usually the apply method of a case class. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam P6 the type of the sixth field of the Product type T. - * @tparam P7 the type of the seventh field of the Product type T. - * @tparam P8 the type of the eighth field of the Product type T. - * @tparam P9 the type of the ninth field of the Product type T. - * @tparam P10 the type of the tenth field of the Product type T. - * @tparam P11 the type of the eleventh field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7, P8, P9, P10 and P11 and thence into a T - */ + * Method to return a CellParser[T] where T is a 11-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11) => T, usually the apply method of a case class. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam P10 the type of the tenth field of the Product type T. + * @tparam P11 the type of the eleventh field of the Product type T. + * @tparam T the underlying type of the result, a Product. + * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7, P8, P9, P10 and P11 and thence into a T + */ def cellParser11[P1: CellParser, P2: CellParser, P3: CellParser, P4: CellParser, P5: CellParser, P6: CellParser, P7: CellParser, P8: CellParser, P9: CellParser, P10: CellParser, P11: CellParser, T <: Product : ClassTag : ColumnHelper](construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11) => T, fields: Seq[String] = Nil): CellParser[T] = { new MultiCellParser[T] { override def toString: String = s"MultiCellParser: cellParser11 for ${implicitly[ClassTag[T]]}" @@ -463,24 +462,24 @@ trait CellParsers { } /** - * Method to return a CellParser[T] where T is a 12-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12) => T, usually the apply method of a case class. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam P6 the type of the sixth field of the Product type T. - * @tparam P7 the type of the seventh field of the Product type T. - * @tparam P8 the type of the eighth field of the Product type T. - * @tparam P9 the type of the ninth field of the Product type T. - * @tparam P10 the type of the tenth field of the Product type T. - * @tparam P11 the type of the eleventh field of the Product type T. - * @tparam P12 the type of the twelfth field of the Product type T. - * @tparam T the underlying type of the result, a Product. - * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11 and P12 and thence into a T - */ + * Method to return a CellParser[T] where T is a 12-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12) => T, usually the apply method of a case class. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam P10 the type of the tenth field of the Product type T. + * @tparam P11 the type of the eleventh field of the Product type T. + * @tparam P12 the type of the twelfth field of the Product type T. + * @tparam T the underlying type of the result, a Product. + * @return a MultiCellParser which converts Strings from a Row into the field types P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11 and P12 and thence into a T + */ def cellParser12[P1: CellParser, P2: CellParser, P3: CellParser, P4: CellParser, P5: CellParser, P6: CellParser, P7: CellParser, P8: CellParser, P9: CellParser, P10: CellParser, P11: CellParser, P12: CellParser, T <: Product : ClassTag : ColumnHelper](construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12) => T, fields: Seq[String] = Nil): CellParser[T] = { new MultiCellParser[T] { override def toString: String = s"MultiCellParser: cellParser12 for ${implicitly[ClassTag[T]]}" @@ -499,34 +498,34 @@ trait CellParsers { /** - * Method to yield a ColumnHelper[T] based on an optional prefix, and some number of explicit aliases, - * - * @param maybePrefix an optional prefix for the column name. - * @param aliases a variable number of explicit aliases to translate specific case class parameter names into their column name equivalents. - * @tparam T the underlying type of the resulting ColumnHelper - * @return a new instance of ColumnHelper[T] - */ + * Method to yield a ColumnHelper[T] based on an optional prefix, and some number of explicit aliases, + * + * @param maybePrefix an optional prefix for the column name. + * @param aliases a variable number of explicit aliases to translate specific case class parameter names into their column name equivalents. + * @tparam T the underlying type of the resulting ColumnHelper + * @return a new instance of ColumnHelper[T] + */ def columnHelper[T](maybePrefix: Option[String], aliases: (String, String)*): ColumnHelper[T] = columnHelper(identity[String] _, maybePrefix, aliases: _*) /** - * Method to yield a ColumnHelper[T] based on a column name mapper, and some number of explicit aliases, - * - * @param columnNameMapper a mapper of String=>String which will translate case class parameter names into column names. - * @param aliases a variable number of explicit aliases to translate specific case class parameter names into their column name equivalents. - * @tparam T the underlying type of the resulting ColumnHelper - * @return a new instance of ColumnHelper[T] - */ + * Method to yield a ColumnHelper[T] based on a column name mapper, and some number of explicit aliases, + * + * @param columnNameMapper a mapper of String=>String which will translate case class parameter names into column names. + * @param aliases a variable number of explicit aliases to translate specific case class parameter names into their column name equivalents. + * @tparam T the underlying type of the resulting ColumnHelper + * @return a new instance of ColumnHelper[T] + */ def columnHelper[T](columnNameMapper: String => String, aliases: (String, String)*): ColumnHelper[T] = columnHelper(columnNameMapper, None, aliases: _*) /** - * Method to yield a ColumnHelper[T] based on a column name mapper, an optional prefix, and some number of explicit aliases, - * - * @param columnNameMapper a mapper of String=>String which will translate case class parameter names into column names. - * @param maybePrefix an optional prefix for the column name. - * @param aliases a variable number of explicit aliases to translate specific case class parameter names into their column name equivalents. - * @tparam T the underlying type of the resulting ColumnHelper - * @return a new instance of ColumnHelper[T] - */ + * Method to yield a ColumnHelper[T] based on a column name mapper, an optional prefix, and some number of explicit aliases, + * + * @param columnNameMapper a mapper of String=>String which will translate case class parameter names into column names. + * @param maybePrefix an optional prefix for the column name. + * @param aliases a variable number of explicit aliases to translate specific case class parameter names into their column name equivalents. + * @tparam T the underlying type of the resulting ColumnHelper + * @return a new instance of ColumnHelper[T] + */ def columnHelper[T](columnNameMapper: String => String, maybePrefix: Option[String], aliases: (String, String)*): ColumnHelper[T] = new ColumnHelper[T] { val maybePrefix_ : Option[String] = maybePrefix val aliases_ : Seq[(String, String)] = aliases @@ -534,21 +533,21 @@ trait CellParsers { } /** - * Method to yield a ColumnHelper[T] based on some number of explicit aliases, - * - * @param aliases a variable number of explicit aliases to translate specific case class parameter names into their column name equivalents. - * @tparam T the underlying type of the resulting ColumnHelper - * @return a new instance of ColumnHelper[T] - */ + * Method to yield a ColumnHelper[T] based on some number of explicit aliases, + * + * @param aliases a variable number of explicit aliases to translate specific case class parameter names into their column name equivalents. + * @tparam T the underlying type of the resulting ColumnHelper + * @return a new instance of ColumnHelper[T] + */ def columnHelper[T](aliases: (String, String)*): ColumnHelper[T] = columnHelper(identity[String] _, aliases: _*) /** - * A default column mapper which will work for any underlying type T and which provides no column name mapping at all. - * This is suitable for the usual case where the names of the, e.g., CSV columns are the same as the names of the case class parameters. - * - * @tparam T the underlying type of the resulting ColumnHelper - * @return a new instance of ColumnHelper[T] - */ + * A default column mapper which will work for any underlying type T and which provides no column name mapping at all. + * This is suitable for the usual case where the names of the, e.g., CSV columns are the same as the names of the case class parameters. + * + * @tparam T the underlying type of the resulting ColumnHelper + * @return a new instance of ColumnHelper[T] + */ implicit def defaultColumnHelper[T]: ColumnHelper[T] = columnHelper() // CONSIDER inlining readCellWithHeader @@ -556,27 +555,29 @@ trait CellParsers { readCellWithHeader(wo, row, columns, p) /** - * Return the field names as Seq[String], from either the fields parameter or by reflection into T. - * Note that fields takes precedence and ClassTag[T] is ignored if fields is used. - * - * @param fields a list of field names to be used instead of the reflected fields of T. - * @tparam T the type (typically a case class) from which we will use reflection to get the field names (referred to only if fields is Nil) - * @return the field names to be used. - */ + * Return the field names as Seq[String], from either the fields parameter or by reflection into T. + * Note that fields takes precedence and ClassTag[T] is ignored if fields is used. + * + * @param fields a list of field names to be used instead of the reflected fields of T. + * @tparam T the type (typically a case class) from which we will use reflection to get the field names (referred to only if fields is Nil) + * @return the field names to be used. + */ private def fieldNames[T: ClassTag](fields: Seq[String]) = fields match { case Nil => Reflection.extractFieldNames(implicitly[ClassTag[T]]).toList case ps => ps } - private def readCellWithHeader[P: CellParser, T <: Product : ClassTag : ColumnHelper](wo: Option[String], row: Row, columns: Header, p: String) = { + private def readCellWithHeader[P: CellParser, T <: Product : ClassTag : ColumnHelper](wo: Option[String], row: Row, header: Header, p: String) = { + val columnNames = header.xs val columnName = implicitly[ColumnHelper[T]].lookup(wo, p) val cellParser = implicitly[CellParser[P]] val idx = row.getIndex(columnName) if (idx >= 0) for (w <- row(idx); z <- cellParser.parse(CellValue(w)).recoverWith { case NonFatal(e) => Failure(InvalidParseException(s"Problem parsing '$w' as ${implicitly[ClassTag[T]].runtimeClass} from $columnName at index $idx of $row", e)) }) yield z - else cellParser.parse(Some(columnName), row, columns).recoverWith { - case _: UnsupportedOperationException => Failure(ParserException(s"unable to find value for column $columnName in $columns")) + else cellParser.parse(Some(columnName), row, header).recoverWith { + case _: UnsupportedOperationException => + Failure(ParserException(s"readCellWithHeader: where original name is based on $wo and $p, unable to find value for column '$columnName' in '$columnNames'")) } } } @@ -584,43 +585,43 @@ trait CellParsers { object StdCellParsers extends CellParsers /** - * CONSIDER: do we really need this exception? - * - * @param w the message. - */ + * CONSIDER: do we really need this exception? It doesn't appear to be used. + * + * @param w the message. + */ case class ParsersException(w: String) extends Exception(w) /** - * This class is used for the situation where a column in a table actually contains a set of - * attributes, typically separated by "," and possibly bracketed by "{}". - * CONSIDER allowing "|" as a separator (as described previously in the documentation here). - * - * @param xs the attribute values. - */ + * This class is used for the situation where a column in a table actually contains a set of + * attributes, typically separated by "," and possibly bracketed by "{}". + * CONSIDER allowing "|" as a separator (as described previously in the documentation here). + * + * @param xs the attribute values. + */ case class AttributeSet(xs: StringList) object AttributeSet { /** - * This method is required to be a String=>AttributeSet and is only invoked inside Try. - * It invokes parse to get its result. - * - * NOTE: essentially, we are doing a get, and trying to make it explicit so that Codacy doesn't get upset ;) - * - * @param w the String to be converted to an AttributeSet. - * @return an AttributeSet. - */ + * This method is required to be a String=>AttributeSet and is only invoked inside Try. + * It invokes parse to get its result. + * + * NOTE: essentially, we are doing a get, and trying to make it explicit so that Codacy doesn't get upset ;) + * + * @param w the String to be converted to an AttributeSet. + * @return an AttributeSet. + */ def apply(w: String): AttributeSet = parse(w) match { case Success(a) => a case Failure(x) => throw x } /** - * Method to parse a String as an AttributeSet. - * - * @param w the String to be parsed as an AttributeSet. - * @return a Try[AttributeSet] - */ + * Method to parse a String as an AttributeSet. + * + * @param w the String to be parsed as an AttributeSet. + * @return a Try[AttributeSet] + */ def parse(w: String): Try[AttributeSet] = Parseable.split(w).map(apply) } diff --git a/src/main/scala/com/phasmidsoftware/parse/ColumnHelper.scala b/src/main/scala/com/phasmidsoftware/parse/ColumnHelper.scala index 84eb034e..a835784a 100644 --- a/src/main/scala/com/phasmidsoftware/parse/ColumnHelper.scala +++ b/src/main/scala/com/phasmidsoftware/parse/ColumnHelper.scala @@ -7,37 +7,37 @@ package com.phasmidsoftware.parse import scala.annotation.implicitNotFound /** - * Type class representing a mapping from a case class parameter to the corresponding column header. - * - * @tparam T the type of the object being helped. - */ + * Type class representing a mapping from a case class parameter to the corresponding column header. + * + * @tparam T the type of the object being helped. + */ @implicitNotFound(msg = "Cannot find an implicit instance of ColumnHelper[${T}]. If ${T} is a case class, you will normally need to provide some help translating column names.") trait ColumnHelper[T] { /** - * This is the format for the prefix of a name, where "x" represents the value, if any, of the optional string. - */ + * This is the format for the prefix of a name, where "x" represents the value, if any, of the optional string. + */ val maybePrefix_ : Option[String] /** - * These are the alias mappings - */ + * These are the alias mappings + */ val aliases_ : Seq[(String, String)] /** - * This defines the default mapping between parameter names and column names. - * It is only used if there is no mapping defined in aliases. - * - * @return the mapper function. - */ + * This defines the default mapping between parameter names and column names. + * It is only used if there is no mapping defined in aliases. + * + * @return the mapper function. + */ val columnNameMapper_ : String => String /** - * This is the lookup function - * - * @param so is an optional string to be inserted into the prefix - * @param w is the name of column, as determined from the Product which we are trying to fill. - * @return a String which may include a prefix. - */ + * This is the lookup function + * + * @param so is an optional string to be inserted into the prefix + * @param w is the name of column, as determined from the Product which we are trying to fill. + * @return a String which may include a prefix. + */ def lookup(so: Option[String], w: String): String = { val column = aliases_.toMap.getOrElse(w, columnNameMapper_(w)) (for (p <- maybePrefix_; s <- so) yield p.replace("$x", s).replace("$c", column)).getOrElse(column) diff --git a/src/main/scala/com/phasmidsoftware/parse/LineParser.scala b/src/main/scala/com/phasmidsoftware/parse/LineParser.scala index 4c344c67..04226ec0 100644 --- a/src/main/scala/com/phasmidsoftware/parse/LineParser.scala +++ b/src/main/scala/com/phasmidsoftware/parse/LineParser.scala @@ -5,25 +5,24 @@ package com.phasmidsoftware.parse import org.slf4j.{Logger, LoggerFactory} - import scala.annotation.tailrec import scala.util.Try import scala.util.matching.Regex import scala.util.parsing.combinator.JavaTokenParsers /** - * LineParser: class to parse lines of a CSV file. - * NOTE: list elements always appear as a string in the form { element0 , element1 , ... } - * - * @param delimiter a Regex used to match a delimiter between cells in a row. - * @param string a Regex used to match the content of a cell. - * @param enclosures the enclosure characters around a list (if any). - * @param listSeparator the list separator character. - * @param quote the quote character which is able to preempt the string regex: - * between two quote characters, - * there can be any number of any character (other than quote). - * @param verbose will print the various parameters. - */ + * LineParser: class to parse lines of a CSV file. + * NOTE: list elements always appear as a string in the form { element0 , element1 , ... } + * + * @param delimiter a Regex used to match a delimiter between cells in a row. + * @param string a Regex used to match the content of a cell. + * @param enclosures the enclosure characters around a list (if any). + * @param listSeparator the list separator character. + * @param quote the quote character which is able to preempt the string regex: + * between two quote characters, + * there can be any number of any character (other than quote). + * @param verbose will print the various parameters. + */ class LineParser(delimiter: Regex, string: Regex, enclosures: String, listSeparator: Char, quote: Char, verbose: Boolean = false) extends JavaTokenParsers { if (verbose) LineParser.logger.info(s"delimiter: '${delimiter.regex}', string: '${string.regex}', enclosures: '$enclosures', quote: '$quote', listSeparator: '$listSeparator', ") @@ -32,14 +31,14 @@ class LineParser(delimiter: Regex, string: Regex, enclosures: String, listSepara override def skipWhitespace: Boolean = false /** - * Method to parse a Row. - * - * NOTE: the expression "end of input expected" must be the same as the failure defined in (trait) Parsers: def phrase[T](p: Parser[T]): Parser[T] - * It's a shame that they didn't make it a constant in Parsers! - * - * @param indexedString a tuple of String and Int denoting the line and its index in the file. - * @return a Try[Strings]. - */ + * Method to parse a Row. + * + * NOTE: the expression "end of input expected" must be the same as the failure defined in (trait) Parsers: def phrase[T](p: Parser[T]): Parser[T] + * It's a shame that they didn't make it a constant in Parsers! + * + * @param indexedString a tuple of String and Int denoting the line and its index in the file. + * @return a Try[Strings]. + */ def parseRow(indexedString: (String, Int)): Try[Strings] = parseAll(row, indexedString._1) match { case Success(s, _) => scala.util.Success(s) case Failure("end of input expected", _) => scala.util.Failure(MultiLineException(indexedString)) @@ -63,8 +62,8 @@ class LineParser(delimiter: Regex, string: Regex, enclosures: String, listSepara lazy val list: Parser[String] = getOpenChar ~> (component ~ listSeparator ~ rep1sep(component, listSeparator)) <~ getCloseChar ^^ { case x ~ _ ~ xs => (x +: xs).mkString("{", ",", "}") } - // TODO why is this complaining about repeated characters? - private lazy val component: Parser[String] = s"""[^,$listSeparator}]+""".r + private val regexComponent = s"""[^,$listSeparator}]+""" + private lazy val component: Parser[String] = regexComponent.r private lazy val getOpenChar: Parser[String] = s"${enclosures.headOption.getOrElse("")}" @@ -120,10 +119,10 @@ class LineParser(delimiter: Regex, string: Regex, enclosures: String, listSepara } ( - check(cell, "Hello", "Hello") && - // check(cell, "http://www.imdb.com/title/tt0499549/?ref_=fn_tt_tt_1", "http://www.imdb.com/title/tt0499549/?ref_=fn_tt_tt_1") && - check(quotedString, s"""${quote}Hello${getDelimiterChar}Goodbye$quote""", s"""Hello${getDelimiterChar}Goodbye""") - ).squawk() + check(cell, "Hello", "Hello") && + // check(cell, "http://www.imdb.com/title/tt0499549/?ref_=fn_tt_tt_1", "http://www.imdb.com/title/tt0499549/?ref_=fn_tt_tt_1") && + check(quotedString, s"""${quote}Hello${getDelimiterChar}Goodbye$quote""", s"""Hello${getDelimiterChar}Goodbye""") + ).squawk() } } diff --git a/src/main/scala/com/phasmidsoftware/parse/Parseable.scala b/src/main/scala/com/phasmidsoftware/parse/Parseable.scala index 996f4eaf..c7450c55 100644 --- a/src/main/scala/com/phasmidsoftware/parse/Parseable.scala +++ b/src/main/scala/com/phasmidsoftware/parse/Parseable.scala @@ -4,37 +4,36 @@ package com.phasmidsoftware.parse -import org.joda.time.LocalDate - import java.io.File import java.net.URL +import org.joda.time.LocalDate import scala.annotation.implicitNotFound import scala.util.parsing.combinator.JavaTokenParsers import scala.util.{Failure, Success, Try} /** - * Type class which describes a type which can be parsed from a String. - * - * @tparam T the resulting type. - */ + * Type class which describes a type which can be parsed from a String. + * + * @tparam T the resulting type. + */ @implicitNotFound(msg = "Cannot find an implicit instance of Parseable[${T}]. This is unusual when your application types are all case classes. Most of the standard types are supported in the Parseable companion object. Take a look and define something similar that works for your type, or consider redefining your type as a case class.") trait Parseable[T] { /** - * Parse a String as a Try[T]. - * - * @param s the String to be parsed. - * @return the corresponding value of type T, wrapped in Try. - */ + * Parse a String as a Try[T]. + * + * @param s the String to be parsed. + * @return the corresponding value of type T, wrapped in Try. + */ def parse(s: String): Try[T] } object Parseable { /** - * Parser of String. - * The exception is useful for ensuring a None in the case of an optional String. - */ + * Parser of String. + * The exception is useful for ensuring a None in the case of an optional String. + */ trait ParseableString extends Parseable[String] { def parse(s: String): Try[String] = if (s.isEmpty) Failure(BlankException()) else Success(s) } @@ -42,8 +41,8 @@ object Parseable { implicit object ParseableString extends ParseableString /** - * Parser of Boolean. - */ + * Parser of Boolean. + */ trait ParseableBoolean extends Parseable[Boolean] { def parse(s: String): Try[Boolean] = parseAndRecover(s)(lift(_.toBoolean))(w => s"ParseableBoolean: cannot interpret '$w' as a Boolean") } @@ -51,8 +50,8 @@ object Parseable { implicit object ParseableBoolean extends ParseableBoolean /** - * Parser of Byte. - */ + * Parser of Byte. + */ trait ParseableByte extends Parseable[Byte] { def parse(s: String): Try[Byte] = parseAndRecover(s)(lift(_.toByte))(w => s"ParseableByte: cannot interpret '$w' as a Byte") } @@ -60,8 +59,8 @@ object Parseable { implicit object ParseableByte extends ParseableByte /** - * Parser of Char. - */ + * Parser of Char. + */ trait ParseableChar extends Parseable[Char] { def parse(s: String): Try[Char] = parseAndRecover(s)(lift(_.head))(w => s"ParseableChar: cannot interpret '$w' as a Char") } @@ -69,8 +68,8 @@ object Parseable { implicit object ParseableChar extends ParseableChar /** - * Parser of Short. - */ + * Parser of Short. + */ trait ParseableShort extends Parseable[Short] { def parse(s: String): Try[Short] = parseAndRecover(s)(lift(_.toShort))(w => s"ParseableShort: cannot interpret '$w' as a Short") } @@ -78,8 +77,8 @@ object Parseable { implicit object ParseableShort extends ParseableShort /** - * Parser of Int. - */ + * Parser of Int. + */ trait ParseableInt extends Parseable[Int] { def parse(s: String): Try[Int] = parseAndRecover(s)(lift(_.toInt))(w => s"ParseableInt: cannot interpret '$w' as an Int") } @@ -87,8 +86,8 @@ object Parseable { implicit object ParseableInt extends ParseableInt /** - * Parser of Long. - */ + * Parser of Long. + */ trait ParseableLong extends Parseable[Long] { def parse(s: String): Try[Long] = parseAndRecover(s)(lift(_.toLong))(w => s"ParseableLong: cannot interpret '$w' as a Long") } @@ -96,8 +95,8 @@ object Parseable { implicit object ParseableLong extends ParseableLong /** - * Parser of BigInt. - */ + * Parser of BigInt. + */ trait ParseableBigInt extends Parseable[BigInt] { def parse(s: String): Try[BigInt] = parseAndRecover(s)(lift(BigInt.apply))(w => s"ParseableBigInt: cannot interpret '$w' as a BigInt") } @@ -105,8 +104,8 @@ object Parseable { implicit object ParseableBigInt extends ParseableBigInt /** - * Parser of BigDecimal. - */ + * Parser of BigDecimal. + */ trait ParseableBigDecimal extends Parseable[BigDecimal] { def parse(s: String): Try[BigDecimal] = parseAndRecover(s)(lift(BigDecimal.apply))(w => s"ParseableBigDecimal: cannot interpret '$w' as a BigDecimal") } @@ -114,8 +113,8 @@ object Parseable { implicit object ParseableBigDecimal extends ParseableBigDecimal /** - * Parser of Double. - */ + * Parser of Double. + */ trait ParseableDouble extends Parseable[Double] { def parse(s: String): Try[Double] = parseAndRecover(s)(lift(_.toDouble))(w => s"ParseableDouble: cannot interpret '$w' as a Double") } @@ -123,8 +122,8 @@ object Parseable { implicit object ParseableDouble extends ParseableDouble /** - * Parser of Float. - */ + * Parser of Float. + */ trait ParseableFloat extends Parseable[Float] { def parse(s: String): Try[Float] = parseAndRecover(s)(lift(_.toFloat))(w => s"ParseableFloat: cannot interpret '$w' as a Float") } @@ -132,8 +131,8 @@ object Parseable { implicit object ParseableFloat extends ParseableFloat /** - * Parser of LocalDate. - */ + * Parser of LocalDate. + */ trait ParseableLocalDate extends Parseable[LocalDate] { def parse(s: String): Try[LocalDate] = parseAndRecover(s)(lift(LocalDate.parse))(w => s"ParseableLocalDate: cannot interpret '$w' as a LocalDate") } @@ -141,8 +140,8 @@ object Parseable { implicit object ParseableLocalDate extends ParseableLocalDate /** - * Parser of URL. - */ + * Parser of URL. + */ trait ParseableURL extends Parseable[URL] { def parse(s: String): Try[URL] = parseAndRecover(s)(lift(new URL(_)))(w => s"ParseableURL: cannot interpret '$w' as an URL") } @@ -150,8 +149,8 @@ object Parseable { implicit object ParseableURL extends ParseableURL /** - * Parser of File. - */ + * Parser of File. + */ trait ParseableFile extends Parseable[File] { def parse(s: String): Try[File] = parseAndRecover(s)(lift(new File(_)))(w => s"ParseableFile: cannot interpret '$w' as a File") } @@ -159,9 +158,9 @@ object Parseable { implicit object ParseableFile extends ParseableFile /** - * Parser of StringList. - * This trait splits strings of the form {x,y,z}, regardless of the format specified by the RowConfig object. - */ + * Parser of StringList. + * This trait splits strings of the form {x,y,z}, regardless of the format specified by the RowConfig object. + */ trait ParseableStringList extends Parseable[StringList] { def parse(s: String): Try[StringList] = parseAndRecover(s)(split)(w => s"ParseableStringList: cannot interpret '$w' as a StringList") } @@ -169,11 +168,11 @@ object Parseable { implicit object ParseableStringList extends ParseableStringList /** - * Method to split a String into a StringList, parser.list. - * - * @param w the String to parse. - * @return a Try[StringList]. - */ + * Method to split a String into a StringList, parser.list. + * + * @param w the String to parse. + * @return a Try[StringList]. + */ def split(w: String): Try[StringList] = ListParser.parseAll(ListParser.list, w) match { case ListParser.Success(ws: StringList, _) => Success(ws) case ListParser.Failure(msg, _) => Failure(ParseLogicException(s"cannot split string '$w': $msg")) @@ -190,15 +189,14 @@ object Parseable { } /** - * Abstract class to parse optional scala values. - * - * @tparam T the resulting type for which there must be evidence of a Parseable[T]. - */ + * Abstract class to parse optional scala values. + * + * @tparam T the resulting type for which there must be evidence of a Parseable[T]. + */ abstract class ParseableOption[T: Parseable] extends Parseable[Option[T]] { def parse(s: String): Try[Option[T]] = implicitly[Parseable[T]].parse(s).map(Option(_)).recoverWith { case _: BlankException => Success(None) } - } object ParseableOption { @@ -233,9 +231,9 @@ object ParseableOption { } /** - * This object is a parser of lists. - * A list is considered to be enclosed by {} and separated by commas. - */ + * This object is a parser of lists. + * A list is considered to be enclosed by {} and separated by commas. + */ object ListParser extends JavaTokenParsers { lazy val list: Parser[StringList] = "{" ~> strings <~ "}" | singleton diff --git a/src/main/scala/com/phasmidsoftware/parse/RawParsers.scala b/src/main/scala/com/phasmidsoftware/parse/RawParsers.scala index a902ed15..226eccce 100644 --- a/src/main/scala/com/phasmidsoftware/parse/RawParsers.scala +++ b/src/main/scala/com/phasmidsoftware/parse/RawParsers.scala @@ -8,12 +8,12 @@ import com.phasmidsoftware.RawRow import com.phasmidsoftware.table.{HeadedTable, Header, Table} /** - * Abstract class to define a raw parser, that's to say a Parser of Seq[String] - * - * @param maybeHeader a header if appropriate. - * @param forgiving true if we want this parser to be forgiving (defaults to false). - */ -abstract class RawParsers(maybeHeader: Option[Header], forgiving: Boolean = false) extends CellParsers { + * Abstract class to define a raw parser, that's to say a Parser of Seq[String] + * + * @param maybeHeader a header if appropriate. + * @param forgiving true if we want this parser to be forgiving (defaults to false). + */ +abstract class RawParsers(maybeHeader: Option[Header], forgiving: Boolean = false, headerRows: Int = 1) extends CellParsers { self => implicit val stringSeqParser: CellParser[RawRow] = cellParserSeq @@ -26,6 +26,8 @@ abstract class RawParsers(maybeHeader: Option[Header], forgiving: Boolean = fals val maybeFixedHeader: Option[Header] = maybeHeader + val headerRowsToRead: Int = headerRows + override val forgiving: Boolean = self.forgiving val rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] diff --git a/src/main/scala/com/phasmidsoftware/parse/RowConfig.scala b/src/main/scala/com/phasmidsoftware/parse/RowConfig.scala index 3de0d583..ec34a6db 100644 --- a/src/main/scala/com/phasmidsoftware/parse/RowConfig.scala +++ b/src/main/scala/com/phasmidsoftware/parse/RowConfig.scala @@ -7,67 +7,65 @@ package com.phasmidsoftware.parse import scala.util.matching.Regex /** - * Trait to define the configuration for parsing a row. - */ + * Trait to define the configuration for parsing a row. + */ trait RowConfig { /** - * the delimiter Regex (see LineParser). defaults to ", *".r, i.e. a comma followed by any n=umber of spaces.* - */ + * the delimiter Regex (see LineParser). defaults to ", *".r, i.e. a comma followed by any n=umber of spaces.* + */ val delimiter: Regex /** - * the "string" Regex (see LineParser). defaults to "\w+".r, i.e. at least one word character. - */ + * the "string" Regex (see LineParser). defaults to "\w+".r, i.e. at least one word character. + */ val string: Regex /** - * the "listSep" character (see LineParser). defaults to "|" - */ + * the "listSep" character (see LineParser). defaults to "|" + */ val listSep: Char /** - * the "listEnclosure" characters (see LineParser). defaults to "{}" - */ + * the "listEnclosure" characters (see LineParser). defaults to "{}" + */ val listEnclosure: String /** - * the "quote" Char (see LineParser). defaults to ". - */ + * the "quote" Char (see LineParser). defaults to ". + */ val quote: Char override def toString: String = s"RowConfig: delimiter='$delimiter', string='$string', listSep='$listSep', listEnclosure='$listEnclosure', $quote='$quote'" } /** - * Default RowConfig trait. - */ + * Default RowConfig trait. + */ trait DefaultRowConfig extends RowConfig { /** - * the "listSep" character (see LineParser). defaults to "|" - */ + * the "listSep" character (see LineParser). defaults to "|" + */ val listSep: Char = '|' /** - * the "listEnclosure" characters (see LineParser). defaults to "{}" - */ + * the "listEnclosure" characters (see LineParser). defaults to "{}" + */ val listEnclosure: String = "{}" /** - * the delimiter Regex (see LineParser). defaults to ", *".r, i.e. a comma followed by any n=umber of spaces.* - */ - val delimiter: Regex = ", *".r + * the delimiter Regex (see LineParser). defaults to ", *".r, i.e. a comma followed by any n=umber of spaces.* + */ + val delimiter: Regex = """\s*,\s*""".r /** - * the "string" Regex (see LineParser). defaults to "\w+".r, i.e. at least one word character. - * CONSIDER making the string regex derive from the delimiter - */ - val string: Regex = - """[^,"]*""".r + * the "string" Regex (see LineParser). defaults to "\w+".r, i.e. at least one word character. + * CONSIDER making the string regex derive from the delimiter + */ + val string: Regex = """[^,"]*""".r /** - * the "quote" Char (see LineParser). defaults to ". - */ + * the "quote" Char (see LineParser). defaults to ". + */ val quote: Char = '"' } /** - * Companion object to RowConfig. - */ + * Companion object to RowConfig. + */ object RowConfig { // CONSIDER removing this default row configuration. It might be better to have the compiler issue a warning when it's missing. implicit object defaultRowConfig extends DefaultRowConfig - } diff --git a/src/main/scala/com/phasmidsoftware/parse/RowParser.scala b/src/main/scala/com/phasmidsoftware/parse/RowParser.scala index dc14d596..2a143ab8 100644 --- a/src/main/scala/com/phasmidsoftware/parse/RowParser.scala +++ b/src/main/scala/com/phasmidsoftware/parse/RowParser.scala @@ -5,71 +5,74 @@ package com.phasmidsoftware.parse import com.phasmidsoftware.table.{Header, Row} - +import com.phasmidsoftware.util.FP import scala.annotation.implicitNotFound import scala.util.Try /** - * Trait to describe a parser which will yield a Try[Row] from a String representing a row of a table. - * - * @tparam Row the (parametric) Row type for which there must be evidence of a RowParser[Row, Input]. - * @tparam Input the (parametric) Input type for which there must be evidence of a RowParser[Row, Input]. - */ + * Trait to describe a parser which will yield a Try[Row] from a String representing a row of a table. + * + * @tparam Row the (parametric) Row type for which there must be evidence of a RowParser[Row, Input]. + * @tparam Input the (parametric) Input type for which there must be evidence of a RowParser[Row, Input]. + */ @implicitNotFound(msg = "Cannot find an implicit instance of RowParser[${Row}, ${Input}]. Typically, you might define a StandardRowParser or StandardStringsParser") trait RowParser[Row, Input] { /** - * Parse the Input, resulting in a Try[Row] - * - * @param indexedRow the Input, and its index to be parsed. - * @param header the header already parsed. - * @return a Try[Row]. - */ + * Parse the Input, resulting in a Try[Row] + * + * @param indexedRow the Input, and its index to be parsed. + * @param header the header already parsed. + * @return a Try[Row]. + */ def parse(indexedRow: (Input, Int))(header: Header): Try[Row] /** - * Parse the Input, resulting in a Try[Header] - * - * CONSIDER making this share the same signature as parse but for different Row type. - * - * @param x the Input to be parsed. - * @return a Try[Header] - */ - def parseHeader(x: Input): Try[Header] + * Parse the Input, resulting in a Try[Header] + * + * CONSIDER making this share the same signature as parse but for different Row type. + * + * @param xs a sequence of Inputs to be parsed. + * @return a Try[Header] + */ + def parseHeader(xs: Seq[Input]): Try[Header] } /** - * A RowParser whose input type is String. - * - * @tparam Row the (parametric) Row type for which there must be evidence of a RowParser[Row, Input]. - */ + * A RowParser whose input type is String. + * + * @tparam Row the (parametric) Row type for which there must be evidence of a RowParser[Row, Input]. + */ trait StringParser[Row] extends RowParser[Row, String] /** - * StandardRowParser: a parser which extends RowParser[Row] and will yield a Try[Row] from a String representing a line of a table. - * - * @param parser the LineParser to use - * @tparam Row the (parametric) Row type for which there must be evidence of a CellParser[Row]. - */ + * StandardRowParser: a parser which extends RowParser[Row] and will yield a Try[Row] from a String representing a line of a table. + * + * @param parser the LineParser to use + * @tparam Row the (parametric) Row type for which there must be evidence of a CellParser[Row]. + */ case class StandardRowParser[Row: CellParser](parser: LineParser) extends StringParser[Row] { /** - * Method to parse a String and return a Try[Row]. - * - * @param indexedString the row and index as a (String., Int) - * @param header the header already parsed. - * @return a Try[Row]. - */ + * Method to parse a String and return a Try[Row]. + * + * @param indexedString the row and index as a (String., Int) + * @param header the header already parsed. + * @return a Try[Row]. + */ def parse(indexedString: (String, Int))(header: Header): Try[Row] = for (ws <- parser.parseRow(indexedString); r <- doConversion(indexedString, header, ws)) yield r /** - * Method to parse a String as a Try[Header]. - * - * @param w the header row as a String. - * @return a Try[Header]. - */ - def parseHeader(w: String): Try[Header] = for (ws <- parser.parseRow((w, -1))) yield Header(ws) + * Method to parse a String as a Try[Header]. + * + * @param xs the header row(s) as a String. + * @return a Try[Header]. + */ + def parseHeader(xs: Seq[String]): Try[Header] = { + val wsys: Seq[Try[Strings]] = for (x <- xs.tail) yield parser.parseRow(x, -1) + for (w <- Try(xs.head); ws <- parser.parseRow((w, -1)); wss <- FP.sequence(wsys)) yield Header(ws, wss) + } private def doConversion(indexedString: (String, Int), header: Header, ws: Strings) = RowValues(Row(ws, header, indexedString._2)).convertTo[Row] @@ -80,35 +83,35 @@ object StandardRowParser { } /** - * Trait to describe a parser which will yield a Try[Row] from a sequence of Strings representing a row of a table. - * - * @tparam Row the (parametric) Row type for which there must be evidence of a CellParser[Row]. - */ + * Trait to describe a parser which will yield a Try[Row] from a sequence of Strings representing a row of a table. + * + * @tparam Row the (parametric) Row type for which there must be evidence of a CellParser[Row]. + */ trait StringsParser[Row] extends RowParser[Row, Strings] /** - * StandardRowParser: a parser which extends RowParser[Row] and will yield a Try[Row] from a String representing a line of a table. - * - * @tparam Row the (parametric) Row type for which there must be evidence of a CellParser[Row]. - */ + * StandardRowParser: a parser which extends RowParser[Row] and will yield a Try[Row] from a String representing a line of a table. + * + * @tparam Row the (parametric) Row type for which there must be evidence of a CellParser[Row]. + */ case class StandardStringsParser[Row: CellParser]() extends StringsParser[Row] { /** - * Method to parse a sequence of String into a Try[Row]. - * - * @param indexedString the rows and index as a (Strings., Int) - * @param header the header already parsed. - * @return a Try[Row]. - */ + * Method to parse a sequence of String into a Try[Row]. + * + * @param indexedString the rows and index as a (Strings., Int) + * @param header the header already parsed. + * @return a Try[Row]. + */ def parse(indexedString: (Strings, Int))(header: Header): Try[Row] = RowValues(Row(indexedString._1, header, indexedString._2)).convertTo[Row] /** - * Method to parse a sequence of Strings as a Try[Header]. - * - * @param ws the header row as a sequence of Strings. - * @return a Try[Header]. - */ - def parseHeader(ws: Strings): Try[Header] = Try(Header(ws)) + * Method to parse a sequence of Strings as a Try[Header]. + * + * @param ws the header row as a sequence of Strings. + * @return a Try[Header]. + */ + def parseHeader(ws: Seq[Strings]): Try[Header] = Try(Header(ws)) } diff --git a/src/main/scala/com/phasmidsoftware/parse/TableParser.scala b/src/main/scala/com/phasmidsoftware/parse/TableParser.scala index 3294d956..2d43c279 100644 --- a/src/main/scala/com/phasmidsoftware/parse/TableParser.scala +++ b/src/main/scala/com/phasmidsoftware/parse/TableParser.scala @@ -5,123 +5,129 @@ package com.phasmidsoftware.parse import com.phasmidsoftware.RawRow +import com.phasmidsoftware.parse.AbstractTableParser.logException import com.phasmidsoftware.parse.TableParser.includeAll import com.phasmidsoftware.table.{HeadedTable, Header, Table} import com.phasmidsoftware.util.FP.partition -import com.phasmidsoftware.util.{FP, FunctionIterator, Joinable, TryUsing} +import com.phasmidsoftware.util._ import org.slf4j.{Logger, LoggerFactory} - import scala.annotation.implicitNotFound import scala.io.Source import scala.reflect.ClassTag import scala.util.{Failure, Random, Success, Try} /** - * Type class to parse a set of rows as a Table. - * - * @tparam Table the Table type. - */ + * Type class to parse a set of rows as a Table. + * + * @tparam Table the Table type. + */ @implicitNotFound(msg = "Cannot find an implicit instance of TableParser[${Table}]. Typically, you should define an instance of StringTableParser or StringsTableParser.") trait TableParser[Table] { /** - * The row type. - */ + * The row type. + */ type Row /** - * The input type. - */ + * The input type. + */ type Input /** - * This variable determines if there is a programmed, i.e. fixed, header for the parser. - * If its value is None, it signifies that we must look to the first line of data - * for an appropriate header. - */ + * This variable determines if there is a programmed, i.e. fixed, header for the parser. + * If its value is None, it signifies that we must look to the first line of data + * for an appropriate header. + */ protected val maybeFixedHeader: Option[Header] /** - * Default method to create a new table. - * It does this by invoking either builderWithHeader or builderWithoutHeader, as appropriate. - * - * CONSIDER changing Iterable back to Iterator as it was at V1.0.13. - * - * @param rows the rows which will make up the table. - * @param header the Header, derived either from the program or the data. - * @return an instance of Table. - */ + * This indicates the number of header rows which must be read from the input. + * If maybeFixedHeader exists, then this number should be zero. + */ + val headerRowsToRead: Int + + /** + * Default method to create a new table. + * It does this by invoking either builderWithHeader or builderWithoutHeader, as appropriate. + * + * CONSIDER changing Iterable back to Iterator as it was at V1.0.13. + * + * @param rows the rows which will make up the table. + * @param header the Header, derived either from the program or the data. + * @return an instance of Table. + */ protected def builder(rows: Iterable[Row], header: Header): Table /** - * Method to determine how errors are handled. - * - * @return true if individual errors are logged but do not cause parsing to fail. - */ + * Method to determine how errors are handled. + * + * @return true if individual errors are logged but do not cause parsing to fail. + */ protected val forgiving: Boolean = false /** - * Value to determine whether it is acceptable to have a quoted string span more than one line. - * - * @return true if quoted strings may span more than one line. - */ + * Value to determine whether it is acceptable to have a quoted string span more than one line. + * + * @return true if quoted strings may span more than one line. + */ protected val multiline: Boolean = false /** - * Function to determine whether or not a row should be included in the table. - * Typically used for random sampling. - */ + * Function to determine whether or not a row should be included in the table. + * Typically used for random sampling. + */ protected val predicate: Try[Row] => Boolean = includeAll /** - * Method to define a row parser. - * - * @return a RowParser[Row, Input]. - */ + * Method to define a row parser. + * + * @return a RowParser[Row, Input]. + */ protected val rowParser: RowParser[Row, Input] /** - * Method to parse a table based on a sequence of Inputs. - * - * @param xs the sequence of Inputs, one for each row - * @return a Try[Table] - */ - def parse(xs: Iterator[Input]): Try[Table] + * Method to parse a table based on a sequence of Inputs. + * + * @param xs the sequence of Inputs, one for each row + * @return a Try[Table] + */ + def parse(xs: Iterator[Input], n: Int): Try[Table] } object TableParser { /** - * Class to allow the simplification of an expression to parse a source, given a StringTableParser. - * - * @param p a StringTableParser. - * @tparam T the underlying type of p (T will be Table[_]). - */ + * Class to allow the simplification of an expression to parse a source, given a StringTableParser. + * + * @param p a StringTableParser. + * @tparam T the underlying type of p (T will be Table[_]). + */ implicit class ImplicitParser[T](p: StringTableParser[T]) { /** - * Method to parse an iterator of String. - * - * @param xs an Iterator[String]. - * @return a Try[T]. - */ - def parse(xs: Iterator[String]): Try[T] = p.parse(xs) + * Method to parse an iterator of String. + * + * @param xs an Iterator[String]. + * @return a Try[T]. + */ + def parse(xs: Iterator[String]): Try[T] = p.parse(xs, 1) /** - * Method to parse a Source. - * NOTE the source s will be closed after parsing has been completed (no resource leaks). - * - * @param s a Source. - * @return a Try[T]. - */ + * Method to parse a Source. + * NOTE the source s will be closed after parsing has been completed (no resource leaks). + * + * @param s a Source. + * @return a Try[T]. + */ def parse(s: Source): Try[T] = TryUsing(s)(x => parse(x.getLines())) /** - * Method to parse a Try[Source]. - * NOTE the underlying source of sy will be closed after parsing has been completed (no resource leaks). - * - * @param sy a Source. - * @return a Try[T]. - */ + * Method to parse a Try[Source]. + * NOTE the underlying source of sy will be closed after parsing has been completed (no resource leaks). + * + * @param sy a Source. + * @return a Try[T]. + */ def parse(sy: Try[Source]): Try[T] = sy flatMap parse } @@ -130,20 +136,20 @@ object TableParser { val logger: Logger = LoggerFactory.getLogger(TableParser.getClass) /** - * Method to return a random sampling function. - * - * @param n this is the sample factor: approximately one in every n successful results will form part of the result. - * @return a Try[Any] => Boolean function which is always yields false if its input is a failure, otherwise, - * it chooses every nth value (approximately). - */ + * Method to return a random sampling function. + * + * @param n this is the sample factor: approximately one in every n successful results will form part of the result. + * @return a Try[Any] => Boolean function which is always yields false if its input is a failure, otherwise, + * it chooses every nth value (approximately). + */ def sampler(n: Int): Try[Any] => Boolean = { case Success(_) => r.nextInt(n) == 0 case _ => false } /** - * a function which always evaluates as true, regardless of the successfulness of the input. - */ + * a function which always evaluates as true, regardless of the successfulness of the input. + */ val includeAll: Try[Any] => Boolean = _ => true } @@ -160,15 +166,15 @@ trait CopyableTableParser[Row, Input, Table] { } /** - * Class used to parse files as a Table of Seq[String]. - * That's to say, no parsing of individual (or groups of) columns. - * - * @param predicate a predicate which, if true, allows inclusion of the input row. - * @param maybeFixedHeader an optional fixed header. If None, we expect to find the header defined in the first line of the file. - * @param forgiving forcing (defaults to true). If true then an individual malformed row will not prevent subsequent rows being parsed. - */ -case class RawTableParser(override protected val predicate: Try[RawRow] => Boolean = TableParser.includeAll, maybeFixedHeader: Option[Header] = None, override val forgiving: Boolean = false, override val multiline: Boolean = false) - extends StringTableParser[Table[Seq[String]]] with CopyableTableParser[RawRow, String, Table[Seq[String]]] { + * Class used to parse files as a Table of Seq[String]. + * That's to say, no parsing of individual (or groups of) columns. + * + * @param predicate a predicate which, if true, allows inclusion of the input row. + * @param maybeFixedHeader an optional fixed header. If None, we expect to find the header defined in the first line of the file. + * @param forgiving forcing (defaults to true). If true then an individual malformed row will not prevent subsequent rows being parsed. + */ +case class RawTableParser(override protected val predicate: Try[RawRow] => Boolean = TableParser.includeAll, maybeFixedHeader: Option[Header] = None, override val forgiving: Boolean = false, override val multiline: Boolean = false, override val headerRowsToRead: Int = 1) + extends StringTableParser[Table[Seq[String]]] with CopyableTableParser[RawRow, String, Table[Seq[String]]] { type Row = RawRow @@ -179,35 +185,38 @@ case class RawTableParser(override protected val predicate: Try[RawRow] => Boole // CONSIDER why do we have a concrete Table type mentioned here? protected def builder(rows: Iterable[Row], header: Header): Table[Row] = HeadedTable(rows, header) + // TEST def setHeader(header: Header): RawTableParser = copy(maybeFixedHeader = Some(header)) + // TEST def setForgiving(b: Boolean): RawTableParser = copy(forgiving = b) def setMultiline(b: Boolean): RawTableParser = copy(multiline = b) def setPredicate(p: Try[RawRow] => Boolean): RawTableParser = copy(predicate = p) + // TEST def setRowParser(rp: RowParser[RawRow, String]): RawTableParser = new RawTableParser(predicate, maybeFixedHeader, forgiving, multiline) { override val rowParser: RowParser[Row, String] = rp } } /** - * Case class to define a StringTableParser that assumes a header to be found in the input file. - * This class attempts to provide as much built-in functionality as possible. - * - * This class assumes that the names of the columns are in the first line. - * This class implements builder with a HeadedTable object. - * This class uses StandardRowParser of its rowParser. - * - * @param maybeFixedHeader None => requires that the data source has a header row. - * Some(h) => specifies that the header is to be taken from h. - * NOTE: that the simplest is to specify the header directly from the type X: - * @see HeadedStringTableParser#create - * @tparam X the underlying row type which must provide evidence of a CellParser and ClassTag. - */ -case class HeadedStringTableParser[X: CellParser : ClassTag](maybeFixedHeader: Option[Header] = None, override val forgiving: Boolean = false) - extends StringTableParser[Table[X]] with CopyableTableParser[X, String, Table[X]] { + * Case class to define a StringTableParser that assumes a header to be found in the input file. + * This class attempts to provide as much built-in functionality as possible. + * + * This class assumes that the names of the columns are in the first line. + * This class implements builder with a HeadedTable object. + * This class uses StandardRowParser of its rowParser. + * + * @param maybeFixedHeader None => requires that the data source has a header row. + * Some(h) => specifies that the header is to be taken from h. + * NOTE: that the simplest is to specify the header directly from the type X: + * @see HeadedStringTableParser#create + * @tparam X the underlying row type which must provide evidence of a CellParser and ClassTag. + */ +case class HeadedStringTableParser[X: CellParser : ClassTag](maybeFixedHeader: Option[Header] = None, override val forgiving: Boolean = false, override val headerRowsToRead: Int = 1) + extends StringTableParser[Table[X]] with CopyableTableParser[X, String, Table[X]] { type Row = X @@ -218,18 +227,23 @@ case class HeadedStringTableParser[X: CellParser : ClassTag](maybeFixedHeader: O protected val rowParser: RowParser[X, String] = StandardRowParser[X] + // TEST def setHeader(header: Header): HeadedStringTableParser[X] = copy(maybeFixedHeader = Some(header)) + // TEST def setForgiving(b: Boolean): HeadedStringTableParser[X] = copy(forgiving = b) + // TEST def setMultiline(b: Boolean): HeadedStringTableParser[X] = new HeadedStringTableParser[X](maybeFixedHeader, forgiving) { override val multiline: Boolean = b } + // TEST def setPredicate(p: Try[X] => Boolean): HeadedStringTableParser[X] = new HeadedStringTableParser[X](maybeFixedHeader, forgiving) { override val predicate: Try[X] => Boolean = p } + // TEST def setRowParser(rp: RowParser[X, Input]): TableParser[Table[X]] = new HeadedStringTableParser[X] { override protected val rowParser: RowParser[X, String] = rp } @@ -237,65 +251,64 @@ case class HeadedStringTableParser[X: CellParser : ClassTag](maybeFixedHeader: O object HeadedStringTableParser { /** - * This create method constructs a HeadedStringTableParser with header based simply on the type X. - * In this case, the source data must have the same number of columns as X has parameters, and they must be in the - * same order. Additionally, there should not be a header row in the source data. - * - * @tparam X the underlying type. There must be evidence of CellParser[X] and ClassTag[X]. - * @return a HeadedStringTableParser[X]. - */ - def create[X: CellParser : ClassTag](forgiving: Boolean): HeadedStringTableParser[X] = HeadedStringTableParser[X](Some(Header.apply[X]()), forgiving) + * This create method constructs a HeadedStringTableParser with header based simply on the type X. + * In this case, the source data must have the same number of columns as X has parameters, and they must be in the + * same order. Additionally, there should not be a header row in the source data. + * + * @tparam X the underlying type. There must be evidence of CellParser[X] and ClassTag[X]. + * @return a HeadedStringTableParser[X]. + */ + def create[X: CellParser : ClassTag](forgiving: Boolean): HeadedStringTableParser[X] = HeadedStringTableParser[X](Some(Header.apply[X]()), forgiving, 0) } /** - * Abstract base class for implementations of TableParser[T]. - * NOTE: that Table is a parametric type and does NOT refer to the type Table defined elsewhere. - * - * @tparam Table the (parametric) Table type. - */ + * Abstract base class for implementations of TableParser[T]. + * NOTE: that Table is a parametric type and does NOT refer to the type Table defined elsewhere. + * + * @tparam Table the (parametric) Table type. + */ abstract class AbstractTableParser[Table] extends TableParser[Table] { + protected def failureHandler(ry: Try[Row]): Unit = logException[Row](ry) + /** - * Abstract method to parse a sequence of Inputs, with a given header. - * - * @param xs the sequence of Inputs, one for each row - * @param header the header to be used. - * @return a Try[Table] - */ + * Abstract method to parse a sequence of Inputs, with a given header. + * + * @param xs the sequence of Inputs, one for each row + * @param header the header to be used. + * @return a Try[Table] + */ def parseRows(xs: Iterator[Input], header: Header): Try[Table] /** - * Method to parse a table based on a sequence of Inputs. - * - * @param xs the sequence of Inputs, one for each row - * @return a Try[Table] - */ - def parse(xs: Iterator[Input]): Try[Table] = { - def separateHeaderAndRows(h: Input, t: Iterator[Input]): Try[Table] = - for (ws <- rowParser.parseHeader(h); rs <- parseRows(t, ws)) yield rs - - maybeFixedHeader match { - case Some(h) => parseRows(xs, h) - case None => - // NOTE: it is possible that we still don't really have a header encoded in the data either - if (xs.hasNext) separateHeaderAndRows(xs.next(), xs) - else Failure(ParserException("no rows to parse")) - } + * Method to parse a table based on a sequence of Inputs. + * + * @param xs the sequence of Inputs, one for each row + * @param n the number of lines that should be used as a Header. + * If n == 0 == maybeFixedHeader.empty then there is a logic error. + * @return a Try[Table] + */ + def parse(xs: Iterator[Input], n: Int = 0): Try[Table] = maybeFixedHeader match { + case Some(h) if n == 0 => parseRows(xs, h) + case None if n > 0 => + val ys = new TeeIterator(n)(xs) + for (h <- rowParser.parseHeader(ys.tee); t <- parseRows(ys, h)) yield t + case _ => Failure(ParserException(s"AbstractTableParser.parse: logic error: n=$n, maybeFixedHeader=$maybeFixedHeader")) } /** - * Common code for parsing rows. - * - * CONSIDER convert T to Input - * - * CONSIDER switch order of f - * - * @param ts a sequence of Ts. - * @param header the Header. - * @param f a curried function which transforms a (T, Int) into a function which is of type Header => Try[Row]. - * @tparam T the parametric type of the resulting Table. T corresponds to Input in the calling method, i.e. a Row. Must be Joinable. - * @return a Try of Table - */ + * Common code for parsing rows. + * + * CONSIDER convert T to Input + * + * CONSIDER switch order of f + * + * @param ts a sequence of Ts. + * @param header the Header. + * @param f a curried function which transforms a (T, Int) into a function which is of type Header => Try[Row]. + * @tparam T the parametric type of the resulting Table. T corresponds to Input in the calling method, i.e. a Row. Must be Joinable. + * @return a Try of Table + */ protected def doParseRows[T: Joinable](ts: Iterator[T], header: Header, f: ((T, Int)) => Header => Try[Row]): Try[Table] = { implicit object Z extends Joinable[(T, Int)] { private val tj: Joinable[T] = implicitly[Joinable[T]] @@ -314,7 +327,7 @@ abstract class AbstractTableParser[Table] extends TableParser[Table] { def handleFailures(rys: Iterator[Try[Row]]) = if (forgiving) { val (good, bad) = partition(rys) - bad foreach AbstractTableParser.logException[Row] + bad foreach failureHandler //AbstractTableParser.logException[Row] FP.sequence(good filter predicate) } else @@ -339,11 +352,11 @@ object AbstractTableParser { } /** - * Abstract class to extend AbstractTableParser but with Input = String. - * This is the normal situation where a file is a sequence of Strings, each representing one line. - * - * @tparam Table the table type. - */ + * Abstract class to extend AbstractTableParser but with Input = String. + * This is the normal situation where a file is a sequence of Strings, each representing one line. + * + * @tparam Table the table type. + */ abstract class StringTableParser[Table] extends AbstractTableParser[Table] { type Input = String @@ -351,11 +364,11 @@ abstract class StringTableParser[Table] extends AbstractTableParser[Table] { } /** - * Abstract class to extend AbstractTableParser but with Input = Strings - * This is the unusual situation where a file is a sequence of a sequence of Strings, each representing one value. - * - * @tparam Table the table type. - */ + * Abstract class to extend AbstractTableParser but with Input = Strings + * This is the unusual situation where a file is a sequence of a sequence of Strings, each representing one value. + * + * @tparam Table the table type. + */ abstract class StringsTableParser[Table] extends AbstractTableParser[Table] { type Input = Strings @@ -363,32 +376,32 @@ abstract class StringsTableParser[Table] extends AbstractTableParser[Table] { } /** - * TableParserHelper: abstract class to help with defining an implicit TableParser of Table[X]. - * Note that this class extends CellParser[X]. - * It is expected that this should be sub-classed by the object which is the companion object of X. - * That will make it easiest for the compiler to discover the implicit value of type TableParser of Table[X] - * - * NOTE: this class should be used for simple cases where the the data and type X match according to one of options - * for sourceHasHeaderRow. - * More complex situations can easily be handled but not using this TableParserHelper class. - * - * @param sourceHasHeaderRow true (default) if the data to be read has an explicit header row with column names that match the parameters - * of type X; - * false if there is no header row in the data AND if the data has (unnamed) columns of the same number - * and in the same order as defined by type X. - * @param forgiving true if individual rows of the source which do not parse successfully, - * are logged but otherwise do not affect the success of the overall parsing. - * @tparam X the type for which we require a TableParser[X]. - */ + * TableParserHelper: abstract class to help with defining an implicit TableParser of Table[X]. + * Note that this class extends CellParser[X]. + * It is expected that this should be sub-classed by the object which is the companion object of X. + * That will make it easiest for the compiler to discover the implicit value of type TableParser of Table[X] + * + * NOTE: this class should be used for simple cases where the the data and type X match according to one of options + * for sourceHasHeaderRow. + * More complex situations can easily be handled but not using this TableParserHelper class. + * + * @param sourceHasHeaderRow true (default) if the data to be read has an explicit header row with column names that match the parameters + * of type X; + * false if there is no header row in the data AND if the data has (unnamed) columns of the same number + * and in the same order as defined by type X. + * @param forgiving true if individual rows of the source which do not parse successfully, + * are logged but otherwise do not affect the success of the overall parsing. + * @tparam X the type for which we require a TableParser[X]. + */ abstract class TableParserHelper[X: ClassTag](sourceHasHeaderRow: Boolean = true, forgiving: Boolean = false) extends CellParsers { /** - * Abstract method which will return a CellParser[X]. - * NOTE that a typical definition will be something like cellParser2(Player.apply) where, in this case, the number - * is 2 corresponding to the number of parameters in Player. - * - * @return - */ + * Abstract method which will return a CellParser[X]. + * NOTE that a typical definition will be something like cellParser2(Player.apply) where, in this case, the number + * is 2 corresponding to the number of parameters in Player. + * + * @return + */ def cellParser: CellParser[X] implicit val xp: CellParser[X] = cellParser diff --git a/src/main/scala/com/phasmidsoftware/parse/package.scala b/src/main/scala/com/phasmidsoftware/parse/package.scala index ab21589b..8bd6c310 100644 --- a/src/main/scala/com/phasmidsoftware/parse/package.scala +++ b/src/main/scala/com/phasmidsoftware/parse/package.scala @@ -7,13 +7,13 @@ package com.phasmidsoftware package object parse { /** - * type alias for the results of parsing repetitions of String. - */ + * type alias for the results of parsing repetitions of String. + */ type StringList = List[String] /** - * type alias for parsing rows which are composed of a sequence of String. - */ + * type alias for parsing rows which are composed of a sequence of String. + */ type Strings = Seq[String] // CONSIDER moving this definition and renaming it diff --git a/src/main/scala/com/phasmidsoftware/render/CsvRenderers.scala b/src/main/scala/com/phasmidsoftware/render/CsvRenderers.scala new file mode 100644 index 00000000..b2544acc --- /dev/null +++ b/src/main/scala/com/phasmidsoftware/render/CsvRenderers.scala @@ -0,0 +1,855 @@ +/* + * Copyright (c) 2019. Phasmid Software + */ + +package com.phasmidsoftware.render + +import com.phasmidsoftware.table.{BaseCsvGenerator, CsvAttributes, CsvGenerator, CsvProductGenerator} +import java.net.URL +import scala.reflect.ClassTag + +/** + * Trait to define various renderers for rendering instance of case classes (with their various parameters), + * containers (Seq and Option), etc. to CSV output. + * + * CONSIDER a mechanism to ensure that objects involving case classes are presented in the same order as specified by the header. + */ +trait CsvRenderers { + + /** + * Method to return a CsvRenderer[ Seq[T] ]. + * + * TEST + * + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvRenderer[ Seq[T] ] + */ + def sequenceRenderer[T: CsvRenderer](implicit ca: CsvAttributes): CsvRenderer[Seq[T]] = new CsvRenderer[Seq[T]] { + + def render(ts: Seq[T], attrs: Map[String, String]): String = (ts map { t: T => implicitly[CsvRenderer[T]].render(t) }).mkString(csvAttributes.delimiter) + + val csvAttributes: CsvAttributes = ca + } + + /** + * Method to return a CsvRenderer[ Option[T] ]. + * + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvRenderer[ Option[T] ]. + */ + def optionRenderer[T: CsvRenderer](implicit ca: CsvAttributes): CsvRenderer[Option[T]] = new CsvRenderer[Option[T]] { + val csvAttributes: CsvAttributes = ca + + def render(to: Option[T], attrs: Map[String, String]): String = (to map (t => implicitly[CsvRenderer[T]].render(t))).getOrElse("") + } + + /** + * Method to return a CsvRenderer[T] which does not output a T at all, only a number of delimiters according to the value of alignment. + * + * @param alignment (defaults to 1): one more than the number of delimiters to output. + * If you are skipping a Product (such as a case class instance), then you should carefully count up how many (nested) elements to skip. + * So, for example, if you are skipping a Product with three members, you would set alignment = 3, even though you only want to output 2 delimiters. + * @tparam T the type of the parameter to the render method. + * @return a CsvRenderer[T]. + */ + def skipRenderer[T](alignment: Int = 1)(implicit ca: CsvAttributes): CsvRenderer[T] = new CsvRenderer[T] { + val csvAttributes: CsvAttributes = ca + + def render(t: T, attrs: Map[String, String]): String = ca.delimiter * (alignment - 1) + } + + /** + * Method to return a CsvRenderer[T] where T is a 1-ary Product and which is based on a function to convert a P into a T. + * + * NOTE: be careful using this particular method it only applies where T is a 1-tuple (e.g. a case class with one field -- not common). + * + * @param construct a function P => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the (single) field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ + def renderer1[P1: CsvRenderer, T <: Product : ClassTag](construct: P1 => T)(implicit csvAttributes: CsvAttributes): CsvRenderer[T] = new ProductCsvRenderer[T]() { + + protected def elements(t: T): Seq[String] = Seq( + implicitly[CsvRenderer[P1]].render(t.productElement(0).asInstanceOf[P1]) + ) + } + + /** + * Method to return a CsvRenderer[T] where T is a 2-ary Product and which is based on a function to convert a (P1,P2) into a T. + * + * @param construct a function (P1,P2) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvRenderer[T]. + */ + def renderer2[P1: CsvRenderer, P2: CsvRenderer, T <: Product : ClassTag](construct: (P1, P2) => T)(implicit csvAttributes: CsvAttributes): CsvRenderer[T] = new ProductCsvRenderer[T]() { + + protected def elements(t: T): Seq[String] = Seq( + implicitly[CsvRenderer[P1]].render(t.productElement(0).asInstanceOf[P1]) + , implicitly[CsvRenderer[P2]].render(t.productElement(1).asInstanceOf[P2]) + ) + } + + /** + * Method to return a CsvRenderer[T] where T is a 3-ary Product and which is based on a function to convert a (P1,P2,P3) into a T. + * + * @param construct a function (P1,P2,P3) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvRenderer[T]. + */ + def renderer3[P1: CsvRenderer, P2: CsvRenderer, P3: CsvRenderer, T <: Product : ClassTag](construct: (P1, P2, P3) => T)(implicit csvAttributes: CsvAttributes): CsvRenderer[T] = new ProductCsvRenderer[T]() { + + protected def elements(t: T): Seq[String] = { + Seq( + implicitly[CsvRenderer[P1]].render(t.productElement(0).asInstanceOf[P1]) + , implicitly[CsvRenderer[P2]].render(t.productElement(1).asInstanceOf[P2]) + , implicitly[CsvRenderer[P3]].render(t.productElement(2).asInstanceOf[P3]) + ) + } + } + + /** + * Method to return a CsvRenderer[T] where T is a 4-ary Product and which is based on the given "construct" function. + * + * @param construct a function (P1,P2,P3,P4) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvRenderer[T]. + */ + def renderer4[P1: CsvRenderer, P2: CsvRenderer, P3: CsvRenderer, P4: CsvRenderer, T <: Product : ClassTag](construct: (P1, P2, P3, P4) => T)(implicit csvAttributes: CsvAttributes): CsvRenderer[T] = new ProductCsvRenderer[T]() { + + protected def elements(t: T): Seq[String] = { + Seq( + implicitly[CsvRenderer[P1]].render(t.productElement(0).asInstanceOf[P1]) + , implicitly[CsvRenderer[P2]].render(t.productElement(1).asInstanceOf[P2]) + , implicitly[CsvRenderer[P3]].render(t.productElement(2).asInstanceOf[P3]) + , implicitly[CsvRenderer[P4]].render(t.productElement(3).asInstanceOf[P4]) + ) + } + } + + /** + * Method to return a CsvRenderer[T] where T is a 5-ary Product and which is based on the given "construct" function. + * + * @param construct a function (P1,P2,P3,P4,P5) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvRenderer[T]. + */ + def renderer5[P1: CsvRenderer, P2: CsvRenderer, P3: CsvRenderer, P4: CsvRenderer, P5: CsvRenderer, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5) => T)(implicit csvAttributes: CsvAttributes): CsvRenderer[T] = new ProductCsvRenderer[T]() { + + protected def elements(t: T): Seq[String] = { + Seq( + implicitly[CsvRenderer[P1]].render(t.productElement(0).asInstanceOf[P1]) + , implicitly[CsvRenderer[P2]].render(t.productElement(1).asInstanceOf[P2]) + , implicitly[CsvRenderer[P3]].render(t.productElement(2).asInstanceOf[P3]) + , implicitly[CsvRenderer[P4]].render(t.productElement(3).asInstanceOf[P4]) + , implicitly[CsvRenderer[P5]].render(t.productElement(4).asInstanceOf[P5]) + ) + } + } + + /** + * Method to return a CsvRenderer[T] where T is a 6-ary Product and which is based on the given "construct" function. + * + * @param construct a function (P1,P2,P3,P4,P5,P6) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvRenderer[T]. + */ + def renderer6[P1: CsvRenderer, P2: CsvRenderer, P3: CsvRenderer, P4: CsvRenderer, P5: CsvRenderer, P6: CsvRenderer, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5, P6) => T)(implicit csvAttributes: CsvAttributes): CsvRenderer[T] = new ProductCsvRenderer[T]() { + + protected def elements(t: T): Seq[String] = { + Seq( + implicitly[CsvRenderer[P1]].render(t.productElement(0).asInstanceOf[P1]) + , implicitly[CsvRenderer[P2]].render(t.productElement(1).asInstanceOf[P2]) + , implicitly[CsvRenderer[P3]].render(t.productElement(2).asInstanceOf[P3]) + , implicitly[CsvRenderer[P4]].render(t.productElement(3).asInstanceOf[P4]) + , implicitly[CsvRenderer[P5]].render(t.productElement(4).asInstanceOf[P5]) + , implicitly[CsvRenderer[P6]].render(t.productElement(5).asInstanceOf[P6]) + ) + } + } + + /** + * Method to return a CsvRenderer[T] where T is a 7-ary Product and which is based on the given "construct" function. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvRenderer[T]. + */ + def renderer7[P1: CsvRenderer, P2: CsvRenderer, P3: CsvRenderer, P4: CsvRenderer, P5: CsvRenderer, P6: CsvRenderer, P7: CsvRenderer, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5, P6, P7) => T)(implicit csvAttributes: CsvAttributes): CsvRenderer[T] = new ProductCsvRenderer[T]() { + + protected def elements(t: T): Seq[String] = { + Seq( + implicitly[CsvRenderer[P1]].render(t.productElement(0).asInstanceOf[P1]) + , implicitly[CsvRenderer[P2]].render(t.productElement(1).asInstanceOf[P2]) + , implicitly[CsvRenderer[P3]].render(t.productElement(2).asInstanceOf[P3]) + , implicitly[CsvRenderer[P4]].render(t.productElement(3).asInstanceOf[P4]) + , implicitly[CsvRenderer[P5]].render(t.productElement(4).asInstanceOf[P5]) + , implicitly[CsvRenderer[P6]].render(t.productElement(5).asInstanceOf[P6]) + , implicitly[CsvRenderer[P7]].render(t.productElement(6).asInstanceOf[P7]) + ) + } + } + + /** + * Method to return a CsvRenderer[T] where T is a 8-ary Product and which is based on the given "construct" function. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvRenderer[T]. + */ + def renderer8[P1: CsvRenderer, P2: CsvRenderer, P3: CsvRenderer, P4: CsvRenderer, P5: CsvRenderer, P6: CsvRenderer, P7: CsvRenderer, P8: CsvRenderer, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5, P6, P7, P8) => T)(implicit csvAttributes: CsvAttributes): CsvRenderer[T] = new ProductCsvRenderer[T]() { + + protected def elements(t: T): Seq[String] = { + Seq( + implicitly[CsvRenderer[P1]].render(t.productElement(0).asInstanceOf[P1]) + , implicitly[CsvRenderer[P2]].render(t.productElement(1).asInstanceOf[P2]) + , implicitly[CsvRenderer[P3]].render(t.productElement(2).asInstanceOf[P3]) + , implicitly[CsvRenderer[P4]].render(t.productElement(3).asInstanceOf[P4]) + , implicitly[CsvRenderer[P5]].render(t.productElement(4).asInstanceOf[P5]) + , implicitly[CsvRenderer[P6]].render(t.productElement(5).asInstanceOf[P6]) + , implicitly[CsvRenderer[P7]].render(t.productElement(6).asInstanceOf[P7]) + , implicitly[CsvRenderer[P8]].render(t.productElement(7).asInstanceOf[P8]) + ) + } + } + + /** + * Method to return a CsvRenderer[T] where T is a 9-ary Product and which is based on the given "construct" function. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvRenderer[T]. + */ + def renderer9[P1: CsvRenderer, P2: CsvRenderer, P3: CsvRenderer, P4: CsvRenderer, P5: CsvRenderer, P6: CsvRenderer, P7: CsvRenderer, P8: CsvRenderer, P9: CsvRenderer, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9) => T)(implicit csvAttributes: CsvAttributes): CsvRenderer[T] = new ProductCsvRenderer[T]() { + + protected def elements(t: T): Seq[String] = { + Seq( + implicitly[CsvRenderer[P1]].render(t.productElement(0).asInstanceOf[P1]) + , implicitly[CsvRenderer[P2]].render(t.productElement(1).asInstanceOf[P2]) + , implicitly[CsvRenderer[P3]].render(t.productElement(2).asInstanceOf[P3]) + , implicitly[CsvRenderer[P4]].render(t.productElement(3).asInstanceOf[P4]) + , implicitly[CsvRenderer[P5]].render(t.productElement(4).asInstanceOf[P5]) + , implicitly[CsvRenderer[P6]].render(t.productElement(5).asInstanceOf[P6]) + , implicitly[CsvRenderer[P7]].render(t.productElement(6).asInstanceOf[P7]) + , implicitly[CsvRenderer[P8]].render(t.productElement(7).asInstanceOf[P8]) + , implicitly[CsvRenderer[P9]].render(t.productElement(8).asInstanceOf[P9]) + ) + } + } + + /** + * Method to return a CsvRenderer[T] where T is a 10-ary Product and which is based on the given "construct" function. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam P10 the type of the tenth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvRenderer[T]. + */ + def renderer10[P1: CsvRenderer, P2: CsvRenderer, P3: CsvRenderer, P4: CsvRenderer, P5: CsvRenderer, P6: CsvRenderer, P7: CsvRenderer, P8: CsvRenderer, P9: CsvRenderer, P10: CsvRenderer, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10) => T)(implicit csvAttributes: CsvAttributes): CsvRenderer[T] = new ProductCsvRenderer[T]() { + + protected def elements(t: T): Seq[String] = { + Seq( + implicitly[CsvRenderer[P1]].render(t.productElement(0).asInstanceOf[P1]) + , implicitly[CsvRenderer[P2]].render(t.productElement(1).asInstanceOf[P2]) + , implicitly[CsvRenderer[P3]].render(t.productElement(2).asInstanceOf[P3]) + , implicitly[CsvRenderer[P4]].render(t.productElement(3).asInstanceOf[P4]) + , implicitly[CsvRenderer[P5]].render(t.productElement(4).asInstanceOf[P5]) + , implicitly[CsvRenderer[P6]].render(t.productElement(5).asInstanceOf[P6]) + , implicitly[CsvRenderer[P7]].render(t.productElement(6).asInstanceOf[P7]) + , implicitly[CsvRenderer[P8]].render(t.productElement(7).asInstanceOf[P8]) + , implicitly[CsvRenderer[P9]].render(t.productElement(8).asInstanceOf[P9]) + , implicitly[CsvRenderer[P10]].render(t.productElement(9).asInstanceOf[P10]) + ) + } + } + + /** + * Method to return a CsvRenderer[T] where T is a 11-ary Product and which is based on the given "construct" function. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam P10 the type of the tenth field of the Product type T. + * @tparam P11 the type of the eleventh field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvRenderer[T]. + */ + def renderer11[P1: CsvRenderer, P2: CsvRenderer, P3: CsvRenderer, P4: CsvRenderer, P5: CsvRenderer, P6: CsvRenderer, P7: CsvRenderer, P8: CsvRenderer, P9: CsvRenderer, P10: CsvRenderer, P11: CsvRenderer, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11) => T)(implicit csvAttributes: CsvAttributes): CsvRenderer[T] = new ProductCsvRenderer[T]() { + + protected def elements(t: T): Seq[String] = { + Seq( + implicitly[CsvRenderer[P1]].render(t.productElement(0).asInstanceOf[P1]) + , implicitly[CsvRenderer[P2]].render(t.productElement(1).asInstanceOf[P2]) + , implicitly[CsvRenderer[P3]].render(t.productElement(2).asInstanceOf[P3]) + , implicitly[CsvRenderer[P4]].render(t.productElement(3).asInstanceOf[P4]) + , implicitly[CsvRenderer[P5]].render(t.productElement(4).asInstanceOf[P5]) + , implicitly[CsvRenderer[P6]].render(t.productElement(5).asInstanceOf[P6]) + , implicitly[CsvRenderer[P7]].render(t.productElement(6).asInstanceOf[P7]) + , implicitly[CsvRenderer[P8]].render(t.productElement(7).asInstanceOf[P8]) + , implicitly[CsvRenderer[P9]].render(t.productElement(8).asInstanceOf[P9]) + , implicitly[CsvRenderer[P10]].render(t.productElement(9).asInstanceOf[P10]) + , implicitly[CsvRenderer[P11]].render(t.productElement(10).asInstanceOf[P11]) + ) + } + } + + /** + * Method to return a CsvRenderer[T] where T is a 12-ary Product and which is based on the given "construct" function. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam P10 the type of the tenth field of the Product type T. + * @tparam P11 the type of the eleventh field of the Product type T. + * @tparam P12 the type of the twelfth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvRenderer[T]. + */ + def renderer12[P1: CsvRenderer, P2: CsvRenderer, P3: CsvRenderer, P4: CsvRenderer, P5: CsvRenderer, P6: CsvRenderer, P7: CsvRenderer, P8: CsvRenderer, P9: CsvRenderer, P10: CsvRenderer, P11: CsvRenderer, P12: CsvRenderer, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12) => T)(implicit csvAttributes: CsvAttributes): CsvRenderer[T] = new ProductCsvRenderer[T]() { + + protected def elements(t: T): Seq[String] = { + Seq( + implicitly[CsvRenderer[P1]].render(t.productElement(0).asInstanceOf[P1]) + , implicitly[CsvRenderer[P2]].render(t.productElement(1).asInstanceOf[P2]) + , implicitly[CsvRenderer[P3]].render(t.productElement(2).asInstanceOf[P3]) + , implicitly[CsvRenderer[P4]].render(t.productElement(3).asInstanceOf[P4]) + , implicitly[CsvRenderer[P5]].render(t.productElement(4).asInstanceOf[P5]) + , implicitly[CsvRenderer[P6]].render(t.productElement(5).asInstanceOf[P6]) + , implicitly[CsvRenderer[P7]].render(t.productElement(6).asInstanceOf[P7]) + , implicitly[CsvRenderer[P8]].render(t.productElement(7).asInstanceOf[P8]) + , implicitly[CsvRenderer[P9]].render(t.productElement(8).asInstanceOf[P9]) + , implicitly[CsvRenderer[P10]].render(t.productElement(9).asInstanceOf[P10]) + , implicitly[CsvRenderer[P11]].render(t.productElement(10).asInstanceOf[P11]) + , implicitly[CsvRenderer[P12]].render(t.productElement(11).asInstanceOf[P12]) + ) + } + } +} + +abstract class ProductCsvRenderer[T <: Product : ClassTag](implicit c: CsvAttributes) extends CsvRenderer[T] { + protected def elements(t: T): Seq[String] + + val csvAttributes: CsvAttributes = c + + def render(t: T, attrs: Map[String, String]): String = elements(t) mkString csvAttributes.delimiter +} + +object CsvRenderers { + abstract class StandardCsvRenderer[T] extends CsvRenderer[T] { + val csvAttributes: CsvAttributes = implicitly[CsvAttributes] + + def render(t: T, attrs: Map[String, String]): String = t.toString + } + + implicit object CsvRendererBoolean extends StandardCsvRenderer[Boolean] + + implicit object CsvRendererInt extends StandardCsvRenderer[Int] + + implicit object CsvRendererLong extends StandardCsvRenderer[Long] + + implicit object CsvRendererDouble extends StandardCsvRenderer[Double] + + implicit object CsvRendererString extends StandardCsvRenderer[String] + + implicit object CsvRendererURL extends StandardCsvRenderer[URL] +} + +trait CsvGenerators { + + /** + * Method to return a CsvGenerator[ Seq[T] ]. + * + * TEST + * + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvGenerator[ Seq[T] ] + */ + def sequenceGenerator[T](implicit ca: CsvAttributes): CsvGenerator[Seq[T]] = new BaseCsvGenerator[Seq[T]] + + /** + * Method to return a CsvGenerator[ Option[T] ]. + * + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvGenerator[ Option[T] ]. + */ + def optionGenerator[T](implicit ca: CsvAttributes): CsvGenerator[Option[T]] = new BaseCsvGenerator[Option[T]] + + /** + * Method to return a CsvGenerator[T] which does not output a column header for at all. + * + * @tparam T the type of the column objects. + * @return a CsvGenerator[T]. + */ + def skipGenerator[T](implicit ca: CsvAttributes): CsvGenerator[T] = new CsvProductGenerator[T] { + val csvAttributes: CsvAttributes = ca + + override def toColumnName(po: Option[String], name: String): String = "" + + // TEST (not actually used). + def toColumnNames(po: Option[String], no: Option[String]): String = "" + } + + /** + * Method to return a CsvGenerator[T] where T is a 1-ary Product and which is based on a function to convert a P into a T. + * + * NOTE: be careful using this particular method it only applies where T is a 1-tuple (e.g. a case class with one field -- not common). + * + * @param construct a function P => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the (single) field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvGenerator[T]. + */ + def generator1[P1: CsvGenerator, T <: Product : ClassTag](construct: P1 => T)(implicit c: CsvAttributes): CsvGenerator[T] = new BaseCsvGenerator[T]() with CsvProductGenerator[T] { + private val Array(p1) = fieldNames + + def toColumnNames(po: Option[String], no: Option[String]): String = Seq( + implicitly[CsvGenerator[P1]].toColumnName(merge(po, no), p1) + ) mkString c.delimiter + } + + /** + * Method to return a CsvGenerator[T] where T is a 2-ary Product and which is based on a function to convert a (P1,P2) into a T. + * + * @param construct a function (P1,P2) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvGenerator[T]. + */ + def generator2[P1: CsvGenerator, P2: CsvGenerator, T <: Product : ClassTag](construct: (P1, P2) => T)(implicit c: CsvAttributes): CsvProductGenerator[T] = new BaseCsvGenerator[T]() with CsvProductGenerator[T] { + private val Array(p1, p2) = fieldNames + + def toColumnNames(po: Option[String], no: Option[String]): String = { + val wo = merge(po, no) + Seq( + implicitly[CsvGenerator[P1]].toColumnName(wo, p1) + , implicitly[CsvGenerator[P2]].toColumnName(wo, p2) + ) mkString c.delimiter + } + } + + /** + * Method to return a CsvGenerator[T] where T is a 3-ary Product and which is based on a function to convert a (P1,P2,P3) into a T. + * + * @param construct a function (P1,P2,P3) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvGenerator[T]. + */ + def generator3[P1: CsvGenerator, P2: CsvGenerator, P3: CsvGenerator, T <: Product : ClassTag](construct: (P1, P2, P3) => T)(implicit c: CsvAttributes): CsvProductGenerator[T] = new BaseCsvGenerator[T]() with CsvProductGenerator[T] { + private val Array(p1, p2, p3) = fieldNames + + def toColumnNames(po: Option[String], no: Option[String]): String = { + val wo = merge(po, no) + Seq( + implicitly[CsvGenerator[P1]].toColumnName(wo, p1) + , implicitly[CsvGenerator[P2]].toColumnName(wo, p2) + , implicitly[CsvGenerator[P3]].toColumnName(wo, p3) + ) mkString c.delimiter + } + } + + /** + * Method to return a CsvGenerator[T] where T is a 4-ary Product and which is based on a function to convert a (P1,P2,P3,P4) into a T. + * + * @param construct a function (P1,P2,P3,P4) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the third field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvGenerator[T]. + */ + def generator4[P1: CsvGenerator, P2: CsvGenerator, P3: CsvGenerator, P4: CsvGenerator, T <: Product : ClassTag](construct: (P1, P2, P3, P4) => T)(implicit c: CsvAttributes): CsvProductGenerator[T] = new BaseCsvGenerator[T]() with CsvProductGenerator[T] { + private val Array(p1, p2, p3, p4) = fieldNames + + def toColumnNames(po: Option[String], no: Option[String]): String = { + val wo = merge(po, no) + Seq( + implicitly[CsvGenerator[P1]].toColumnName(wo, p1) + , implicitly[CsvGenerator[P2]].toColumnName(wo, p2) + , implicitly[CsvGenerator[P3]].toColumnName(wo, p3) + , implicitly[CsvGenerator[P4]].toColumnName(wo, p4) + ) mkString c.delimiter + } + } + + /** + * Method to return a CsvGenerator[T] where T is a 5-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvGenerator[T]. + */ + def generator5[P1: CsvGenerator, P2: CsvGenerator, P3: CsvGenerator, P4: CsvGenerator, P5: CsvGenerator, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5) => T)(implicit c: CsvAttributes): CsvProductGenerator[T] = new BaseCsvGenerator[T]() with CsvProductGenerator[T] { + private val Array(p1, p2, p3, p4, p5) = fieldNames + + def toColumnNames(po: Option[String], no: Option[String]): String = { + val wo = merge(po, no) + Seq( + implicitly[CsvGenerator[P1]].toColumnName(wo, p1) + , implicitly[CsvGenerator[P2]].toColumnName(wo, p2) + , implicitly[CsvGenerator[P3]].toColumnName(wo, p3) + , implicitly[CsvGenerator[P4]].toColumnName(wo, p4) + , implicitly[CsvGenerator[P5]].toColumnName(wo, p5) + ) mkString c.delimiter + } + } + + /** + * Method to return a CsvGenerator[T] where T is a 6-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvGenerator[T]. + */ + def generator6[P1: CsvGenerator, P2: CsvGenerator, P3: CsvGenerator, P4: CsvGenerator, P5: CsvGenerator, P6: CsvGenerator, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5, P6) => T)(implicit c: CsvAttributes): CsvProductGenerator[T] = new BaseCsvGenerator[T]() with CsvProductGenerator[T] { + private val Array(p1, p2, p3, p4, p5, p6) = fieldNames + + def toColumnNames(po: Option[String], no: Option[String]): String = { + val wo = merge(po, no) + Seq( + implicitly[CsvGenerator[P1]].toColumnName(wo, p1) + , implicitly[CsvGenerator[P2]].toColumnName(wo, p2) + , implicitly[CsvGenerator[P3]].toColumnName(wo, p3) + , implicitly[CsvGenerator[P4]].toColumnName(wo, p4) + , implicitly[CsvGenerator[P5]].toColumnName(wo, p5) + , implicitly[CsvGenerator[P6]].toColumnName(wo, p6) + ) mkString c.delimiter + } + } + + /** + * Method to return a CsvGenerator[T] where T is a 7-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvGenerator[T]. + */ + def generator7[P1: CsvGenerator, P2: CsvGenerator, P3: CsvGenerator, P4: CsvGenerator, P5: CsvGenerator, P6: CsvGenerator, P7: CsvGenerator, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5, P6, P7) => T)(implicit c: CsvAttributes): CsvProductGenerator[T] = new BaseCsvGenerator[T]() with CsvProductGenerator[T] { + private val Array(p1, p2, p3, p4, p5, p6, p7) = fieldNames + + def toColumnNames(po: Option[String], no: Option[String]): String = { + val wo = merge(po, no) + Seq( + implicitly[CsvGenerator[P1]].toColumnName(wo, p1) + , implicitly[CsvGenerator[P2]].toColumnName(wo, p2) + , implicitly[CsvGenerator[P3]].toColumnName(wo, p3) + , implicitly[CsvGenerator[P4]].toColumnName(wo, p4) + , implicitly[CsvGenerator[P5]].toColumnName(wo, p5) + , implicitly[CsvGenerator[P6]].toColumnName(wo, p6) + , implicitly[CsvGenerator[P7]].toColumnName(wo, p7) + ) mkString c.delimiter + } + } + + /** + * Method to return a CsvGenerator[T] where T is a 8-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvGenerator[T]. + */ + def generator8[P1: CsvGenerator, P2: CsvGenerator, P3: CsvGenerator, P4: CsvGenerator, P5: CsvGenerator, P6: CsvGenerator, P7: CsvGenerator, P8: CsvGenerator, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5, P6, P7, P8) => T)(implicit c: CsvAttributes): CsvProductGenerator[T] = new BaseCsvGenerator[T]() with CsvProductGenerator[T] { + private val Array(p1, p2, p3, p4, p5, p6, p7, p8) = fieldNames + + def toColumnNames(po: Option[String], no: Option[String]): String = { + val wo = merge(po, no) + Seq( + implicitly[CsvGenerator[P1]].toColumnName(wo, p1) + , implicitly[CsvGenerator[P2]].toColumnName(wo, p2) + , implicitly[CsvGenerator[P3]].toColumnName(wo, p3) + , implicitly[CsvGenerator[P4]].toColumnName(wo, p4) + , implicitly[CsvGenerator[P5]].toColumnName(wo, p5) + , implicitly[CsvGenerator[P6]].toColumnName(wo, p6) + , implicitly[CsvGenerator[P7]].toColumnName(wo, p7) + , implicitly[CsvGenerator[P8]].toColumnName(wo, p8) + ) mkString c.delimiter + } + } + + /** + * Method to return a CsvGenerator[T] where T is a 9-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvGenerator[T]. + */ + def generator9[P1: CsvGenerator, P2: CsvGenerator, P3: CsvGenerator, P4: CsvGenerator, P5: CsvGenerator, P6: CsvGenerator, P7: CsvGenerator, P8: CsvGenerator, P9: CsvGenerator, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9) => T)(implicit c: CsvAttributes): CsvProductGenerator[T] = new BaseCsvGenerator[T]() with CsvProductGenerator[T] { + private val Array(p1, p2, p3, p4, p5, p6, p7, p8, p9) = fieldNames + + def toColumnNames(po: Option[String], no: Option[String]): String = { + val wo = merge(po, no) + Seq( + implicitly[CsvGenerator[P1]].toColumnName(wo, p1) + , implicitly[CsvGenerator[P2]].toColumnName(wo, p2) + , implicitly[CsvGenerator[P3]].toColumnName(wo, p3) + , implicitly[CsvGenerator[P4]].toColumnName(wo, p4) + , implicitly[CsvGenerator[P5]].toColumnName(wo, p5) + , implicitly[CsvGenerator[P6]].toColumnName(wo, p6) + , implicitly[CsvGenerator[P7]].toColumnName(wo, p7) + , implicitly[CsvGenerator[P8]].toColumnName(wo, p8) + , implicitly[CsvGenerator[P9]].toColumnName(wo, p9) + ) mkString c.delimiter + } + } + + /** + * Method to return a CsvGenerator[T] where T is a 10-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam P10 the type of the tenth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvGenerator[T]. + */ + def generator10[P1: CsvGenerator, P2: CsvGenerator, P3: CsvGenerator, P4: CsvGenerator, P5: CsvGenerator, P6: CsvGenerator, P7: CsvGenerator, P8: CsvGenerator, P9: CsvGenerator, P10: CsvGenerator, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10) => T)(implicit c: CsvAttributes): CsvProductGenerator[T] = new BaseCsvGenerator[T]() with CsvProductGenerator[T] { + private val Array(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10) = fieldNames + + def toColumnNames(po: Option[String], no: Option[String]): String = { + val wo = merge(po, no) + Seq( + implicitly[CsvGenerator[P1]].toColumnName(wo, p1) + , implicitly[CsvGenerator[P2]].toColumnName(wo, p2) + , implicitly[CsvGenerator[P3]].toColumnName(wo, p3) + , implicitly[CsvGenerator[P4]].toColumnName(wo, p4) + , implicitly[CsvGenerator[P5]].toColumnName(wo, p5) + , implicitly[CsvGenerator[P6]].toColumnName(wo, p6) + , implicitly[CsvGenerator[P7]].toColumnName(wo, p7) + , implicitly[CsvGenerator[P8]].toColumnName(wo, p8) + , implicitly[CsvGenerator[P9]].toColumnName(wo, p9) + , implicitly[CsvGenerator[P10]].toColumnName(wo, p10) + ) mkString c.delimiter + } + } + + /** + * Method to return a CsvGenerator[T] where T is a 11-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam P10 the type of the tenth field of the Product type T. + * @tparam P11 the type of the eleventh field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvGenerator[T]. + */ + def generator11[P1: CsvGenerator, P2: CsvGenerator, P3: CsvGenerator, P4: CsvGenerator, P5: CsvGenerator, P6: CsvGenerator, P7: CsvGenerator, P8: CsvGenerator, P9: CsvGenerator, P10: CsvGenerator, P11: CsvGenerator, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11) => T)(implicit c: CsvAttributes): CsvProductGenerator[T] = new BaseCsvGenerator[T]() with CsvProductGenerator[T] { + private val Array(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11) = fieldNames + + def toColumnNames(po: Option[String], no: Option[String]): String = { + val wo = merge(po, no) + Seq( + implicitly[CsvGenerator[P1]].toColumnName(wo, p1) + , implicitly[CsvGenerator[P2]].toColumnName(wo, p2) + , implicitly[CsvGenerator[P3]].toColumnName(wo, p3) + , implicitly[CsvGenerator[P4]].toColumnName(wo, p4) + , implicitly[CsvGenerator[P5]].toColumnName(wo, p5) + , implicitly[CsvGenerator[P6]].toColumnName(wo, p6) + , implicitly[CsvGenerator[P7]].toColumnName(wo, p7) + , implicitly[CsvGenerator[P8]].toColumnName(wo, p8) + , implicitly[CsvGenerator[P9]].toColumnName(wo, p9) + , implicitly[CsvGenerator[P10]].toColumnName(wo, p10) + , implicitly[CsvGenerator[P11]].toColumnName(wo, p11) + ) mkString c.delimiter + } + } + + /** + * Method to return a CsvGenerator[T] where T is a 12-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam P10 the type of the tenth field of the Product type T. + * @tparam P11 the type of the eleventh field of the Product type T. + * @tparam P12 the type of the twelfth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a CsvGenerator[T]. + */ + def generator12[P1: CsvGenerator, P2: CsvGenerator, P3: CsvGenerator, P4: CsvGenerator, P5: CsvGenerator, P6: CsvGenerator, P7: CsvGenerator, P8: CsvGenerator, P9: CsvGenerator, P10: CsvGenerator, P11: CsvGenerator, P12: CsvGenerator, T <: Product : ClassTag](construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12) => T)(implicit c: CsvAttributes): CsvProductGenerator[T] = new BaseCsvGenerator[T]() with CsvProductGenerator[T] { + private val Array(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12) = fieldNames + + def toColumnNames(po: Option[String], no: Option[String]): String = { + val wo = merge(po, no) + Seq( + implicitly[CsvGenerator[P1]].toColumnName(wo, p1) + , implicitly[CsvGenerator[P2]].toColumnName(wo, p2) + , implicitly[CsvGenerator[P3]].toColumnName(wo, p3) + , implicitly[CsvGenerator[P4]].toColumnName(wo, p4) + , implicitly[CsvGenerator[P5]].toColumnName(wo, p5) + , implicitly[CsvGenerator[P6]].toColumnName(wo, p6) + , implicitly[CsvGenerator[P7]].toColumnName(wo, p7) + , implicitly[CsvGenerator[P8]].toColumnName(wo, p8) + , implicitly[CsvGenerator[P9]].toColumnName(wo, p9) + , implicitly[CsvGenerator[P10]].toColumnName(wo, p10) + , implicitly[CsvGenerator[P11]].toColumnName(wo, p11) + , implicitly[CsvGenerator[P12]].toColumnName(wo, p12) + ) mkString c.delimiter + } + } +} + +object CsvGenerators { + implicit object CsvGeneratorBoolean extends BaseCsvGenerator[Boolean] + + implicit object CsvGeneratorInt extends BaseCsvGenerator[Int] + + implicit object CsvGeneratorLong extends BaseCsvGenerator[Long] + + implicit object CsvGeneratorDouble extends BaseCsvGenerator[Double] + + implicit object CsvGeneratorString extends BaseCsvGenerator[String] + + implicit object CsvGeneratorURL extends BaseCsvGenerator[URL] +} diff --git a/src/main/scala/com/phasmidsoftware/render/HierarchicalRenderers.scala b/src/main/scala/com/phasmidsoftware/render/HierarchicalRenderers.scala index e923c1c4..aa159acc 100644 --- a/src/main/scala/com/phasmidsoftware/render/HierarchicalRenderers.scala +++ b/src/main/scala/com/phasmidsoftware/render/HierarchicalRenderers.scala @@ -6,60 +6,59 @@ package com.phasmidsoftware.render import com.phasmidsoftware.table.{Header, Indexed} import com.phasmidsoftware.util.Reflection - import scala.reflect.ClassTag /** - * Trait to define various renderers for rendering instance of case classes (with their various parameters), - * containers (Seq and Option), etc. to hierarchical output elements such as XML or HTML. - * - */ + * Trait to define various renderers for rendering instance of case classes (with their various parameters), + * containers (Seq and Option), etc. to hierarchical output elements such as XML or HTML. + * + */ trait HierarchicalRenderers { /** - * Method to return a HierarchicalRenderer[T]. - * You do not need to define such a renderer if you can use the default style. - * - * @param style the style of the resulting renderer. - * @param attrs a set of base attributes which are explicitly set for this HierarchicalRenderer; - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[T]. - */ + * Method to return a HierarchicalRenderer[T]. + * You do not need to define such a renderer if you can use the default style. + * + * @param style the style of the resulting renderer. + * @param attrs a set of base attributes which are explicitly set for this HierarchicalRenderer; + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ def renderer[T: HierarchicalRenderer](style: String, attrs: Map[String, String] = Map()): HierarchicalRenderer[T] = new TaggedHierarchicalRenderer[T](style, attrs) {} /** - * Method to return a HierarchicalRenderer[T] where you wish to explicitly define a conversion o a T into a String. - * - * TEST - * - * @param style the style of the resulting renderer. - * @param attrs a set of base attributes which are explicitly set for this HierarchicalRenderer; - * @param f the rendering function to transform a T into a String (overrides the default asString method). - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[T]. - */ + * Method to return a HierarchicalRenderer[T] where you wish to explicitly define a conversion o a T into a String. + * + * TEST + * + * @param style the style of the resulting renderer. + * @param attrs a set of base attributes which are explicitly set for this HierarchicalRenderer; + * @param f the rendering function to transform a T into a String (overrides the default asString method). + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ def rendererExplicit[T: HierarchicalRenderer](style: String, attrs: Map[String, String] = Map())(f: T => String): HierarchicalRenderer[T] = new TaggedHierarchicalRenderer[T](style, attrs) { override def asString(t: T): String = f(t) } /** - * Method to return a HierarchicalRenderer[Header]. - * - * CONSIDER using sequenceRenderer - * - * @param style the style for the header (e.g. "th" for an HTML table). - * @param attrs the attributes. - * @return a HierarchicalRenderer[ Seq[String] ] - */ + * Method to return a HierarchicalRenderer[Header]. + * + * CONSIDER using sequenceRenderer + * + * @param style the style for the header (e.g. "th" for an HTML table). + * @param attrs the attributes. + * @return a HierarchicalRenderer[ Seq[String] ] + */ def headerRenderer(style: String, attrs: Map[String, String] = Map(), sequenced: Boolean)(renderer: HierarchicalRenderer[String]): HierarchicalRenderer[Header] = new TaggedHierarchicalRenderer[Header](style, attrs) { /** - * CONSIDER should we define a different sub-class of HierarchicalRenderer for this case? - * - * @param h the Header. - * @param attrs a map of attributes for this kind of output. - * @return an instance of type String. - */ + * CONSIDER should we define a different sub-class of HierarchicalRenderer for this case? + * + * @param h the Header. + * @param attrs a map of attributes for this kind of output. + * @return an instance of type String. + */ override def render(h: Header, attrs: Map[String, String]): Node = Node(style, attrs, headerElements(h).map((t: String) => renderer.render(t))) private def headerElements(h: Header): Seq[String] = { @@ -70,31 +69,31 @@ trait HierarchicalRenderers { /** - * Method to return a HierarchicalRenderer[ Seq[T] ]. - * NOTE: there are no identifiers generated with this HierarchicalRenderer. - * - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[ Seq[T] ] - */ + * Method to return a HierarchicalRenderer[ Seq[T] ]. + * NOTE: there are no identifiers generated with this HierarchicalRenderer. + * + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[ Seq[T] ] + */ def sequenceRenderer[T: HierarchicalRenderer](style: String, attrs: Map[String, String] = Map()): HierarchicalRenderer[Seq[T]] = new TaggedHierarchicalRenderer[Seq[T]](style, attrs) { override def render(ts: Seq[T], attrs: Map[String, String]): Node = Node(style, attrs, ts map (implicitly[HierarchicalRenderer[T]].render(_))) } /** - * - * @param overallStyle the style of the Node which will be created by this HierarchicalRenderer. - * @param indexStyle the style of the Index node which will form part of the Node created by this HierarchicalRenderer. - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[ Indexed[T] ]. - */ + * + * @param overallStyle the style of the Node which will be created by this HierarchicalRenderer. + * @param indexStyle the style of the Index node which will form part of the Node created by this HierarchicalRenderer. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[ Indexed[T] ]. + */ def indexedRenderer[T: HierarchicalRenderer](overallStyle: String, indexStyle: String): HierarchicalRenderer[Indexed[T]] = new HierarchicalRenderer[Indexed[T]] { /** - * Render an instance of Indexed[T] as a U. - * - * @param ti the input parameter, i.e. the object to be rendered. - * @param attrs a map of attributes for this value of U. - * @return a new instance of U. - */ + * Render an instance of Indexed[T] as a U. + * + * @param ti the input parameter, i.e. the object to be rendered. + * @param attrs a map of attributes for this value of U. + * @return a new instance of U. + */ override def render(ti: Indexed[T], attrs: Map[String, String]): Node = { val sequence = new TaggedHierarchicalRenderer[Int](indexStyle) {}.render(ti.i) val value = implicitly[HierarchicalRenderer[T]].render(ti.t) @@ -102,18 +101,18 @@ trait HierarchicalRenderers { } /** - * Defines the default style for type T. - */ + * Defines the default style for type T. + */ val style: String = overallStyle } /** - * Method to return a HierarchicalRenderer[ Option[T] ]. - * NOTE: there are no identifiers generated with this HierarchicalRenderer. - * - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[ Option[T] ]. - */ + * Method to return a HierarchicalRenderer[ Option[T] ]. + * NOTE: there are no identifiers generated with this HierarchicalRenderer. + * + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[ Option[T] ]. + */ def optionRenderer[T: HierarchicalRenderer](style: String, attrs: Map[String, String] = Map()): HierarchicalRenderer[Option[T]] = new TaggedHierarchicalRenderer[Option[T]](style, attrs) { override def render(to: Option[T], attrs: Map[String, String]): Node = to match { case Some(t) => Node(style, attrs, Seq(implicitly[HierarchicalRenderer[T]].render(t))) @@ -122,18 +121,18 @@ trait HierarchicalRenderers { } /** - * Method to return a HierarchicalRenderer[T] where T is a 1-ary Product and which is based on a function to convert a P into a T. - * - * NOTE: be careful using this particular method it only applies where T is a 1-tuple (e.g. a case class with one field -- not common). - * It probably shouldn't ever be used in practice. It can cause strange initialization errors! - * This note may be irrelevant now that we have overridden convertString to fix issue #1. - * - * @param construct a function P => T, usually the apply method of a case class. - * The sole purpose of this function is for type inference--it is never actually invoked. - * @tparam P1 the type of the (single) field of the Product type T. - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[T]. - */ + * Method to return a HierarchicalRenderer[T] where T is a 1-ary Product and which is based on a function to convert a P into a T. + * + * NOTE: be careful using this particular method it only applies where T is a 1-tuple (e.g. a case class with one field -- not common). + * It probably shouldn't ever be used in practice. It can cause strange initialization errors! + * This note may be irrelevant now that we have overridden convertString to fix issue #1. + * + * @param construct a function P => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the (single) field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ def renderer1[P1: HierarchicalRenderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: P1 => T): HierarchicalRenderer[T] = new ProductHierarchicalRenderer[T](style, attrs) { protected def nodes(t: T): Seq[Node] = { @@ -145,15 +144,15 @@ trait HierarchicalRenderers { } /** - * Method to return a HierarchicalRenderer[T] where T is a 2-ary Product and which is based on a function to convert a (P1,P2) into a T. - * - * @param construct a function (P1,P2) => T, usually the apply method of a case class. - * The sole purpose of this function is for type inference--it is never actually invoked. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[T]. - */ + * Method to return a HierarchicalRenderer[T] where T is a 2-ary Product and which is based on a function to convert a (P1,P2) into a T. + * + * @param construct a function (P1,P2) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ def renderer2[P1: HierarchicalRenderer, P2: HierarchicalRenderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2) => T): HierarchicalRenderer[T] = new ProductHierarchicalRenderer[T](style, attrs) { protected def nodes(t: T): Seq[Node] = { @@ -166,16 +165,16 @@ trait HierarchicalRenderers { } /** - * Method to return a HierarchicalRenderer[T] where T is a 3-ary Product and which is based on a function to convert a (P1,P2,P3) into a T. - * - * @param construct a function (P1,P2,P3) => T, usually the apply method of a case class. - * The sole purpose of this function is for type inference--it is never actually invoked. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the third field of the Product type T. - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[T]. - */ + * Method to return a HierarchicalRenderer[T] where T is a 3-ary Product and which is based on a function to convert a (P1,P2,P3) into a T. + * + * @param construct a function (P1,P2,P3) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the third field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ def renderer3[P1: HierarchicalRenderer, P2: HierarchicalRenderer, P3: HierarchicalRenderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3) => T): HierarchicalRenderer[T] = new ProductHierarchicalRenderer[T](style, attrs) { protected def nodes(t: T): Seq[Node] = { @@ -189,17 +188,17 @@ trait HierarchicalRenderers { } /** - * Method to return a HierarchicalRenderer[T] where T is a 4-ary Product and which is based on a function to convert a (P1,P2,P3,P4) into a T. - * - * @param construct a function (P1,P2,P3,P4) => T, usually the apply method of a case class. - * The sole purpose of this function is for type inference--it is never actually invoked. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[T]. - */ + * Method to return a HierarchicalRenderer[T] where T is a 4-ary Product and which is based on a function to convert a (P1,P2,P3,P4) into a T. + * + * @param construct a function (P1,P2,P3,P4) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the second field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ def renderer4[P1: HierarchicalRenderer, P2: HierarchicalRenderer, P3: HierarchicalRenderer, P4: HierarchicalRenderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4) => T): HierarchicalRenderer[T] = new ProductHierarchicalRenderer[T](style, attrs) { protected def nodes(t: T): Seq[Node] = { @@ -214,18 +213,18 @@ trait HierarchicalRenderers { } /** - * Method to return a HierarchicalRenderer[T] where T is a 5-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5) => T, usually the apply method of a case class. - * The sole purpose of this function is for type inference--it is never actually invoked. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[T]. - */ + * Method to return a HierarchicalRenderer[T] where T is a 5-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the second field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ def renderer5[P1: HierarchicalRenderer, P2: HierarchicalRenderer, P3: HierarchicalRenderer, P4: HierarchicalRenderer, P5: HierarchicalRenderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5) => T): HierarchicalRenderer[T] = new ProductHierarchicalRenderer[T](style, attrs) { protected def nodes(t: T): Seq[Node] = { @@ -241,19 +240,19 @@ trait HierarchicalRenderers { } /** - * Method to return a HierarchicalRenderer[T] where T is a 6-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5,P6) => T, usually the apply method of a case class. - * The sole purpose of this function is for type inference--it is never actually invoked. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam P6 the type of the sixth field of the Product type T. - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[T]. - */ + * Method to return a HierarchicalRenderer[T] where T is a 6-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the second field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ def renderer6[P1: HierarchicalRenderer, P2: HierarchicalRenderer, P3: HierarchicalRenderer, P4: HierarchicalRenderer, P5: HierarchicalRenderer, P6: HierarchicalRenderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5, P6) => T): HierarchicalRenderer[T] = new ProductHierarchicalRenderer[T](style, attrs) { protected def nodes(t: T): Seq[Node] = { val Array(p1, p2, p3, p4, p5, p6) = Reflection.extractFieldNames(implicitly[ClassTag[T]]) @@ -269,20 +268,20 @@ trait HierarchicalRenderers { } /** - * Method to return a HierarchicalRenderer[T] where T is a 7-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5,P6,P7) => T, usually the apply method of a case class. - * The sole purpose of this function is for type inference--it is never actually invoked. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam P6 the type of the sixth field of the Product type T. - * @tparam P7 the type of the seventh field of the Product type T. - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[T]. - */ + * Method to return a HierarchicalRenderer[T] where T is a 7-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the second field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ def renderer7[P1: HierarchicalRenderer, P2: HierarchicalRenderer, P3: HierarchicalRenderer, P4: HierarchicalRenderer, P5: HierarchicalRenderer, P6: HierarchicalRenderer, P7: HierarchicalRenderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5, P6, P7) => T): HierarchicalRenderer[T] = new ProductHierarchicalRenderer[T](style, attrs) { protected def nodes(t: T): Seq[Node] = { @@ -300,21 +299,21 @@ trait HierarchicalRenderers { } /** - * Method to return a HierarchicalRenderer[T] where T is a 8-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8) => T, usually the apply method of a case class. - * The sole purpose of this function is for type inference--it is never actually invoked. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam P6 the type of the sixth field of the Product type T. - * @tparam P7 the type of the seventh field of the Product type T. - * @tparam P8 the type of the eighth field of the Product type T. - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[T]. - */ + * Method to return a HierarchicalRenderer[T] where T is a 8-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the second field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ def renderer8[P1: HierarchicalRenderer, P2: HierarchicalRenderer, P3: HierarchicalRenderer, P4: HierarchicalRenderer, P5: HierarchicalRenderer, P6: HierarchicalRenderer, P7: HierarchicalRenderer, P8: HierarchicalRenderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5, P6, P7, P8) => T): HierarchicalRenderer[T] = new ProductHierarchicalRenderer[T](style, attrs) { protected def nodes(t: T): Seq[Node] = { @@ -333,22 +332,22 @@ trait HierarchicalRenderers { } /** - * Method to return a HierarchicalRenderer[T] where T is a 9-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9) => T, usually the apply method of a case class. - * The sole purpose of this function is for type inference--it is never actually invoked. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam P6 the type of the sixth field of the Product type T. - * @tparam P7 the type of the seventh field of the Product type T. - * @tparam P8 the type of the eighth field of the Product type T. - * @tparam P9 the type of the ninth field of the Product type T. - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[T]. - */ + * Method to return a HierarchicalRenderer[T] where T is a 9-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the second field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ def renderer9[P1: HierarchicalRenderer, P2: HierarchicalRenderer, P3: HierarchicalRenderer, P4: HierarchicalRenderer, P5: HierarchicalRenderer, P6: HierarchicalRenderer, P7: HierarchicalRenderer, P8: HierarchicalRenderer, P9: HierarchicalRenderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9) => T): HierarchicalRenderer[T] = new ProductHierarchicalRenderer[T](style, attrs) { protected def nodes(t: T): Seq[Node] = { @@ -368,23 +367,23 @@ trait HierarchicalRenderers { } /** - * Method to return a HierarchicalRenderer[T] where T is a 10-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10) => T, usually the apply method of a case class. - * The sole purpose of this function is for type inference--it is never actually invoked. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam P6 the type of the sixth field of the Product type T. - * @tparam P7 the type of the seventh field of the Product type T. - * @tparam P8 the type of the eighth field of the Product type T. - * @tparam P9 the type of the ninth field of the Product type T. - * @tparam P10 the type of the tenth field of the Product type T. - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[T]. - */ + * Method to return a HierarchicalRenderer[T] where T is a 10-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the second field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam P10 the type of the tenth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ def renderer10[P1: HierarchicalRenderer, P2: HierarchicalRenderer, P3: HierarchicalRenderer, P4: HierarchicalRenderer, P5: HierarchicalRenderer, P6: HierarchicalRenderer, P7: HierarchicalRenderer, P8: HierarchicalRenderer, P9: HierarchicalRenderer, P10: HierarchicalRenderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10) => T): HierarchicalRenderer[T] = new ProductHierarchicalRenderer[T](style, attrs) { protected def nodes(t: T): Seq[Node] = { @@ -405,24 +404,24 @@ trait HierarchicalRenderers { } /** - * Method to return a HierarchicalRenderer[T] where T is a 11-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11) => T, usually the apply method of a case class. - * The sole purpose of this function is for type inference--it is never actually invoked. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam P6 the type of the sixth field of the Product type T. - * @tparam P7 the type of the seventh field of the Product type T. - * @tparam P8 the type of the eighth field of the Product type T. - * @tparam P9 the type of the ninth field of the Product type T. - * @tparam P10 the type of the tenth field of the Product type T. - * @tparam P11 the type of the eleventh field of the Product type T. - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[T]. - */ + * Method to return a HierarchicalRenderer[T] where T is a 11-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the second field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam P10 the type of the tenth field of the Product type T. + * @tparam P11 the type of the eleventh field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ def renderer11[P1: HierarchicalRenderer, P2: HierarchicalRenderer, P3: HierarchicalRenderer, P4: HierarchicalRenderer, P5: HierarchicalRenderer, P6: HierarchicalRenderer, P7: HierarchicalRenderer, P8: HierarchicalRenderer, P9: HierarchicalRenderer, P10: HierarchicalRenderer, P11: HierarchicalRenderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11) => T): HierarchicalRenderer[T] = new ProductHierarchicalRenderer[T](style, attrs) { protected def nodes(t: T): Seq[Node] = { @@ -444,25 +443,25 @@ trait HierarchicalRenderers { } /** - * Method to return a HierarchicalRenderer[T] where T is a 12-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12) into a T. - * - * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12) => T, usually the apply method of a case class. - * The sole purpose of this function is for type inference--it is never actually invoked. - * @tparam P1 the type of the first field of the Product type T. - * @tparam P2 the type of the second field of the Product type T. - * @tparam P3 the type of the second field of the Product type T. - * @tparam P4 the type of the fourth field of the Product type T. - * @tparam P5 the type of the fifth field of the Product type T. - * @tparam P6 the type of the sixth field of the Product type T. - * @tparam P7 the type of the seventh field of the Product type T. - * @tparam P8 the type of the eighth field of the Product type T. - * @tparam P9 the type of the ninth field of the Product type T. - * @tparam P10 the type of the tenth field of the Product type T. - * @tparam P11 the type of the eleventh field of the Product type T. - * @tparam P12 the type of the twelfth field of the Product type T. - * @tparam T the underlying type of the first parameter of the input to the render method. - * @return a HierarchicalRenderer[T]. - */ + * Method to return a HierarchicalRenderer[T] where T is a 12-ary Product and which is based on a function to convert a (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12) into a T. + * + * @param construct a function (P1,P2,P3,P4,P5,P6,P7,P8,P9,P10,P11,P12) => T, usually the apply method of a case class. + * The sole purpose of this function is for type inference--it is never actually invoked. + * @tparam P1 the type of the first field of the Product type T. + * @tparam P2 the type of the second field of the Product type T. + * @tparam P3 the type of the second field of the Product type T. + * @tparam P4 the type of the fourth field of the Product type T. + * @tparam P5 the type of the fifth field of the Product type T. + * @tparam P6 the type of the sixth field of the Product type T. + * @tparam P7 the type of the seventh field of the Product type T. + * @tparam P8 the type of the eighth field of the Product type T. + * @tparam P9 the type of the ninth field of the Product type T. + * @tparam P10 the type of the tenth field of the Product type T. + * @tparam P11 the type of the eleventh field of the Product type T. + * @tparam P12 the type of the twelfth field of the Product type T. + * @tparam T the underlying type of the first parameter of the input to the render method. + * @return a HierarchicalRenderer[T]. + */ def renderer12[P1: HierarchicalRenderer, P2: HierarchicalRenderer, P3: HierarchicalRenderer, P4: HierarchicalRenderer, P5: HierarchicalRenderer, P6: HierarchicalRenderer, P7: HierarchicalRenderer, P8: HierarchicalRenderer, P9: HierarchicalRenderer, P10: HierarchicalRenderer, P11: HierarchicalRenderer, P12: HierarchicalRenderer, T <: Product : ClassTag](style: String, attrs: Map[String, String] = Map())(construct: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12) => T): HierarchicalRenderer[T] = new ProductHierarchicalRenderer[T](style, attrs) { protected def nodes(t: T): Seq[Node] = { diff --git a/src/main/scala/com/phasmidsoftware/render/JsonTableRenderer.scala b/src/main/scala/com/phasmidsoftware/render/JsonTableRenderer.scala index ddfc6d4f..8a75aacb 100644 --- a/src/main/scala/com/phasmidsoftware/render/JsonTableRenderer.scala +++ b/src/main/scala/com/phasmidsoftware/render/JsonTableRenderer.scala @@ -8,12 +8,12 @@ import com.phasmidsoftware.table.{Table, TableJsonFormat} import spray.json.{JsonFormat, enrichAny} /** - * Abstract Class JsonTableRenderer which will render a Table[T] as a JsValue. - * - * TEST - * - * @tparam T the underlying type of the Table (i.e. the Row type) for which there must be evidence of JsonWriter[T]. - */ + * Abstract Class JsonTableRenderer which will render a Table[T] as a JsValue. + * + * TEST + * + * @tparam T the underlying type of the Table (i.e. the Row type) for which there must be evidence of JsonWriter[T]. + */ abstract class JsonTableRenderer[T]()(implicit tj: JsonFormat[T]) extends Renderer[Table[T], String] { def render(tt: Table[T], attrs: Map[String, String]): String = { diff --git a/src/main/scala/com/phasmidsoftware/render/Renderable.scala b/src/main/scala/com/phasmidsoftware/render/Renderable.scala index 85b1e396..9f516dac 100644 --- a/src/main/scala/com/phasmidsoftware/render/Renderable.scala +++ b/src/main/scala/com/phasmidsoftware/render/Renderable.scala @@ -7,66 +7,70 @@ package com.phasmidsoftware.render import com.phasmidsoftware.table.{Indexed, Table} /** - * Polymorphic trait which defines the behavior of some sort of collection with an underlying type X and which can be rendered. - * - * CONSIDER: do we really need this trait? - * This trait is not a type class because X does not appear as a parameter or result of any of the methods. - * - * NOTE: this trait has no direct relationship with Renderer. - * CONSIDER a refactoring of the whole set of traits. - * - * @tparam X the underlying type of this Renderable. - */ + * Polymorphic trait which defines the behavior of some sort of collection with an underlying type X and which can be rendered. + * + * CONSIDER: do we really need this trait? + * This trait is not a type class because X does not appear as a parameter or result of any of the methods. + * + * NOTE: this trait has no direct relationship with Renderer. + * CONSIDER a refactoring of the whole set of traits. + * + * @tparam X the underlying type of this Renderable. + */ trait Renderable[X] { /** - * Method to render this Renderable to serialized type O without any constraint on O. - * - * CONSIDER combining this with RenderToWritable. - * - * CONSIDER generalizing the type of ev. - * - * @param ev implicit evidence for Renderer of Table of X. - * @tparam O the type of the result. - * @return an instance of O. - */ + * Method to render this Renderable to serialized type O without any constraint on O. + * + * CONSIDER combining this with renderToWritable. + * + * CONSIDER generalizing the type of ev. + * + * @param ev implicit evidence for Renderer of Table of X. + * @tparam O the type of the result. + * @return an instance of O. + */ def render[O](implicit ev: Renderer[Table[X], O]): O /** - * CONSIDER redefining the definition of Renderer such that we can accommodate the various different types of output. - * - * Method to render a table in a sequential (serialized) fashion. - * - * @tparam O a type which supports Writable (via evidence of type Writable[O]) - * @return a new (or possibly old) instance of O. - */ - def RenderToWritable[O: Writable]: O + * CONSIDER redefining the definition of Renderer such that we can accommodate the various different types of output. + * + * Method to render a table in a sequential (serialized) fashion. + * + * @tparam O a type which supports Writable (via evidence of type Writable[O]) + * @return a new (or possibly old) instance of O. + */ + def renderToWritable[O: Writable]: O /** - * Method to render a table in a hierarchical fashion. - * - * NOTE: if your output structure is not hierarchical in nature, then simply loop through the rows of this table, - * outputting each row as you go. - * - * @param style the "style" to be used for the node which will represent this table. - * @param attributes the attributes to be applied to the top level node for this table. - * @param xr an (implicit) HierarchicalRenderer[Row] - * @tparam U a class which supports TreeWriter (i.e. there is evidence of TreeWriter[U]). - * @return a new instance of U which represents this Table as a tree of some sort. - */ + * Method to render a table in a hierarchical fashion. + * + * TEST + * + * NOTE: if your output structure is not hierarchical in nature, then simply loop through the rows of this table, + * outputting each row as you go. + * + * @param style the "style" to be used for the node which will represent this table. + * @param attributes the attributes to be applied to the top level node for this table. + * @param xr an (implicit) HierarchicalRenderer[Row] + * @tparam U a class which supports TreeWriter (i.e. there is evidence of TreeWriter[U]). + * @return a new instance of U which represents this Table as a tree of some sort. + */ def renderHierarchical[U: TreeWriter](style: String, attributes: Map[String, String] = Map())(implicit xr: HierarchicalRenderer[X]): U /** - * Method to render a table in a hierarchical fashion. - * - * NOTE: if your output structure is not hierarchical in nature, then simply loop through the rows of this table, - * outputting each row as you go. - * - * @param style the "style" to be used for the node which will represent this table. - * @param attributes the attributes to be applied to the top level node for this table. - * @param xr an (implicit) HierarchicalRenderer[ Indexed [ Row ] ] - * @tparam U a class which supports TreeWriter (i.e. there is evidence of TreeWriter[U]). - * @return a new instance of U which represents this Table as a tree of some sort. - */ + * Method to render a table in a hierarchical fashion. + * + * TEST + * + * NOTE: if your output structure is not hierarchical in nature, then simply loop through the rows of this table, + * outputting each row as you go. + * + * @param style the "style" to be used for the node which will represent this table. + * @param attributes the attributes to be applied to the top level node for this table. + * @param xr an (implicit) HierarchicalRenderer[ Indexed [ Row ] ] + * @tparam U a class which supports TreeWriter (i.e. there is evidence of TreeWriter[U]). + * @return a new instance of U which represents this Table as a tree of some sort. + */ def renderHierarchicalSequenced[U: TreeWriter](style: String, attributes: Map[String, String] = Map())(implicit xr: HierarchicalRenderer[Indexed[X]]): U } diff --git a/src/main/scala/com/phasmidsoftware/render/Renderer.scala b/src/main/scala/com/phasmidsoftware/render/Renderer.scala index 44d06b98..a82bff09 100644 --- a/src/main/scala/com/phasmidsoftware/render/Renderer.scala +++ b/src/main/scala/com/phasmidsoftware/render/Renderer.scala @@ -4,98 +4,99 @@ package com.phasmidsoftware.render +import com.phasmidsoftware.table.{CsvAttributes, CsvGenerator, CsvProductGenerator, Table} +import java.io.{File, FileWriter} import org.joda.time.LocalDate - import scala.annotation.implicitNotFound import scala.reflect.ClassTag /** - * Definition of trait Renderer for the purpose of serializing objects of type T as an object of type O. - * This trait may be used as a type class for either T or O (or both). - * - * NOTE: this trait has no direct relationship with Renderable. - * - * @tparam T the type of object to be rendered. - * @tparam O the type of the serialization result. - */ + * Definition of trait Renderer for the purpose of serializing objects of type T as an object of type O. + * This trait may be used as a type class for either T or O (or both). + * + * NOTE: this trait has no direct relationship with Renderable. + * + * @tparam T the type of object to be rendered. + * @tparam O the type of the serialization result. + */ @implicitNotFound(msg = "Cannot find an implicit instance of Renderer[${T},{O}].") trait Renderer[T, O] { /** - * Render an instance of T as an O, qualifying the rendering with no attributes. - * - * @param t a T to be rendered. - * @return an instance of type O. - */ + * Render an instance of T as an O, qualifying the rendering with no attributes. + * + * @param t a T to be rendered. + * @return an instance of type O. + */ def render(t: T): O = render(t, Map()) /** - * Render an instance of T as an O, qualifying the rendering with attributes defined in attrs. - * - * @param t the input parameter, i.e. the T object to render. - * @param attrs a map of attributes for this value of O. - * @return an instance of type O. - */ + * Render an instance of T as an O, qualifying the rendering with attributes defined in attrs. + * + * @param t the input parameter, i.e. the T object to render. + * @param attrs a map of attributes for this value of O. + * @return an instance of type O. + */ def render(t: T, attrs: Map[String, String]): O } /** - * Definition of type class HierarchicalRenderer for the purpose of serializing objects of type T. - * Since, in general, T will be a case class which may include parameters which are case classes, - * we render to a hierarchical type: Node. - * - * @tparam T the type of object to be rendered. - */ + * Definition of type class HierarchicalRenderer for the purpose of serializing objects of type T. + * Since, in general, T will be a case class which may include parameters which are case classes, + * we render to a hierarchical type: Node. + * + * @tparam T the type of object to be rendered. + */ @implicitNotFound(msg = "Cannot find an implicit instance of HierarchicalRenderer[${T}].") trait HierarchicalRenderer[T] extends Renderer[T, Node] { /** - * Defines the default style for type T. - */ + * Defines the default style for type T. + */ val style: String /** - * Defines the base attribute set for type T. - */ + * Defines the base attribute set for type T. + */ val baseAttrs: Map[String, String] = Map() /** - * Render an instance of T as a U. - * - * @param t the input parameter, i.e. the object to be rendered. - * @param attrs a map of attributes for this value of U. - * @return a new instance of U. - */ + * Render an instance of T as a U. + * + * @param t the input parameter, i.e. the object to be rendered. + * @param attrs a map of attributes for this value of U. + * @return a new instance of U. + */ def render(t: T, attrs: Map[String, String]): Node /** - * Method to render content as a String. - * This method is invoked only when T is not a Product, sequence or Option. - * Normally, the default method is what is required, but it might be necessary to override - * in some situations. - * This method does not apply to style or attribute values. - * - * @param t the content value. - * @return a String corresponding to t. - */ + * Method to render content as a String. + * This method is invoked only when T is not a Product, sequence or Option. + * Normally, the default method is what is required, but it might be necessary to override + * in some situations. + * This method does not apply to style or attribute values. + * + * @param t the content value. + * @return a String corresponding to t. + */ def asString(t: T): String = t.toString } /** - * CONSIDER having style defined as an Option[String] - * - * @tparam T the type of object to be rendered. - */ + * CONSIDER having style defined as an Option[String] + * + * @tparam T the type of object to be rendered. + */ trait UntaggedHierarchicalRenderer[T] extends HierarchicalRenderer[T] { val style: String = "" /** - * Render an instance of T as a U. - * - * @param t the input parameter, i.e. the object to be rendered. - * @param attrs a map of attributes for this value of U. - * @return a new instance of U. - */ + * Render an instance of T as a U. + * + * @param t the input parameter, i.e. the object to be rendered. + * @param attrs a map of attributes for this value of U. + * @return a new instance of U. + */ def render(t: T, attrs: Map[String, String]): Node = Node(style, Some(asString(t)), baseAttrs ++ attrs) } @@ -103,12 +104,12 @@ trait UntaggedHierarchicalRenderer[T] extends HierarchicalRenderer[T] { abstract class TaggedHierarchicalRenderer[T](val style: String, override val baseAttrs: Map[String, String] = Map()) extends HierarchicalRenderer[T] { /** - * Render an instance of T as a U. - * - * @param t the input parameter, i.e. the object to be rendered. - * @param attrs a map of attributes for this value of U. - * @return a new instance of U. - */ + * Render an instance of T as a U. + * + * @param t the input parameter, i.e. the object to be rendered. + * @param attrs a map of attributes for this value of U. + * @return a new instance of U. + */ def render(t: T, attrs: Map[String, String]): Node = Node(style, Some(asString(t)), baseAttrs ++ attrs) } @@ -147,6 +148,59 @@ object HierarchicalRenderer { trait LocalDateHierarchicalRenderer extends UntaggedHierarchicalRenderer[LocalDate] implicit object LocalDateHierarchicalRenderer extends LocalDateHierarchicalRenderer +} + +/** + * Type class for rendering instances to CSV. + * + * @tparam T the type of object to be rendered. + */ +trait CsvRenderer[T] extends Renderer[T, String] { + // CONSIDER removing this abstract val. + val csvAttributes: CsvAttributes +} +abstract class CsvTableRenderer[T: CsvRenderer : CsvGenerator, O: Writable]()(implicit csvAttributes: CsvAttributes) extends Renderer[Table[T], O] { + /** + * Render an instance of T as an O, qualifying the rendering with attributes defined in attrs. + * + * @param t the input parameter, i.e. the Table[T] instance to render. + * @param attrs a map of attributes for this value of O. + * @return an instance of type O. + */ + def render(t: Table[T], attrs: Map[String, String]): O = t match { + case x: Table[_] => + val sw = implicitly[Writable[O]] + val tc = implicitly[CsvRenderer[T]] + val tg = implicitly[CsvGenerator[T]] + val hdr: String = tg match { + case _tg: CsvProductGenerator[_] => _tg.toColumnNames(None, None) + case _tg: CsvGenerator[_] => _tg.toColumnName(None, "") + } + val o = sw.unit + sw.writeRawLine(o)(hdr) + for (x <- x.rows.toSeq) sw.writeRawLine(o)(tc.render(x, Map())) + sw.close(o) + o + } } + +/** + * Case class to help render a Table to a StringBuilder in CSV format. + * + * @param csvAttributes implicit instance of CsvAttributes. + * @tparam T the type of object to be rendered, must provide evidence of CsvRenderer[T] amd CsvGenerator[T]. + */ +case class CsvTableStringRenderer[T: CsvRenderer : CsvGenerator]()(implicit csvAttributes: CsvAttributes) extends CsvTableRenderer[T, StringBuilder]()(implicitly[CsvRenderer[T]], implicitly[CsvGenerator[T]], Writable.stringBuilderWritable(csvAttributes.delimiter, csvAttributes.quote), csvAttributes) + +/** + * Case class to help render a Table to a File in CSV format. + * + * @param file the file to which the table will be written. + * @param csvAttributes implicit instance of CsvAttributes. + * @tparam T the type of object to be rendered, must provide evidence of CsvRenderer[T] amd CsvGenerator[T]. + */ +case class CsvTableFileRenderer[T: CsvRenderer : CsvGenerator](file: File)(implicit csvAttributes: CsvAttributes) extends CsvTableRenderer[T, FileWriter]()(implicitly[CsvRenderer[T]], implicitly[CsvGenerator[T]], Writable.fileWritable(file), csvAttributes) + + diff --git a/src/main/scala/com/phasmidsoftware/render/TreeWriter.scala b/src/main/scala/com/phasmidsoftware/render/TreeWriter.scala index e951b70c..adb97bfd 100644 --- a/src/main/scala/com/phasmidsoftware/render/TreeWriter.scala +++ b/src/main/scala/com/phasmidsoftware/render/TreeWriter.scala @@ -8,46 +8,46 @@ import scala.annotation.implicitNotFound /** - * This trait defines the behavior of a hierarchical writer of objects. - * For example, U might be defined as an HTML or XML document. - * TreeWriter is of course typically used as a type class. - * - * CONSIDER parameterizing the underlying type of the content parameter. - * - * @tparam U the type of a node of the tree. - */ + * This trait defines the behavior of a hierarchical writer of objects. + * For example, U might be defined as an HTML or XML document. + * TreeWriter is of course typically used as a type class. + * + * CONSIDER parameterizing the underlying type of the content parameter. + * + * @tparam U the type of a node of the tree. + */ @implicitNotFound(msg = "Cannot find an implicit instance of TreeWriter[${U}].") trait TreeWriter[U] { /** - * This method is required to convert a Node into a U. - * - * @param node the Node to be converted. - * @return an instance of U. - */ + * This method is required to convert a Node into a U. + * + * @param node the Node to be converted. + * @return an instance of U. + */ def evaluate(node: Node): U } /** - * This case class defines a Node in the hierarchical output produced by rendering. - * This class is used in conjunction with TreeWriter. - * The reason for this temporary structure is that we need the ability to merge (or otherwise process) nodes of the tree. - * Since U (see TreeWriter) is an opaque type as far as this code is concerned, we need our own representation of the tree. - * - * @param style a label that characterizes a particular node type. - * This will typically be translated directly into the "tag" parameter of the corresponding U type. - * @param content the content of this Node, if any. - * @param attributes the attributes of this Node (may be empty). - * @param children the children of this Node (may be empty). - */ + * This case class defines a Node in the hierarchical output produced by rendering. + * This class is used in conjunction with TreeWriter. + * The reason for this temporary structure is that we need the ability to merge (or otherwise process) nodes of the tree. + * Since U (see TreeWriter) is an opaque type as far as this code is concerned, we need our own representation of the tree. + * + * @param style a label that characterizes a particular node type. + * This will typically be translated directly into the "tag" parameter of the corresponding U type. + * @param content the content of this Node, if any. + * @param attributes the attributes of this Node (may be empty). + * @param children the children of this Node (may be empty). + */ case class Node(style: String, content: Option[String], attributes: Map[String, String], children: Seq[Node]) { /** - * Method to eliminate nodes of the form Node("", None, Map.empty, ...). - * - * @return a subtree rooted at this, but with nodes trimmed. - */ + * Method to eliminate nodes of the form Node("", None, Map.empty, ...). + * + * @return a subtree rooted at this, but with nodes trimmed. + */ lazy val trim: Node = this match { case Node(s, wo, kVm, ns) => Node(s, wo, kVm, for (n <- ns; x <- doTrim(n)) yield x) } @@ -59,83 +59,83 @@ case class Node(style: String, content: Option[String], attributes: Map[String, } /** - * Companion object to Node. - */ + * Companion object to Node. + */ object Node { /** - * Create a content-less leaf Node (with no children). - * NOTE: I'm not sure if this makes sense and is only used by unit tests. - * - * @param style a label that characterizes a particular node type. - * This will typically be translated directly into the "tag" parameter of the corresponding U type. - * @param attributes the attributes of this Node (may be empty). - * @return a new Node. - */ + * Create a content-less leaf Node (with no children). + * NOTE: I'm not sure if this makes sense and is only used by unit tests. + * + * @param style a label that characterizes a particular node type. + * This will typically be translated directly into the "tag" parameter of the corresponding U type. + * @param attributes the attributes of this Node (may be empty). + * @return a new Node. + */ def apply(style: String, attributes: Map[String, String]): Node = apply(style, None, attributes) /** - * Create a leaf Node (with no children). - * - * @param style a label that characterizes a particular node type. - * This will typically be translated directly into the "tag" parameter of the corresponding U type. - * @param content the content of this Node, if any. - * @param attributes the attributes of this Node (may be empty). - * @return a new Node. - */ + * Create a leaf Node (with no children). + * + * @param style a label that characterizes a particular node type. + * This will typically be translated directly into the "tag" parameter of the corresponding U type. + * @param content the content of this Node, if any. + * @param attributes the attributes of this Node (may be empty). + * @return a new Node. + */ def apply(style: String, content: Option[String], attributes: Map[String, String]): Node = apply(style, content, attributes, Nil) /** - * Create a content-less, no-attribute, leaf Node (with no children). - * NOTE: I'm not sure if this makes sense and is only used by unit tests. - * - * @param style a label that characterizes a particular node type. - * This will typically be translated directly into the "tag" parameter of the corresponding U type. - * @return a new Node. - */ + * Create a content-less, no-attribute, leaf Node (with no children). + * NOTE: I'm not sure if this makes sense and is only used by unit tests. + * + * @param style a label that characterizes a particular node type. + * This will typically be translated directly into the "tag" parameter of the corresponding U type. + * @return a new Node. + */ def apply(style: String): Node = apply(style, Nil) /** - * Create a Node with only style and children. - * - * @param style a label that characterizes a particular node type. - * This will typically be translated directly into the "tag" parameter of the corresponding U type. - * @param children the children of this Node (may be empty). - * @return a new Node. - */ + * Create a Node with only style and children. + * + * @param style a label that characterizes a particular node type. + * This will typically be translated directly into the "tag" parameter of the corresponding U type. + * @param children the children of this Node (may be empty). + * @return a new Node. + */ def apply(style: String, children: Seq[Node]): Node = apply(style, Map[String, String](), children) /** - * Create a Node with no content. - * - * @param style a label that characterizes a particular node type. - * This will typically be translated directly into the "tag" parameter of the corresponding U type. - * @param attributes the attributes of this Node (may be empty). - * @param children the children of this Node (may be empty). - * @return a new Node. - */ + * Create a Node with no content. + * + * @param style a label that characterizes a particular node type. + * This will typically be translated directly into the "tag" parameter of the corresponding U type. + * @param attributes the attributes of this Node (may be empty). + * @param children the children of this Node (may be empty). + * @return a new Node. + */ def apply(style: String, attributes: Map[String, String], children: Seq[Node]): Node = apply(style, None, attributes, children) /** - * TODO can eliminate - * - * Merge two Nodes together, taking the style, content (if any) and attributes from the left node. - * - * @param left the left Node (its children will precede the children of the right Node). - * @param right the right Node (its children will succeed the children of the left Node). - * @return a new Node containing all the children of the left and right. - */ + * TODO can eliminate + * + * Merge two Nodes together, taking the style, content (if any) and attributes from the left node. + * + * @param left the left Node (its children will precede the children of the right Node). + * @param right the right Node (its children will succeed the children of the left Node). + * @return a new Node containing all the children of the left and right. + */ def mergeLeft(left: Node, right: Node): Node = Node(left.style, left.content, left.attributes, left.children ++ right.children) /** - * TODO can eliminate - * - * Merge two Nodes together, taking the style, content (if any) and attributes from the right node. - * - * @param left the left Node (its children will precede the children of the right Node). - * @param right the right Node (its children will succeed the children of the left Node). - * @return a new Node containing all the children of the left and right. - */ + * TODO can eliminate + * + * Merge two Nodes together, taking the style, content (if any) and attributes from the right node. + * + * @param left the left Node (its children will precede the children of the right Node). + * @param right the right Node (its children will succeed the children of the left Node). + * @return a new Node containing all the children of the left and right. + */ def mergeRight(left: Node, right: Node): Node = Node(right.style, right.content, right.attributes, left.children ++ right.children) } diff --git a/src/main/scala/com/phasmidsoftware/render/Writable.scala b/src/main/scala/com/phasmidsoftware/render/Writable.scala index 6e605792..c3f16662 100644 --- a/src/main/scala/com/phasmidsoftware/render/Writable.scala +++ b/src/main/scala/com/phasmidsoftware/render/Writable.scala @@ -4,49 +4,65 @@ package com.phasmidsoftware.render +import java.io.{File, FileWriter} + /** - * Trait to enable rendering of a table to a sequential (non-hierarchical) output format. - * - * @tparam O the underlying type, for example, a StringBuilder. - */ + * Trait to enable rendering of a table to a sequential (non-hierarchical) output format. + * + * @tparam O the underlying type, for example, a StringBuilder. + */ trait Writable[O] { // TODO create some off-the-shelf Writables /** - * Method to return an empty (i.e. new) instance of O - * - * @return - */ + * Method to return an empty (i.e. new) instance of O + * + * @return + */ def unit: O /** - * Method to write a character sequence to the given instance o. - * - * @param o the instance of O whither the parameter x should be written. - * @param x the character sequence to be written. - * @return an instance of O which represents the updated output structure. - */ + * Method to close this Writable, if appropriate. + */ + def close(o: O): Unit = () + + /** + * Method to write a character sequence to the given instance o. + * + * @param o the instance of O whither the parameter x should be written. + * @param x the character sequence to be written. + * @return an instance of O which represents the updated output structure. + */ def writeRaw(o: O)(x: CharSequence): O /** - * Method to write a value of type Any to the given instance o, possibly quoted. - * - * @param o the instance of O whither the xs values should be written. - * @param x the row instance to be written. - * @return an instance of O which represents the updated output structure. - */ + * Method to write a character sequence to the given instance o followed by a newline. + * + * @param o the instance of O whither the parameter x should be written. + * @param x the character sequence to be written. + * @return an instance of O which represents the updated output structure. + */ + def writeRawLine(o: O)(x: CharSequence): O = writeRaw(writeRaw(o)(x))(newline) + + /** + * Method to write a value of type Any to the given instance o, possibly quoted. + * + * @param o the instance of O whither the xs values should be written. + * @param x the row instance to be written. + * @return an instance of O which represents the updated output structure. + */ def writeRow[Row <: Product](o: O)(x: Row): O = writeRaw(writeRowElements(o)(x.productIterator.toSeq))(newline) /** - * Method to write a value of type Any to the given instance o, possibly quoted. - * Elements will be separated by the delimiter, but no newline is appended. - * Element strings may be enclosed in quotes if appropriate. - * - * @param o the instance of O whither the xs values should be written. - * @param xs the sequence of elements (values) to be written. - * @return an instance of O which represents the updated output structure. - */ + * Method to write a value of type Any to the given instance o, possibly quoted. + * Elements will be separated by the delimiter, but no newline is appended. + * Element strings may be enclosed in quotes if appropriate. + * + * @param o the instance of O whither the xs values should be written. + * @param xs the sequence of elements (values) to be written. + * @return an instance of O which represents the updated output structure. + */ def writeRowElements(o: O)(xs: Seq[Any]): O = { // CONSIDER using foldLeft so that we can use the updated value of o at each step. for (x <- xs.headOption) writeValue(o)(x) @@ -57,49 +73,68 @@ trait Writable[O] { private val sQuote: String = quote.toString /** - * Method to write a value of type Any to the given instance o, possibly quoted. - * - * @param o the instance of O whither the parameter x should be written. - * @param x the character sequence to be written. - * @return an instance of O which represents the updated output structure. - */ + * Method to write a value of type Any to the given instance o, possibly quoted. + * + * @param o the instance of O whither the parameter x should be written. + * @param x the character sequence to be written. + * @return an instance of O which represents the updated output structure. + */ def writeValue(o: O)(x: Any): O = if (x.toString.contains(delimiter.toString) || x.toString.contains(sQuote)) writeQuoted(o)(x.toString) else writeRaw(o)(x.toString) /** - * Method to write a character sequence to the given instance o, but within quotes. - * Any quote characters in x will be doubled. - * - * @param o the instance of O whither the parameter x should be written. - * @param x the character sequence to be written. - * @return an instance of O which represents the updated output structure. - */ + * Method to write a character sequence to the given instance o, but within quotes. + * Any quote characters in x will be doubled. + * + * @param o the instance of O whither the parameter x should be written. + * @param x the character sequence to be written. + * @return an instance of O which represents the updated output structure. + */ def writeQuoted(o: O)(x: CharSequence): O = { val w = x.toString.replaceAll(quote.toString, s"$quote$quote") writeRaw(o)(s"$quote$w$quote") } /** - * The default quote is one double-quote symbol - * - * @return " - */ - def quote: CharSequence = - """"""" + * The default quote is one double-quote symbol + * + * @return " + */ + def quote: CharSequence = """"""" /** - * The default delimiter is a comma followed by a space. - * - * @return ", " - */ + * The default delimiter is a comma followed by a space. + * + * @return ", " + */ def delimiter: CharSequence = ", " /** - * The default newline character is the newline. - * - * @return \n - */ + * The default newline character is the newline. + * + * @return \n + */ def newline: CharSequence = "\n" } + +object Writable { + def stringBuilderWritable(delim: CharSequence = ",", quoteChar: CharSequence = """""""): Writable[StringBuilder] = new Writable[StringBuilder] { + def writeRaw(o: StringBuilder)(x: CharSequence): StringBuilder = o.append(x) + + def unit: StringBuilder = new StringBuilder + + override def delimiter: CharSequence = delim + + override def quote: CharSequence = quoteChar + } + + def fileWritable(file: File): Writable[FileWriter] = new Writable[FileWriter] { + def unit: FileWriter = new FileWriter(file) + + def writeRaw(o: FileWriter)(x: CharSequence): FileWriter = o.append(x).asInstanceOf[FileWriter] + + override def close(o: FileWriter): Unit = o.close() + } +} \ No newline at end of file diff --git a/src/main/scala/com/phasmidsoftware/render/tag/HTML.scala b/src/main/scala/com/phasmidsoftware/render/tag/HTML.scala index e1af8088..49316f1c 100644 --- a/src/main/scala/com/phasmidsoftware/render/tag/HTML.scala +++ b/src/main/scala/com/phasmidsoftware/render/tag/HTML.scala @@ -7,28 +7,30 @@ package com.phasmidsoftware.render.tag import scala.language.implicitConversions /** - * Case class to model an HTML document. - * - * @param name the name of the tag at the root of the document. - * @param attributes the attributes of the tag. - * @param content the (optional) content of the tag. - * @param tags the child tags. - * @param rules the "rules" (currently ignored) but useful in the future to validate documents. - */ + * Case class to model an HTML document. + * + * TEST + * + * @param name the name of the tag at the root of the document. + * @param attributes the attributes of the tag. + * @param content the (optional) content of the tag. + * @param tags the child tags. + * @param rules the "rules" (currently ignored) but useful in the future to validate documents. + */ case class HTML(name: String, attributes: Seq[Attribute], content: Option[String], tags: Seq[Tag])(implicit rules: TagRules) extends BaseTag(name, attributes, content, tags) { /** - * Method to add a child to this Tag - * - * @param tag the tag to be added - * @return a new version of this Tag with the additional tag added as a child - */ + * Method to add a child to this Tag + * + * @param tag the tag to be added + * @return a new version of this Tag with the additional tag added as a child + */ override def :+(tag: Tag): Tag = HTML(name, attributes, content, tags :+ tag) } /** - * Companion object to HTML - */ + * Companion object to HTML + */ object HTML { implicit object HtmlRules extends TagRules diff --git a/src/main/scala/com/phasmidsoftware/render/tag/Tag.scala b/src/main/scala/com/phasmidsoftware/render/tag/Tag.scala index ae69f677..b70250f8 100644 --- a/src/main/scala/com/phasmidsoftware/render/tag/Tag.scala +++ b/src/main/scala/com/phasmidsoftware/render/tag/Tag.scala @@ -7,74 +7,76 @@ package com.phasmidsoftware.render.tag import scala.language.implicitConversions /** - * Case class defining an attribute. - * - * @param key the attribute's key. - * @param value the attribute's value. - */ + * Case class defining an attribute. + * + * @param key the attribute's key. + * @param value the attribute's value. + */ case class Attribute(key: String, value: String) { override def toString: String = s"""$key="$value"""" } /** - * Trait Tag to model a tagged (i.e. Markup Language-type) document. - * Examples of a tagged document would be XML or HTML. - */ + * Trait Tag to model a tagged (i.e. Markup Language-type) document. + * Examples of a tagged document would be XML or HTML. + */ trait Tag { /** - * Method to yield the name of this Tag - * - * @return the name, that's to say what goes between < and > - */ + * Method to yield the name of this Tag + * + * @return the name, that's to say what goes between < and > + */ def name: String /** - * Method to yield the attributes of this Tag. - * - * @return a sequence of Attributes - */ + * Method to yield the attributes of this Tag. + * + * @return a sequence of Attributes + */ def attributes: Seq[Attribute] /** - * Method to yield the content of this Tag. - * - * @return the (optional) content as a String. - */ + * Method to yield the content of this Tag. + * + * @return the (optional) content as a String. + */ def content: Option[String] /** - * Method to yield the child Tags of this Tag. - * - * @return a Seq of Tags. - */ + * Method to yield the child Tags of this Tag. + * + * @return a Seq of Tags. + */ def tags: Seq[Tag] /** - * Method to add a child to this Tag - * - * @param tag the tag to be added - * @return a new version of this Tag with the additional tag added as a child - */ + * Method to add a child to this Tag + * + * @param tag the tag to be added + * @return a new version of this Tag with the additional tag added as a child + */ def :+(tag: Tag): Tag /** - * Method to yield the tag names depth-first in a Seq - * - * @return a sequence of tag names - */ + * Method to yield the tag names depth-first in a Seq + * + * TEST + * + * @return a sequence of tag names + */ def \\ : Seq[String] = name +: (for (t <- tags; x <- t.\\) yield x) } /** - * Abstract class representing a base tag. - * - * @param name the name of the tag. - * @param attributes the attributes as a sequence. - * @param content an optional content. - * @param tags the children tags. - * @param rules an implicit set of rules (like a DTD). - */ + * Abstract class representing a base tag. + * + * @param name the name of the tag. + * @param attributes the attributes as a sequence. + * @param content an optional content. + * @param tags the children tags. + * @param rules an implicit set of rules (like a DTD). + */ abstract class BaseTag(name: String, attributes: Seq[Attribute], content: Option[String], tags: Seq[Tag])(implicit rules: TagRules) extends Tag { override def toString: String = s"""\n${tagString()}$contentString$tagsString${tagString(true)}""" @@ -92,6 +94,12 @@ abstract class BaseTag(name: String, attributes: Seq[Attribute], content: Option object Attribute { + /** + * TEST + * + * @param m a Map of attributes. + * @return a Seq[Attribute]. + */ def mapToAttributes(m: Map[String, String]): Seq[Attribute] = m.toSeq.map(apply) def apply(kv: (String, String)): Attribute = convertFromTuple(kv) @@ -100,9 +108,9 @@ object Attribute { } /** - * For future expansion. - * The tag rules will allow us to check the model of a Tag. - * For example, does it conform to HTML5? - * Or XML, etc? - */ + * For future expansion. + * The tag rules will allow us to check the model of a Tag. + * For example, does it conform to HTML5? + * Or XML, etc? + */ trait TagRules diff --git a/src/main/scala/com/phasmidsoftware/table/Analysis.scala b/src/main/scala/com/phasmidsoftware/table/Analysis.scala index fc21b619..ef2f7afb 100644 --- a/src/main/scala/com/phasmidsoftware/table/Analysis.scala +++ b/src/main/scala/com/phasmidsoftware/table/Analysis.scala @@ -25,12 +25,12 @@ object Analysis { } /** - * A representation of the analysis of a column. - * - * @param clazz a String denoting which class (maybe which variant of class) this column may be represented as. - * @param optional if true then this column contains nulls (empty strings). - * @param maybeStatistics an optional set of statistics but only if the column represents numbers. - */ + * A representation of the analysis of a column. + * + * @param clazz a String denoting which class (maybe which variant of class) this column may be represented as. + * @param optional if true then this column contains nulls (empty strings). + * @param maybeStatistics an optional set of statistics but only if the column represents numbers. + */ case class Column(clazz: String, optional: Boolean, maybeStatistics: Option[Statistics]) { override def toString: String = { val sb = new StringBuilder diff --git a/src/main/scala/com/phasmidsoftware/table/CsvAttributes.scala b/src/main/scala/com/phasmidsoftware/table/CsvAttributes.scala new file mode 100644 index 00000000..329875c6 --- /dev/null +++ b/src/main/scala/com/phasmidsoftware/table/CsvAttributes.scala @@ -0,0 +1,78 @@ +package com.phasmidsoftware.table + +import com.phasmidsoftware.util.Reflection +import scala.reflect.ClassTag + +/** + * Case class for CSV attributes, especially for rendering as CSV. + * + * CONSIDER merging this with RowConfig. + * + * @param delimiter the delimiter. + * @param quote the quote character. + */ +case class CsvAttributes(delimiter: String, quote: String) + +object CsvAttributes { + implicit val defaultCsvAttributes: CsvAttributes = CsvAttributes(",") + + def apply(delimiter: String): CsvAttributes = CsvAttributes(delimiter, """"""") +} + +/** + * Trait (type class?) for generating headers for CSV output. + * + * NOTE: this is an unusual type class in that none of its methods reference type T. + * + * @tparam T the type of the objects to be rendered by CSV. + */ +trait CsvGenerator[T] { + // CONSIDER removing this abstract val. + val csvAttributes: CsvAttributes + + /** + * Method to generate a list of appropriate column names for a value of t. + * + * @param po the (optional) name of the parent. + * @param name the name of this column. + * @return a list of names of the form parent.column. + */ + def toColumnName(po: Option[String], name: String): String +} + +/** + * Trait (type class?) for generating headers for CSV output. + * + * NOTE: this is an unusual type class in that none of its methods reference type T. + * + * @tparam T the type of the objects to be rendered by CSV. + */ +trait CsvProductGenerator[T] extends CsvGenerator[T] { + def fieldNames(implicit tc: ClassTag[T]): Array[String] = Reflection.extractFieldNames(tc) + + /** + * Method to generate a list of appropriate column names for a value of t. + * + * @param po the (optional) name of the parent. + * @param no the (optional) name of this column. + * @return a list of names of the form parent.column. + */ + def toColumnNames(po: Option[String], no: Option[String]): String + + override def toColumnName(po: Option[String], name: String): String = toColumnNames(po, Some(name)) +} + +class BaseCsvGenerator[T](implicit ca: CsvAttributes) extends CsvGenerator[T] { + val csvAttributes: CsvAttributes = ca + + def toColumnName(po: Option[String], name: String): String = (po map (w => s"$w.")).getOrElse("") + name + + def merge(po: Option[String], no: Option[String]): Option[String] = po match { + case Some(p) => + no match { + case Some(n) => Some(s"$p.$n") + case None => po + } + case None => no + } +} diff --git a/src/main/scala/com/phasmidsoftware/table/Row.scala b/src/main/scala/com/phasmidsoftware/table/Row.scala index e5f2db64..d92999be 100644 --- a/src/main/scala/com/phasmidsoftware/table/Row.scala +++ b/src/main/scala/com/phasmidsoftware/table/Row.scala @@ -5,57 +5,78 @@ package com.phasmidsoftware.table import com.phasmidsoftware.parse.ParserException - +import com.phasmidsoftware.render.CsvRenderer import scala.util.{Failure, Try} /** - * Case class to represent a (raw) row from a table. - * - * @param ws the (raw) Strings that make up the row. - * @param hdr is the Header containing the column names. - */ + * Case class to represent a (raw) row from a table. + * + * @param ws the (raw) Strings that make up the row. + * @param hdr is the Header containing the column names. + */ case class Row(ws: Seq[String], hdr: Header, index: Int) extends (String => Try[String]) { /** - * Method to yield the value for a given column name - * - * NOTE this doesn't seem to be used. - * TODO: why is ParserException not found to link to? - * - * @param w the column name. - * @return the value as a String. - */ + * Method to yield the value for a given column name + * + * NOTE this doesn't seem to be used. + * TODO: why is ParserException not found to link to? + * + * @param w the column name. + * @return the value as a String. + */ def apply(w: String): Try[String] = hdr.getIndex(w).flatMap { i => apply(i) }.recoverWith[String] { case _: IndexOutOfBoundsException => Failure[String](ParserException(s"Row: unknown column: $w")) } /** - * Method to yield the xth element of this Row. - * - * @param x an index from 0 thru length-1. - * @return the value as a String. - */ + * Method to yield the xth element of this Row. + * + * @param x an index from 0 thru length-1. + * @return the value as a String. + */ def apply(x: Int): Try[String] = Try(ws(x)) recoverWith { case e: IndexOutOfBoundsException if x == -1 => Failure(e) case _: IndexOutOfBoundsException => Failure(ParserException(s"Row: index out of range: $x (there are ${ws.size} elements)")) } /** - * Method to get the index of a column name - * - * @param column the column name - * @return the index, which might be -1 - */ + * Method to get the index of a column name + * + * @param column the column name + * @return the index, which might be -1 + */ def getIndex(column: String): Int = hdr.getIndex(column).getOrElse(-1) override def toString(): String = s"""Row: ${ws.mkString("[", ",", "]")} with header=$hdr""" } +object Row { + implicit object CsvRendererRow extends CsvRenderer[Row] { + val csvAttributes: CsvAttributes = implicitly[CsvAttributes] + + def render(r: Row, attrs: Map[String, String]): String = r.ws mkString csvAttributes.delimiter + } + + /** + * Method to yield a CsvGenerator[T] from an instance of Header. + * + * @param hdr the Header. + * @param c CsvAttributes. + * @return a new CsvGenerator[T]. + */ + def csvGenerator[T](hdr: Header)(implicit c: CsvAttributes): CsvGenerator[T] = new CsvGenerator[T] { + val csvAttributes: CsvAttributes = c + + def toColumnName(po: Option[String], name: String): String = ((hdr.xs +: hdr.xss) map (_.mkString(c.delimiter))).mkString("\n") + } +} + /** - * A wrapper class to index a T. - * - * @param i the index (ordinal value). - * @param t the instance of T. - * @tparam T the underlying type. - */ + * A wrapper class to index a T. + * + * @param i the index (ordinal value). + * @param t the instance of T. + * @tparam T the underlying type. + */ case class Indexed[T](i: Int, t: T) object Indexed { diff --git a/src/main/scala/com/phasmidsoftware/table/Table.scala b/src/main/scala/com/phasmidsoftware/table/Table.scala index 840f33a1..2095df06 100644 --- a/src/main/scala/com/phasmidsoftware/table/Table.scala +++ b/src/main/scala/com/phasmidsoftware/table/Table.scala @@ -10,535 +10,641 @@ import com.phasmidsoftware.parse._ import com.phasmidsoftware.render._ import com.phasmidsoftware.util.FP._ import com.phasmidsoftware.util.{Reflection, TryUsing} - import java.io.{File, InputStream} import java.net.{URI, URL} import scala.io.{Codec, Source} import scala.language.postfixOps import scala.reflect.ClassTag -import scala.util.{Failure, Try} +import scala.util.{Failure, Random, Try} /** - * A Table of Rows. - * - * @tparam Row the type of each row. - */ + * A Table of Rows. + * + * @tparam Row the type of each row. + */ trait Table[Row] extends Iterable[Row] { /** - * Optional value of the Header of this Table, if there is one. - */ + * Optional value of the Header of this Table, if there is one. + */ val maybeHeader: Option[Header] /** - * Method to clone this Table but with a different Header. - * - * @param ho an optional Header. - * @return a Table[Row] with the same rows as this, but with ho as its maybeHeader. - */ + * Method to clone this Table but with a different Header. + * + * @param ho an optional Header. + * @return a Table[Row] with the same rows as this, but with ho as its maybeHeader. + */ def replaceHeader(ho: Option[Header]): Table[Row] = unit(rows, ho) /** - * Transform (map) this Table[Row] into a Table[S]. - * - * @param f a function which transforms a Row into an S. - * @tparam S the type of the rows of the result. - * @return a Table[S] where each row has value f(x) where x is the value of the corresponding row in this. - */ + * Transform (map) this Table[Row] into a Table[S]. + * + * @param f a function which transforms a Row into an S. + * @tparam S the type of the rows of the result. + * @return a Table[S] where each row has value f(x) where x is the value of the corresponding row in this. + */ override def map[S](f: Row => S): Table[S] = unit(rows map f) /** - * Transform (flatMap) this Table[Row] into a Table[S]. - * - * CONSIDER rewriting this method or redefining it as it can be a major source of inefficiency. - * - * @param f a function which transforms a Row into an IterableOnce[S]. - * @tparam S the type of the rows of the result. - * @return a Table[S] which is made up of a concatenation of the results of invoking f on each row this - */ + * Transform (flatMap) this Table[Row] into a Table[S]. + * + * CONSIDER rewriting this method or redefining it as it can be a major source of inefficiency. + * + * @param f a function which transforms a Row into an IterableOnce[S]. + * @tparam S the type of the rows of the result. + * @return a Table[S] which is made up of a concatenation of the results of invoking f on each row this + */ def flatMap[S](f: Row => Iterable[S]): Table[S] = (rows map f).foldLeft(unit[S](Nil))((a, e) => a ++ unit(e)) /** - * Method to zip two Tables together such that the rows of the resulting table are tuples of the rows of the input tables. - * - * TEST - * - * @param table the other Table. - * @tparam R the underlying type of the other Table. - * @return a Table of (Row, R). - */ + * Method to zip two Tables together such that the rows of the resulting table are tuples of the rows of the input tables. + * + * TEST + * + * @param table the other Table. + * @tparam R the underlying type of the other Table. + * @return a Table of (Row, R). + */ def zip[R](table: Table[R]): Table[(Row, R)] = processRows[R, (Row, R)]((rs1, rs2) => rs1 zip rs2)(table) /** - * Method to concatenate two Rows - * - * @param table a table to be concatenated with this table. - * @tparam S the type of the rows of the result. - * @return a new table, which is concatenated to this table, by rows. - */ + * Method to concatenate two Rows + * + * @param table a table to be concatenated with this table. + * @tparam S the type of the rows of the result. + * @return a new table, which is concatenated to this table, by rows. + */ def ++[S >: Row](table: Table[S]): Table[S] = unit[S](rows ++ table.rows) /** - * Method to generate a Table[S] for a set of rows. - * Although declared as an instance method, this method produces its result independent of this. - * - * @param rows a sequence of S. - * @param maybeHeader an optional Header to be used in the resulting Table. - * @tparam S the underlying type of the rows and the result. - * @return a new instance of Table[S]. - */ + * Method to generate a Table[S] for a set of rows. + * Although declared as an instance method, this method produces its result independent of this. + * + * @param rows a sequence of S. + * @param maybeHeader an optional Header to be used in the resulting Table. + * @tparam S the underlying type of the rows and the result. + * @return a new instance of Table[S]. + */ def unit[S](rows: Iterable[S], maybeHeader: Option[Header]): Table[S] /** - * Method to generate a Table[S] for a set of rows. - * Although declared as an instance method, this method produces its result independent of this. - * - * @param rows a sequence of S. - * @tparam S the underlying type of the rows and the result. - * @return a new instance of Table[S]. - */ + * Method to generate a Table[S] for a set of rows. + * Although declared as an instance method, this method produces its result independent of this. + * + * @param rows a sequence of S. + * @tparam S the underlying type of the rows and the result. + * @return a new instance of Table[S]. + */ def unit[S](rows: Iterable[S]): Table[S] = unit(rows, maybeHeader) /** - * Method to access the individual rows of this table. - * - * @return the rows, in the same sequence in which they were parsed. - */ + * Method to access the individual rows of this table. + * + * @return the rows, in the same sequence in which they were parsed. + */ def rows: Iterable[Row] /** - * Method to yield a Seq of the rows of this Table. - * - * @return a Seq[Row] - */ - override def toSeq: Seq[Row] = { lazy val rs = rows.toSeq; rs } + * Method to select those rows defined by the given range. + * NOTE: the rows are numbered 1..N. + * + * @param range a Range + * @return a new Table[Row] consisting only of those rows in the given range. + */ + def select(range: Range): Table[Row] = { + val ys: IndexedSeq[Row] = asOneBasedIndexedSequence + Table(for (i <- range) yield ys(i), maybeHeader) + } + + /** + * Method to select those rows defined by the given range. + * NOTE: the rows are numbered 1..N. + * + * @param n the desired row. + * @return a new Table[Row] consisting only the row requested. + */ + def select(n: Int): Table[Row] = processRows(_.slice(n - 1, n)) + + /** + * Method to yield a Seq of the rows of this Table. + * + * TEST + * + * @return a Seq[Row] + */ + override def toSeq: Seq[Row] = { + lazy val rs = rows.toSeq + rs + } /** - * Method to yield an Array from the rows of this Table. - * - * @tparam Element the element type of the resulting Array. - * @return an Array[Element]. - */ - override def toArray[Element >: Row: ClassTag]: Array[Element] = { lazy val rs = rows.toArray[Element]; rs } + * Method to yield an Array from the rows of this Table. + * + * @tparam Element the element type of the resulting Array. + * @return an Array[Element]. + */ + override def toArray[Element >: Row : ClassTag]: Array[Element] = { + lazy val rs = rows.toArray[Element]; rs + } /** - * Method to return the rows of this table as an iterator. - * - * @return the rows in the form of Iterator[Row] - */ + * Method to return the rows of this table as an iterator. + * + * @return the rows in the form of Iterator[Row] + */ def iterator: Iterator[Row] = rows.iterator /** - * Method to process the rows as an Iterable into an Iterable which will make up the resulting Table. - * NOTE: if you need the rows processed individually, use map or flatMap. - * - * @param f a function which takes an Iterable[Row] and returns an Iterable[S] - * @tparam S the underlying type of the result. - * @return a table[S] - */ + * Method to process the rows as an Iterable into an Iterable which will make up the resulting Table. + * NOTE: if you need the rows processed individually, use map or flatMap. + * + * @param f a function which takes an Iterable[Row] and returns an Iterable[S] + * @tparam S the underlying type of the result. + * @return a table[S] + */ def processRows[S](f: Iterable[Row] => Iterable[S]): Table[S] = unit(f(rows)) /** - * Method to process the rows of this Table and the other Table as a pair of Iterables resulting in an Iterable which will make up the resulting Table. - * NOTE: this is used by zip. - * - * @param f a function which takes an Iterable[Row] and an Iterable[R] and returns an Iterable[S] - * @param other the other table of type Iterable[R]. - * @tparam R the underlying type of the other table. - * @tparam S the underlying type of the result. - * @return a table[S] - */ + * Method to process the rows of this Table and the other Table as a pair of Iterables resulting in an Iterable which will make up the resulting Table. + * NOTE: this is used by zip. + * + * TEST + * + * @param f a function which takes an Iterable[Row] and an Iterable[R] and returns an Iterable[S] + * @param other the other table of type Iterable[R]. + * @tparam R the underlying type of the other table. + * @tparam S the underlying type of the result. + * @return a table[S] + */ def processRows[R, S](f: (Iterable[Row], Iterable[R]) => Iterable[S])(other: Table[R]): Table[S] = unit(f(rows, other.rows)) /** - * Method to transform this Table[Row] into a sorted Table[S] where S is a super-class of Row and for which there is - * evidence of Ordering[S]. - * - * @tparam S the underlying type of the resulting Table (a super-type of Row and for which there is evidence of Ordering[S]). - * @return a Table[S]. - */ - def sorted[S >: Row : Ordering]: Table[S] = processRows(rs => (rs map (_.asInstanceOf[S])).toSeq.sorted) + * Method to transform this Table[Row] into a sorted Table[S] where S is a super-class of Row and for which there is + * evidence of Ordering[S]. + * + * @tparam S the underlying type of the resulting Table (a super-type of Row and for which there is evidence of Ordering[S]). + * @return a Table[S]. + */ + def sort[S >: Row : Ordering]: Table[S] = processRows(rs => (rs map (_.asInstanceOf[S])).toSeq.sorted) + + /** + * Method to shuffle this Table[Row]. + * + * TEST + * + * @return a Table[Row]. + */ + lazy val shuffle: Table[Row] = processRows(rs => Random.shuffle(rs)) /** - * drop - * - * TEST - * - * @param n the number of rows to drop. - * @return a Table like this Table but without its first n rows. - */ + * drop (as defined by Iterable). + * + * TEST + * + * @param n the number of rows to drop. + * @return a Table like this Table but without its first n rows. + */ override def drop(n: Int): Table[Row] = processRows(_.drop(n)) /** - * dropRight - * - * TEST - * - * @param n the number of rows to dropRight. - * @return a Table like this Table but with dropRight(n) rows. - */ + * dropRight (as defined by Iterable). + * + * TEST + * + * @param n the number of rows to dropRight. + * @return a Table like this Table but with dropRight(n) rows. + */ override def dropRight(n: Int): Table[Row] = processRows(_.dropRight(n)) /** - * dropWhile - * - * TEST - * - * @param p the predicate. - * @return a Table like this Table but with dropWhile(p) rows. - */ + * dropWhile (as defined by Iterable). + * + * TEST + * + * @param p the predicate. + * @return a Table like this Table but with dropWhile(p) rows. + */ override def dropWhile(p: Row => Boolean): Table[Row] = processRows(_.dropWhile(p)) /** - * Method to return an empty Table of type Row. - * - * TEST - * - * @return a Table[Row] without any rows. - */ + * Method to return an empty Table of type Row. + * + * TEST + * + * @return a Table[Row] without any rows. + */ override def empty: Table[Row] = unit(Seq.empty) /** - * Method to filter the rows of a table. - * - * TEST - * - * @param p a predicate to be applied to each row. - * @return a Table[Row] consisting only of rows which satisfy the predicate p. - */ + * Method to filter the rows of a table (as defined by Iterable). + * + * TEST + * + * @param p a predicate to be applied to each row. + * @return a Table[Row] consisting only of rows which satisfy the predicate p. + */ override def filter(p: Row => Boolean): Table[Row] = processRows(_.filter(p)) /** - * Method to filter out the rows of a table. - * - * TEST - * - * @param p a predicate to be applied to each row. - * @return a Table[Row] consisting only of rows which do not satisfy the predicate p. - */ + * Method to filter out the rows of a table (as defined by Iterable). + * + * TEST + * + * @param p a predicate to be applied to each row. + * @return a Table[Row] consisting only of rows which do not satisfy the predicate p. + */ override def filterNot(p: Row => Boolean): Table[Row] = processRows(_.filterNot(p)) /** - * slice - * - * TEST - * - * @param from the index at which to begin the slice. - * @param until the index at which to end the slice - * @return a Table like this Table but with slice(from, until) rows. - */ + * slice (as defined by Iterable). + * + * TEST + * + * @param from the index at which to begin the slice (1-based counting). + * @param until the index at which to end the slice (1-based counting). + * @return a Table like this Table but with slice(from, until) rows. + */ override def slice(from: Int, until: Int): Table[Row] = processRows(_.slice(from, until)) /** - * take - * - * @param n the number of rows to take. - * @return a Table like this Table but with only its first n rows. - */ + * take (as defined by Iterable). + * + * @param n the number of rows to take. + * @return a Table like this Table but with only its first n rows. + */ override def take(n: Int): Table[Row] = processRows(_.take(n)) /** - * takeRight - * - * TEST - * - * @param n the number of rows to takeRight. - * @return a Table like this Table but with takeRight(n) rows. - */ + * takeRight (as defined by Iterable). + * + * TEST + * + * @param n the number of rows to takeRight. + * @return a Table like this Table but with takeRight(n) rows. + */ override def takeRight(n: Int): Table[Row] = processRows(_.takeRight(n)) /** - * takeWhile - * - * TEST - * - * @param p the predicate. - * @return a Table like this Table but with takeWhile(p) rows. - */ + * takeWhile (as defined by Iterable). + * + * TEST + * + * @param p the predicate. + * @return a Table like this Table but with takeWhile(p) rows. + */ override def takeWhile(p: Row => Boolean): Table[Row] = processRows(_.takeWhile(p)) + /** + * Method to render this Table[T] as a CSV String with (maybe) header. + * + * @param renderer implicit value of CsvRenderer[Row]. + * @param generator implicit value of CsvProductGenerator[Row]. + * @param csvAttributes implicit value of CsvAttributes. + * @return a String. + */ + def toCSV(implicit renderer: CsvRenderer[Row], generator: CsvGenerator[Row], csvAttributes: CsvAttributes): String = + CsvTableStringRenderer[Row]().render(this).toString + + /** + * Method to render this Table[T] as a CSV file with (maybe) header. + * + * @param file instance of File where the output should be stored. + * @param renderer implicit value of CsvRenderer[Row]. + * @param generator implicit value of CsvProductGenerator[Row]. + * @param csvAttributes implicit value of CsvAttributes. + */ + def writeCSVFile(file: File)(implicit renderer: CsvRenderer[Row], generator: CsvGenerator[Row], csvAttributes: CsvAttributes): Unit = + CsvTableFileRenderer[Row](file).render(this) + def maybeColumnNames: Option[Seq[String]] = maybeHeader map (_.xs) def column(name: String): Iterator[Option[String]] + + private lazy val asOneBasedIndexedSequence = new IndexedSeq[Row]() { + def apply(i: Int): Row = rows.toIndexedSeq(i - 1) + + def length: Int = rows.size + } } object Table { - /** - * Primary method to parse a table from an Iterator of String. - * This method is, in turn, invoked by all other parse methods defined below (other than parseSequence). - * - * @param ws the Strings. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Primary method to parse a table from an Iterator of String. + * This method is, in turn, invoked by all other parse methods defined below (other than parseSequence). + * + * @param ws the Strings. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parse[T: TableParser](ws: Iterator[String]): Try[T] = implicitly[TableParser[T]] match { - case parser: StringTableParser[T] => parser.parse(ws) + case parser: StringTableParser[T] => parser.parse(ws, parser.headerRowsToRead) case x => Failure(ParserException(s"parse method for Seq[String] incompatible with tableParser: $x")) } /** - * Method to parse a table from an Iterable of String. - * - * @param ws the Strings. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Method to parse a table from an Iterable of String. + * + * @param ws the Strings. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parse[T: TableParser](ws: Iterable[String]): Try[T] = parse(ws.iterator) /** - * Method to parse a table from a Source. - * - * NOTE: the caller is responsible for closing the given Source. - * - * @param x the Source. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Method to parse a table from a Source. + * + * NOTE: the caller is responsible for closing the given Source. + * + * @param x the Source. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parse[T: TableParser](x: => Source): Try[T] = for (z <- Try(x.getLines()); y <- parse(z)) yield y /** - * Method to parse a table from a URI with an implicit encoding. - * - * @param u the URI. - * @param codec (implicit) the encoding. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Method to parse a table from a URI with an implicit encoding. + * + * @param u the URI. + * @param codec (implicit) the encoding. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parse[T: TableParser](u: => URI)(implicit codec: Codec): Try[T] = TryUsing(Source.fromURI(u))(parse(_)) /** - * Method to parse a table from a URI with an explicit encoding. - * - * TEST this - * - * @param u the URI. - * @param enc the encoding. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Method to parse a table from a URI with an explicit encoding. + * + * TEST this + * + * @param u the URI. + * @param enc the encoding. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parse[T: TableParser](u: => URI, enc: String): Try[T] = { implicit val codec: Codec = Codec(enc) parse(u) } /** - * Method to parse a table from an InputStream with an implicit encoding. - * - * @param i the InputStream (call-by-name). - * @param codec (implicit) the encoding. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Method to parse a table from an InputStream with an implicit encoding. + * + * @param i the InputStream (call-by-name). + * @param codec (implicit) the encoding. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parseInputStream[T: TableParser](i: => InputStream)(implicit codec: Codec): Try[T] = TryUsing(Source.fromInputStream(i))(parse(_)) /** - * Method to parse a table from an InputStream with an explicit encoding. - * - * TEST this - * - * @param i the InputStream. - * @param enc the encoding. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Method to parse a table from an InputStream with an explicit encoding. + * + * TEST this + * + * @param i the InputStream. + * @param enc the encoding. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parseInputStream[T: TableParser](i: => InputStream, enc: String): Try[T] = { implicit val codec: Codec = Codec(enc) parseInputStream(i) } /** - * Method to parse a table from a File. - * - * TEST this. - * - * @param f the File (call by name in case there is an exception thrown while constructing the file). - * @param enc the explicit encoding. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Method to parse a table from a File. + * + * TEST this. + * + * @param f the File (call by name in case there is an exception thrown while constructing the file). + * @param enc the explicit encoding. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parseFile[T: TableParser](f: => File, enc: String): Try[T] = { implicit val codec: Codec = Codec(enc) parseFile(f) } /** - * Method to parse a table from an File. - * - * @param f the File (call by name in case there is an exception thrown while constructing the file). - * @param codec (implicit) the encoding. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Method to parse a table from an File. + * + * @param f the File (call by name in case there is an exception thrown while constructing the file). + * @param codec (implicit) the encoding. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parseFile[T: TableParser](f: => File)(implicit codec: Codec): Try[T] = TryUsing(Source.fromFile(f))(parse(_)) /** - * Method to parse a table from a File. - * - * TEST this. - * - * @param pathname the file pathname. - * @param enc the explicit encoding. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Method to parse a table from a File. + * + * TEST this. + * + * @param pathname the file pathname. + * @param enc the explicit encoding. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parseFile[T: TableParser](pathname: String, enc: String): Try[T] = Try(parseFile(new File(pathname), enc)).flatten /** - * Method to parse a table from an File. - * - * TEST this. - * - * @param pathname the file pathname. - * @param codec (implicit) the encoding. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Method to parse a table from an File. + * + * TEST this. + * + * @param pathname the file pathname. + * @param codec (implicit) the encoding. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parseFile[T: TableParser](pathname: String)(implicit codec: Codec): Try[T] = Try(parseFile(new File(pathname))).flatten /** - * Method to parse a table from an File. - * - * @param s the resource name. - * @param clazz the class for which the resource should be sought (defaults to the calling class). - * @param codec (implicit) the encoding. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Method to parse a table from an File. + * + * @param s the resource name. + * @param clazz the class for which the resource should be sought (defaults to the calling class). + * @param codec (implicit) the encoding. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parseResource[T: TableParser](s: String, clazz: Class[_] = getClass)(implicit codec: Codec): Try[T] = TryUsing(Source.fromURL(clazz.getResource(s)))(parse(_)) /** - * Method to parse a table from a URL. - * - * @param u the URI. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Method to parse a table from a URL. + * + * @param u the URI. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parseResource[T: TableParser](u: => URL)(implicit codec: Codec): Try[T] = for (uri <- Try(u.toURI); t <- parse(uri)) yield t /** - * Method to parse a table from a URL with an explicit encoding. - * - * NOTE: the logic here is different from that of parseResource(u:=>URL)(implicit codec: Codec) above. - * - * @param u the URL. - * @param enc the encoding. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Method to parse a table from a URL with an explicit encoding. + * + * NOTE: the logic here is different from that of parseResource(u:=>URL)(implicit codec: Codec) above. + * + * @param u the URL. + * @param enc the encoding. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parseResource[T: TableParser](u: => URL, enc: String): Try[T] = TryUsing(Source.fromURL(u, enc))(parse(_)) /** - * Method to parse a table from a Seq of Seq of String. - * - * @param wss the Sequence of Strings. - * @tparam T the type of the resulting table. - * @return a Try[T] - */ + * Method to parse a table from a Seq of Seq of String. + * + * @param wss the Sequence of Strings. + * @tparam T the type of the resulting table. + * @return a Try[T] + */ def parseSequence[T: TableParser](wss: Iterator[Seq[String]]): Try[T] = { val tableParser = implicitly[TableParser[T]] tableParser match { - case parser: StringsTableParser[T] => parser.parse(wss) + case parser: StringsTableParser[T] => parser.parse(wss, 1) case _ => Failure(ParserException(s"parse method for Seq[Seq[String]] incompatible with tableParser: $tableParser")) } } /** - * Method to parse a table from a File as a table of Seq[String]. - * - * @param f the file. - * @param maybeFixedHeader an optional fixed header. If None (the default), we expect to find the header defined in the first line of the file. - * @param forgiving forcing (defaults to true). If true (the default) then an individual malformed row will not prevent subsequent rows being parsed. - * @param codec (implicit) the encoding. - * @return a Try of Table[RawRow] where RawRow is a Seq[String]. - */ + * Method to parse a table from a File as a table of Seq[String]. + * + * TEST + * + * @param f the file. + * @param maybeFixedHeader an optional fixed header. If None (the default), we expect to find the header defined in the first line of the file. + * @param forgiving forcing (defaults to true). If true (the default) then an individual malformed row will not prevent subsequent rows being parsed. + * @param codec (implicit) the encoding. + * @return a Try of Table[RawRow] where RawRow is a Seq[String]. + */ def parseFileRaw(f: File, predicate: Try[RawRow] => Boolean, maybeFixedHeader: Option[Header] = None, forgiving: Boolean = true)(implicit codec: Codec): Try[Table[RawRow]] = { implicit val z: TableParser[Table[RawRow]] = RawTableParser(predicate, maybeFixedHeader, forgiving) parseFile[Table[RawRow]](f) } /** - * Method to parse a table from a File as a table of Seq[String]. - * - * @param pathname the path name. - * @param codec (implicit) the encoding. - * @return a Try of Table[RawRow] where RawRow is a Seq[String]. - */ + * Method to parse a table from a File as a table of Seq[String]. + * + * TEST + * + * @param pathname the path name. + * @param codec (implicit) the encoding. + * @return a Try of Table[RawRow] where RawRow is a Seq[String]. + */ def parseFileRaw(pathname: String, predicate: Try[RawRow] => Boolean)(implicit codec: Codec): Try[Table[RawRow]] = { implicit val z: TableParser[Table[RawRow]] = RawTableParser(predicate, None) parseFile[Table[RawRow]](pathname) } /** - * Method to parse a table from a resource as a table of Seq[String]. - * - * NOTE no longer used. - * - * @param s the resource name. - * @param maybeFixedHeader an optional fixed header. If None (the default), we expect to find the header defined in the first line of the file. - * @param forgiving forcing (defaults to true). If true (the default) then an individual malformed row will not prevent subsequent rows being parsed. - * @param clazz the class for which the resource should be sought (defaults to the calling class). - * @param codec (implicit) the encoding. - * @return a Try of Table[RawRow] where RawRow is a Seq[String]. - */ + * Method to parse a table from a resource as a table of Seq[String]. + * + * NOTE no longer used. + * + * TEST + * + * @param s the resource name. + * @param maybeFixedHeader an optional fixed header. If None (the default), we expect to find the header defined in the first line of the file. + * @param forgiving forcing (defaults to true). If true (the default) then an individual malformed row will not prevent subsequent rows being parsed. + * @param clazz the class for which the resource should be sought (defaults to the calling class). + * @param codec (implicit) the encoding. + * @return a Try of Table[RawRow] where RawRow is a Seq[String]. + */ def parseResourceRaw(s: String, predicate: Try[RawRow] => Boolean = includeAll, maybeFixedHeader: Option[Header] = None, forgiving: Boolean = true, clazz: Class[_] = getClass)(implicit codec: Codec): Try[Table[RawRow]] = { implicit val z: TableParser[Table[RawRow]] = RawTableParser(predicate, maybeFixedHeader, forgiving) parseResource[Table[RawRow]](s, clazz) } /** - * Method to parse a table of raw rows from an Iterable of String. - * - * @param ws the Strings. - * @return a Try of Table[RawRow] - */ + * Method to parse a table of raw rows from an Iterable of String. + * + * @param ws the Strings. + * @return a Try of Table[RawRow] + */ def parseRaw(ws: Iterable[String], predicate: Try[RawRow] => Boolean = includeAll, maybeFixedHeader: Option[Header] = None, forgiving: Boolean = true, multiline: Boolean = true): Try[Table[RawRow]] = { implicit val z: TableParser[Table[RawRow]] = RawTableParser(predicate, maybeFixedHeader, forgiving, multiline) parse(ws.iterator) } /** - * Method to construct one of the standard table types, given an Iterable of T and an optional header. - * - * @param xs an Iterable of X. - * @param maybeHeader an optional Header. - * @tparam X the underlying type of xs. - * @return a Table[X] which is either a HeadedTable or an UnheadedTable, as appropriate. - */ + * Method to construct one of the standard table types, given an Iterable of T and an optional header. + * + * @param xs an Iterable of X. + * @param maybeHeader an optional Header. + * @tparam X the underlying type of xs. + * @return a Table[X] which is either a HeadedTable or an UnheadedTable, as appropriate. + */ def apply[X](xs: Iterable[X], maybeHeader: Option[Header]): Table[X] = maybeHeader match { case Some(h) => HeadedTable(xs, h) case None => UnheadedTable(xs) } + + /** + * Method to render the given Table[Row] as a CSV String with header. + * + * @param t the Table[Row] to be rendered. + * @param csvAttributes implicit value of CsvAttributes. + * @return an Iterable[String] + */ + def toCSVRow(t: Table[Row])(implicit csvAttributes: CsvAttributes): String = { + t.maybeHeader match { + case Some(hdr) => + implicit val z: CsvGenerator[Row] = Row.csvGenerator(hdr) + t.toCSV + case _ => throw TableException("toCSVRow: cannot write this Table to CSV (no header)") + } + } + + /** + * Method to render the given Table[Row] as a CSV String with header. + * + * TEST + * + * @param t the Table[Row] to be rendered. + * @param file the destination File for the rendering of t. + * @param csvAttributes implicit value of CsvAttributes. + * @return an Iterable[String] + */ + def writeCSVFileRow(t: Table[Row], file: File)(implicit csvAttributes: CsvAttributes): Unit = { + t.maybeHeader match { + case Some(hdr) => + implicit val z: CsvGenerator[Row] = Row.csvGenerator(hdr) + t.writeCSVFile(file) + case _ => throw TableException("writeCSVFileRow: cannot write this Table to CSV (no header)") + } + } } /** - * CONSIDER eliminating this base class - * - * @param rows the rows of the table - * @param maybeHeader (optional) header - * @tparam Row the underlying type of each Row - */ + * CONSIDER eliminating this base class + * + * @param rows the rows of the table + * @param maybeHeader (optional) header + * @tparam Row the underlying type of each Row + */ abstract class RenderableTable[Row](rows: Iterable[Row], val maybeHeader: Option[Header]) extends Table[Row] with Renderable[Row] { self => /** - * - * @param ev implicit evidence for Renderer of Table of X. - * @tparam O the type of the result. - * @return an instance of O. - */ + * + * @param ev implicit evidence for Renderer of Table of X. + * @tparam O the type of the result. + * @return an instance of O. + */ def render[O](implicit ev: Renderer[Table[Row], O]): O = ev.render(this) /** - * CONSIDER redefining the definition of Renderer such that we can accommodate the various different types of output. - * - * Method to render a table in a sequential (serialized) fashion. - * - * @tparam O a type which supports Writable (via evidence of type Writable[O]) - * @return a new (or possibly old) instance of O. - */ - def RenderToWritable[O: Writable]: O = { + * CONSIDER redefining the definition of Renderer such that we can accommodate the various different types of output. + * + * Method to render a table in a sequential (serialized) fashion. + * + * @tparam O a type which supports Writable (via evidence of type Writable[O]) + * @return a new (or possibly old) instance of O. + */ + def renderToWritable[O: Writable]: O = { val ww = implicitly[Writable[O]] val o1 = ww.unit val o2 = (maybeHeader map (h => ww.writeRaw(ww.writeRowElements(o1)(h.xs))(ww.newline))).getOrElse(o1) @@ -552,17 +658,17 @@ abstract class RenderableTable[Row](rows: Iterable[Row], val maybeHeader: Option } /** - * Method to render a table in a hierarchical fashion. - * - * NOTE: if your output structure is not hierarchical in nature, then simply loop through the rows of this table, - * outputting each row as you go. - * - * @param style the "style" to be used for the node which will represent this table. - * @param attributes the attributes to be applied to the top level node for this table. - * @param xr an (implicit) HierarchicalRenderer[Row] - * @tparam U a class which supports TreeWriter (i.e. there is evidence of TreeWriter[U]). - * @return a new instance of U which represents this Table as a tree of some sort. - */ + * Method to render a table in a hierarchical fashion. + * + * NOTE: if your output structure is not hierarchical in nature, then simply loop through the rows of this table, + * outputting each row as you go. + * + * @param style the "style" to be used for the node which will represent this table. + * @param attributes the attributes to be applied to the top level node for this table. + * @param xr an (implicit) HierarchicalRenderer[Row] + * @tparam U a class which supports TreeWriter (i.e. there is evidence of TreeWriter[U]). + * @return a new instance of U which represents this Table as a tree of some sort. + */ def renderHierarchical[U: TreeWriter](style: String, attributes: Map[String, String] = Map())(implicit xr: HierarchicalRenderer[Row]): U = { object TableRenderers extends HierarchicalRenderers { val rowsRenderer: HierarchicalRenderer[Seq[Row]] = sequenceRenderer[Row]("tbody") @@ -575,17 +681,17 @@ abstract class RenderableTable[Row](rows: Iterable[Row], val maybeHeader: Option } /** - * Method to render a table in a hierarchical fashion. - * - * NOTE: if your output structure is not hierarchical in nature, then simply loop through the rows of this table, - * outputting each row as you go. - * - * @param style the "style" to be used for the node which will represent this table. - * @param attributes the attributes to be applied to the top level node for this table. - * @param rr an (implicit) HierarchicalRenderer[ Indexed [ Row ] ] - * @tparam U a class which supports TreeWriter (i.e. there is evidence of TreeWriter[U]). - * @return a new instance of U which represents this Table as a tree of some sort. - */ + * Method to render a table in a hierarchical fashion. + * + * NOTE: if your output structure is not hierarchical in nature, then simply loop through the rows of this table, + * outputting each row as you go. + * + * @param style the "style" to be used for the node which will represent this table. + * @param attributes the attributes to be applied to the top level node for this table. + * @param rr an (implicit) HierarchicalRenderer[ Indexed [ Row ] ] + * @tparam U a class which supports TreeWriter (i.e. there is evidence of TreeWriter[U]). + * @return a new instance of U which represents this Table as a tree of some sort. + */ def renderHierarchicalSequenced[U: TreeWriter](style: String, attributes: Map[String, String] = Map())(implicit rr: HierarchicalRenderer[Indexed[Row]]): U = { object TableRenderers extends HierarchicalRenderers { val rowsRenderer: HierarchicalRenderer[Seq[Indexed[Row]]] = sequenceRenderer[Indexed[Row]]("tbody") @@ -601,25 +707,25 @@ abstract class RenderableTable[Row](rows: Iterable[Row], val maybeHeader: Option } /** - * Concrete case class implementing RenderableTable without a Header. - * - * NOTE: the existence or not of a Header in a RenderableTable only affects how the table is rendered. - * The parsing of a table always has a header of some sort. - * - * TEST this: it is currently not used. - * - * @param rows the rows of the table. - * @tparam Row the underlying type of each Row - */ + * Concrete case class implementing RenderableTable without a Header. + * + * NOTE: the existence or not of a Header in a RenderableTable only affects how the table is rendered. + * The parsing of a table always has a header of some sort. + * + * TEST this: it is currently not used. + * + * @param rows the rows of the table. + * @tparam Row the underlying type of each Row + */ case class UnheadedTable[Row](rows: Iterable[Row]) extends RenderableTable[Row](rows, None) { /** - * Method to generate a Table[S] for a set of rows. - * Although declared as an instance method, this method produces its result independent of this. - * - * @param rows a sequence of S. - * @tparam S the underlying type of the rows and the result. - * @return a new instance of Table[S]. - */ + * Method to generate a Table[S] for a set of rows. + * Although declared as an instance method, this method produces its result independent of this. + * + * @param rows a sequence of S. + * @tparam S the underlying type of the rows and the result. + * @return a new instance of Table[S]. + */ override def unit[S](rows: Iterable[S], maybeHeader: Option[Header]): Table[S] = maybeHeader match { case Some(h) => HeadedTable(rows, h) case None => UnheadedTable(rows) @@ -629,25 +735,25 @@ case class UnheadedTable[Row](rows: Iterable[Row]) extends RenderableTable[Row]( } /** - * Concrete case class implementing RenderableTable with a Header. - * The unit and apply methods are such that rows is in fact an Array[Row] (??). - * - * NOTE: the existence or not of a Header in a RenderableTable only affects how the table is rendered. - * The parsing of a table always has a header of some sort. - * - * @param rows the rows of the table, stored as an Array. - * @param header the header. - * @tparam Row the underlying type of each Row - */ + * Concrete case class implementing RenderableTable with a Header. + * The unit and apply methods are such that rows is in fact an Array[Row] (??). + * + * NOTE: the existence or not of a Header in a RenderableTable only affects how the table is rendered. + * The parsing of a table always has a header of some sort. + * + * @param rows the rows of the table, stored as an Array. + * @param header the header. + * @tparam Row the underlying type of each Row + */ case class HeadedTable[Row](rows: Iterable[Row], header: Header) extends RenderableTable[Row](rows, Some(header)) { /** - * Method to generate a Table[S] for a set of rows. - * Although declared as an instance method, this method produces its result independent of this. - * - * @param rows a sequence of S. - * @tparam S the underlying type of the rows and the result. - * @return a new instance of Table[S]. - */ + * Method to generate a Table[S] for a set of rows. + * Although declared as an instance method, this method produces its result independent of this. + * + * @param rows a sequence of S. + * @tparam S the underlying type of the rows and the result. + * @return a new instance of Table[S]. + */ override def unit[S](rows: Iterable[S], maybeHeader: Option[Header]): Table[S] = maybeHeader match { case Some(h) => HeadedTable(rows, h) case None => UnheadedTable(rows) @@ -663,10 +769,10 @@ case class HeadedTable[Row](rows: Iterable[Row], header: Header) extends Rendera } /** - * Companion object for HeadedTable. - * The apply methods provided arbitrarily use Vector as the collection for the rows of the table. - * CONSIDER using something else such as Array. - */ + * Companion object for HeadedTable. + * The apply methods provided arbitrarily use Vector as the collection for the rows of the table. + * CONSIDER using something else such as Array. + */ object HeadedTable { def apply[Row: ClassTag](rows: Iterator[Row], header: Header): Table[Row] = HeadedTable(rows.to(List), header) @@ -674,35 +780,36 @@ object HeadedTable { } /** - * Case class to represent a header. - * - * @param xs the sequence of column names. - */ -case class Header(xs: Seq[String]) { - /** - * Get the number of columns. - * - * @return the size of the header sequence. - */ + * Case class to represent a header. + * + * @param xs the sequence of column names. + * @param xss a sequence of sequence of String representing any additional header lines. + */ +case class Header(xs: Seq[String], xss: Seq[Seq[String]]) { + /** + * Get the number of columns. + * + * @return the size of the header sequence. + */ def size: Int = xs.length /** - * Get the index of w in this Header, ignoring case. - * - * @param w the String to find, a column name. - * @return the index wrapped in Try. - */ + * Get the index of w in this Header, ignoring case. + * + * @param w the String to find, a column name. + * @return the index wrapped in Try. + */ def getIndex(w: String): Try[Int] = indexFound(w, xs.indexWhere(x => x.compareToIgnoreCase(w) == 0)) /** - * Concatenate this Header with other. - * - * TEST this. - * - * @param other the other Header. - * @return a Header made up of these columns and those of other, in that order. - */ - def ++(other: Header): Header = Header(xs ++ other.xs) + * Concatenate this Header with other. + * + * TEST this. + * + * @param other the other Header. + * @return a Header made up of these columns and those of other, in that order. + */ + def ++(other: Header): Header = Header(xs ++ other.xs, for (xs <- xss; ys <- other.xss) yield xs ++ ys) } object Header { @@ -744,40 +851,51 @@ object Header { def prepend(prefix: String, stream: LazyList[String]): LazyList[String] = stream map (prefix + _) /** - * This method constructs a new Header based on Excel row/column names. - * - * @param letters true if we want the sequence A B C D E ... Z AA AB ... BA BB ... - * false is we just want numbers. - * @return a - */ - def apply(letters: Boolean, length: Int): Header = Header(numbers map intToString(letters) take length toList) - - /** - * This method constructs a new Header based on the fields of the class X. - * It should not be considered to be the normal method of generating the header for a table. - * It is mainly used by unit tests. - * - * NOTE: this method does NOT recurse into any fields which happen also to be a class with fields. - * - * @tparam X a class X which is not necessarily a Product. - * @return a List of field names. - */ - def apply[X: ClassTag](): Header = Header(Reflection.extractFieldNames(implicitly[ClassTag[X]]).toList) - - /** - * Create a Header from a variable list of parameters. - * - * @param ws a variable list of Strings. - * @return a Header. - */ - def create(ws: String*): Header = apply(ws) + * This method constructs a new Header based on a header spanning several lines. + * Only the first becomes the true header, the remainder become the "preface." + * + * @param wss a sequence of sequence of String. + * @return a Header. + */ + def apply(wss: Seq[Seq[String]]): Header = apply(wss.head, wss.tail) + + /** + * This method constructs a new Header based on Excel row/column names. + * + * @param letters true if we want the sequence A B C D E ... Z AA AB ... BA BB ... + * false is we just want numbers. + * @return a + */ + def apply(letters: Boolean, length: Int): Header = Header(numbers map intToString(letters) take length toList, Nil) + + /** + * This method constructs a new Header based on the fields of the class X. + * It should not be considered to be the normal method of generating the header for a table. + * It is mainly used by unit tests. + * + * NOTE: this method does NOT recurse into any fields which happen also to be a class with fields. + * + * @tparam X a class X which is not necessarily a Product. + * @return a List of field names. + */ + def apply[X: ClassTag](): Header = Header(Reflection.extractFieldNames(implicitly[ClassTag[X]]).toList, Nil) + + /** + * Create a Header from a variable list of parameters. + * + * @param ws a variable list of Strings. + * @return a Header. + */ + def create(ws: String*): Header = apply(ws, Nil) } /** - * Table Exception. - * - * @param w the message. - */ + * Table Exception. + * + * TEST + * + * @param w the message. + */ case class TableException(w: String) extends Exception(w) diff --git a/src/main/scala/com/phasmidsoftware/table/TableJsonFormat.scala b/src/main/scala/com/phasmidsoftware/table/TableJsonFormat.scala index 188769e4..37502b00 100644 --- a/src/main/scala/com/phasmidsoftware/table/TableJsonFormat.scala +++ b/src/main/scala/com/phasmidsoftware/table/TableJsonFormat.scala @@ -3,6 +3,11 @@ package com.phasmidsoftware.table import spray.json.DefaultJsonProtocol._ import spray.json.{JsArray, JsObject, JsValue, JsonFormat, RootJsonFormat, enrichAny} +/** + * Abstract class TableJsonFormat[T] which extends RootJsonFormat of Table[T] + * + * @tparam T which must provide evidence of JsonFormat[T]. + */ abstract class TableJsonFormat[T: JsonFormat] extends RootJsonFormat[Table[T]] { def write(obj: Table[T]): JsValue = { val jso = obj.maybeHeader map (h => h.xs.map(_.toJson)) @@ -16,7 +21,9 @@ abstract class TableJsonFormat[T: JsonFormat] extends RootJsonFormat[Table[T]] { def read(json: JsValue): Table[T] = json.asJsObject("JsonObject expected to represent Table") match { case JsObject(fields) => - val ho: Option[Header] = fields.get("header") map (j => Header(j.convertTo[List[String]])) + def createHeader(j: JsValue): Header = Header(j.convertTo[Seq[String]], Seq[Seq[String]]()) + + val ho: Option[Header] = fields.get("header") map createHeader val tso: Option[Iterable[T]] = fields.get("rows") map (j => j.convertTo[List[T]]) tso match { case Some(ts) => Table(ts, ho) diff --git a/src/main/scala/com/phasmidsoftware/table/Transformation.scala b/src/main/scala/com/phasmidsoftware/table/Transformation.scala index df798b6b..bb2679c4 100644 --- a/src/main/scala/com/phasmidsoftware/table/Transformation.scala +++ b/src/main/scala/com/phasmidsoftware/table/Transformation.scala @@ -6,6 +6,13 @@ package com.phasmidsoftware.table import com.phasmidsoftware.RawRow +/** + * Trait which transforms an X into a Y. + * Basically, this is just a sub-class of Function1. + * + * @tparam X the input type. + * @tparam Y the output type. + */ trait Transformation[X, Y] extends (X => Y) case class RawTableTransformation(transformers: Map[String, Transformation[String, String]]) extends Transformation[RawTable, RawTable] { @@ -13,15 +20,13 @@ case class RawTableTransformation(transformers: Map[String, Transformation[Strin val xm: Map[Int, Transformation[String, String]] = for ((k, x) <- transformers; h <- t.maybeHeader; index <- h.getIndex(k).toOption) yield (index, x) t.map[RawRow](RawRowTransformation(xm)) } - - // def map[Z](f: RawTable => Z): Transformation[RawTable, Z] = f compose apply } /** - * TEST this - * - * @param aggregators a Map of Transformations indexed by String. - */ + * TEST this + * + * @param aggregators a Map of Transformations indexed by String. + */ case class RawTableAggregation(aggregators: Map[String, Transformation[String, String]]) extends Transformation[RawTable, RawTable] { def apply(t: RawTable): RawTable = { val header = t.maybeHeader.get // there must be a header for a raw table. diff --git a/src/main/scala/com/phasmidsoftware/util/FP.scala b/src/main/scala/com/phasmidsoftware/util/FP.scala index 345149b4..26901fbe 100644 --- a/src/main/scala/com/phasmidsoftware/util/FP.scala +++ b/src/main/scala/com/phasmidsoftware/util/FP.scala @@ -9,126 +9,133 @@ import scala.reflect.ClassTag import scala.util.Using.Releasable import scala.util.{Failure, Success, Try, Using} +/** + * Various utilities for functional programming. + */ object FP { /** - * Sequence method to combine elements of Try. - * - * @param xys an Iterator of Try[X] - * @tparam X the underlying type - * @return a Try of Iterator[X] - */ + * Sequence method to combine elements of Try. + * + * @param xys an Iterator of Try[X] + * @tparam X the underlying type + * @return a Try of Iterator[X] + */ def sequence[X](xys: Iterator[Try[X]]): Try[Iterator[X]] = sequence(xys.to(List)).map(_.iterator) /** - * Sequence method to combine elements of Try. - * - * @param xos an Iterator of Try[X] - * @tparam X the underlying type - * @return a Try of Iterator[X] - */ + * Sequence method to combine elements of Try. + * + * @param xos an Iterator of Try[X] + * @tparam X the underlying type + * @return a Try of Iterator[X] + */ def sequence[X](xos: Iterator[Option[X]]): Option[Iterator[X]] = sequence(xos.to(List)).map(_.iterator) /** - * Sequence method to combine elements of Try. - * - * @param xys an Iterable of Try[X] - * @tparam X the underlying type - * @return a Try of Seq[X] - * NOTE: that the output collection type will be Seq, regardless of the input type - */ + * Sequence method to combine elements of Try. + * + * @param xys an Iterable of Try[X] + * @tparam X the underlying type + * @return a Try of Seq[X] + * NOTE: that the output collection type will be Seq, regardless of the input type + */ def sequence[X](xys: Iterable[Try[X]]): Try[Seq[X]] = xys.foldLeft(Try(Seq[X]())) { (xsy, xy) => for (xs <- xsy; x <- xy) yield xs :+ x } /** - * Sequence method to combine elements of Try. - * - * @param xos an Iterable of Option[X] - * @tparam X the underlying type - * @return an Option of Seq[X] - * NOTE: that the output collection type will be Seq, regardless of the input type - */ + * Sequence method to combine elements of Try. + * + * @param xos an Iterable of Option[X] + * @tparam X the underlying type + * @return an Option of Seq[X] + * NOTE: that the output collection type will be Seq, regardless of the input type + */ def sequence[X](xos: Iterable[Option[X]]): Option[Seq[X]] = xos.foldLeft(Option(Seq[X]())) { (xso, xo) => for (xs <- xso; x <- xo) yield xs :+ x } /** - * Method to partition an method to combine elements of Try. - * - * @param xys an Iterator of Try[X] - * @tparam X the underlying type - * @return a tuple of two iterators of Try[X], the first one being successes, the second one being failures. - */ + * Method to partition an method to combine elements of Try. + * + * @param xys an Iterator of Try[X] + * @tparam X the underlying type + * @return a tuple of two iterators of Try[X], the first one being successes, the second one being failures. + */ def partition[X](xys: Iterator[Try[X]]): (Iterator[Try[X]], Iterator[Try[X]]) = xys.partition(_.isSuccess) /** - * Method to partition an method to combine elements of Try. - * - * @param xys a Seq of Try[X] - * @tparam X the underlying type - * @return a tuple of two Seqs of Try[X], the first one being successes, the second one being failures. - */ + * Method to partition an method to combine elements of Try. + * + * TEST + * + * @param xys a Seq of Try[X] + * @tparam X the underlying type + * @return a tuple of two Seqs of Try[X], the first one being successes, the second one being failures. + */ def partition[X](xys: Seq[Try[X]]): (Seq[Try[X]], Seq[Try[X]]) = xys.partition(_.isSuccess) /** - * Method to yield a URL for a given resourceForClass in the classpath for C. - * - * @param resourceName the name of the resourceForClass. - * @tparam C a class of the package containing the resourceForClass. - * @return a Try[URL]. - */ + * Method to yield a URL for a given resourceForClass in the classpath for C. + * + * @param resourceName the name of the resourceForClass. + * @tparam C a class of the package containing the resourceForClass. + * @return a Try[URL]. + */ def resource[C: ClassTag](resourceName: String): Try[URL] = resourceForClass(resourceName, implicitly[ClassTag[C]].runtimeClass) /** - * Method to yield a Try[URL] for a resource name and a given class. - * - * @param resourceName the name of the resource. - * @param clazz the class, relative to which, the resource can be found (defaults to the caller's class). - * @return a Try[URL] - */ + * Method to yield a Try[URL] for a resource name and a given class. + * + * @param resourceName the name of the resource. + * @param clazz the class, relative to which, the resource can be found (defaults to the caller's class). + * @return a Try[URL] + */ def resourceForClass(resourceName: String, clazz: Class[_] = getClass): Try[URL] = Option(clazz.getResource(resourceName)) match { case Some(u) => Success(u) case None => Failure(FPException(s"$resourceName is not a valid resource for $clazz")) } /** - * Method to determine if the String w was found at a valid index (i). - * - * @param w the String (ignored unless there's an exception). - * @param i the index found. - * @return Success(i) if all well, else Failure(exception). - */ + * Method to determine if the String w was found at a valid index (i). + * + * @param w the String (ignored unless there's an exception). + * @param i the index found. + * @return Success(i) if all well, else Failure(exception). + */ def indexFound(w: String, i: Int): Try[Int] = i match { case x if x >= 0 => Success(x) - case _ => Failure(FPException(s"Header column $w not found")) + case _ => Failure(FPException(s"Header column '$w' not found")) } } object TryUsing { /** - * This method is to Using.apply as flatMap is to Map. - * - * @param resource a resource which is used by f and will be managed via Using.apply - * @param f a function of R => Try[A]. - * @tparam R the resource type. - * @tparam A the underlying type of the result. - * @return a Try[A] - */ + * This method is to Using.apply as flatMap is to Map. + * + * @param resource a resource which is used by f and will be managed via Using.apply + * @param f a function of R => Try[A]. + * @tparam R the resource type. + * @tparam A the underlying type of the result. + * @return a Try[A] + */ def apply[R: Releasable, A](resource: => R)(f: R => Try[A]): Try[A] = Using(resource)(f).flatten /** - * This method is similar to apply(r) but it takes a Try[R] as its parameter. - * The definition of f is the same as in the other apply, however. - * - * @param ry a Try[R] which is passed into f and will be managed via Using.apply - * @param f a function of R => Try[A]. - * @tparam R the resource type. - * @tparam A the underlying type of the result. - * @return a Try[A] - */ + * This method is similar to apply(r) but it takes a Try[R] as its parameter. + * The definition of f is the same as in the other apply, however. + * + * TEST + * + * @param ry a Try[R] which is passed into f and will be managed via Using.apply + * @param f a function of R => Try[A]. + * @tparam R the resource type. + * @tparam A the underlying type of the result. + * @return a Try[A] + */ def tryIt[R: Releasable, A](ry: => Try[R])(f: R => Try[A]): Try[A] = for (r <- ry; a <- apply(r)(f)) yield a } diff --git a/src/main/scala/com/phasmidsoftware/util/FunctionIterator.scala b/src/main/scala/com/phasmidsoftware/util/FunctionIterator.scala index a053a4d5..6cd3dce6 100644 --- a/src/main/scala/com/phasmidsoftware/util/FunctionIterator.scala +++ b/src/main/scala/com/phasmidsoftware/util/FunctionIterator.scala @@ -10,12 +10,12 @@ import scala.collection.AbstractIterator import scala.util.{Failure, Success, Try} /** - * This iterator gets its input from a call-by-name value, which is essentially a parameterless function. - * - * @param f the call-by-name value. - * @tparam X a Joinable input type. - * @tparam R the underlying type of f and also the result. - */ + * This iterator gets its input from a call-by-name value, which is essentially a parameterless function. + * + * @param f the call-by-name value. + * @tparam X a Joinable input type. + * @tparam R the underlying type of f and also the result. + */ class FunctionIterator[X: Joinable, R](f: X => Try[R])(xs: Iterator[X]) extends AbstractIterator[Try[R]] { private val xj = implicitly[Joinable[X]] diff --git a/src/main/scala/com/phasmidsoftware/util/Joinable.scala b/src/main/scala/com/phasmidsoftware/util/Joinable.scala index a24ed7c9..377af2e6 100644 --- a/src/main/scala/com/phasmidsoftware/util/Joinable.scala +++ b/src/main/scala/com/phasmidsoftware/util/Joinable.scala @@ -5,31 +5,31 @@ package com.phasmidsoftware.util /** - * Type class to define behavior of a type which can be joined. - * - * @tparam T the joinable type. - */ + * Type class to define behavior of a type which can be joined. + * + * @tparam T the joinable type. + */ trait Joinable[T] { /** - * Method to join t1 to t2. - * - * @param t1 the first T value. - * @param t2 the second T value. - * @return the joined T value. - */ + * Method to join t1 to t2. + * + * @param t1 the first T value. + * @param t2 the second T value. + * @return the joined T value. + */ def join(t1: T, t2: T): T /** - * The zero value for T. - */ + * The zero value for T. + */ val zero: T /** - * Method to determine if t is valid. - * - * @param t a T value. - * @return true if t is valid. - */ + * Method to determine if t is valid. + * + * @param t a T value. + * @return true if t is valid. + */ def valid(t: T): Boolean } diff --git a/src/main/scala/com/phasmidsoftware/util/Reflection.scala b/src/main/scala/com/phasmidsoftware/util/Reflection.scala index c3cab51b..b8bb826c 100644 --- a/src/main/scala/com/phasmidsoftware/util/Reflection.scala +++ b/src/main/scala/com/phasmidsoftware/util/Reflection.scala @@ -5,23 +5,22 @@ package com.phasmidsoftware.util import java.lang.reflect.Modifier - import scala.reflect.ClassTag import scala.util.control.NonFatal object Reflection { /** - * This method is borrowed directly from Spray ProductFormats. - * - * NOTE: Read this if you are getting exceptions thrown by this method (be aware that sys.error throws an exception). - * You MUST be careful when defining case classes that represent input types or output types. - * DO NOT use val or lazy val instead of def for additional instance methods. - * That is because they mess up the matching of the fields. - * - * @param classTag rhw class tag. - * @return an Array of String. - */ + * This method is borrowed directly from Spray ProductFormats. + * + * NOTE: Read this if you are getting exceptions thrown by this method (be aware that sys.error throws an exception). + * You MUST be careful when defining case classes that represent input types or output types. + * DO NOT use val or lazy val instead of def for additional instance methods. + * That is because they mess up the matching of the fields. + * + * @param classTag rhw class tag. + * @return an Array of String. + */ def extractFieldNames(classTag: ClassTag[_]): Array[String] = { val clazz = classTag.runtimeClass try { @@ -40,7 +39,7 @@ object Reflection { fields.map(f => f.getName) } catch { case NonFatal(ex) => throw new RuntimeException("Cannot automatically determine case class field names and order " + - "for '" + clazz.getName + "' (Did you use val instead of def for a method in this case class?), please use the 'cellParser'N signature with explicit field name specification", ex) + "for '" + clazz.getName + "' (Did you use val instead of def for a method in this case class?), please use the 'cellParser'N signature with explicit field name specification", ex) } } diff --git a/src/main/scala/com/phasmidsoftware/util/TeeIterator.scala b/src/main/scala/com/phasmidsoftware/util/TeeIterator.scala new file mode 100644 index 00000000..ac71f68a --- /dev/null +++ b/src/main/scala/com/phasmidsoftware/util/TeeIterator.scala @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2019. Phasmid Software + */ + +package com.phasmidsoftware.util + +/** + * This iterator gets its input from a call-by-name value, which is essentially a parameterless function. + * + * @param n the number of elements to detach. + * @tparam X the underlying type. + */ +class TeeIterator[X](n: Int)(xs: Iterator[X]) extends Iterator[X] { + val tee: Seq[X] = for (_ <- 0 until n if xs.hasNext) yield xs.next() + + def hasNext: Boolean = xs.hasNext + + def next(): X = xs.next() +} diff --git a/src/test/scala/com/phasmidsoftware/parse/CellParserSpec.scala b/src/test/scala/com/phasmidsoftware/parse/CellParserSpec.scala index 2b128d5d..259027cb 100644 --- a/src/test/scala/com/phasmidsoftware/parse/CellParserSpec.scala +++ b/src/test/scala/com/phasmidsoftware/parse/CellParserSpec.scala @@ -7,7 +7,6 @@ package com.phasmidsoftware.parse import com.phasmidsoftware.table.{Header, Row} import org.scalatest.flatspec import org.scalatest.matchers.should - import scala.util.{Failure, Success, Try} //noinspection NotImplementedCode diff --git a/src/test/scala/com/phasmidsoftware/parse/CellParsersSpec.scala b/src/test/scala/com/phasmidsoftware/parse/CellParsersSpec.scala index ca554910..8dc6d43d 100644 --- a/src/test/scala/com/phasmidsoftware/parse/CellParsersSpec.scala +++ b/src/test/scala/com/phasmidsoftware/parse/CellParsersSpec.scala @@ -9,7 +9,6 @@ import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat import org.scalatest.flatspec import org.scalatest.matchers.should - import scala.util.{Failure, Success} class CellParsersSpec extends flatspec.AnyFlatSpec with should.Matchers { @@ -67,8 +66,8 @@ class CellParsersSpec extends flatspec.AnyFlatSpec with should.Matchers { } /** - * NOTE: only used in testing currently - */ + * NOTE: only used in testing currently + */ object IntSeqParser extends CellParsers { implicit val intSeqParser: CellParser[Seq[Int]] = cellParserSeq diff --git a/src/test/scala/com/phasmidsoftware/parse/ImplicitParserSpec.scala b/src/test/scala/com/phasmidsoftware/parse/ImplicitParserSpec.scala index 658e0de1..117eb397 100644 --- a/src/test/scala/com/phasmidsoftware/parse/ImplicitParserSpec.scala +++ b/src/test/scala/com/phasmidsoftware/parse/ImplicitParserSpec.scala @@ -2,7 +2,6 @@ package com.phasmidsoftware.parse import org.scalatest.flatspec import org.scalatest.matchers.should - import scala.io.BufferedSource import scala.util.Failure @@ -10,7 +9,6 @@ class ImplicitParserSpec extends flatspec.AnyFlatSpec with should.Matchers { import com.phasmidsoftware.table.MovieParser.MovieTableParser import com.phasmidsoftware.table.Table - import scala.io.Source import scala.util.Success diff --git a/src/test/scala/com/phasmidsoftware/parse/LineParserSpec.scala b/src/test/scala/com/phasmidsoftware/parse/LineParserSpec.scala index daec3fd3..344d3285 100644 --- a/src/test/scala/com/phasmidsoftware/parse/LineParserSpec.scala +++ b/src/test/scala/com/phasmidsoftware/parse/LineParserSpec.scala @@ -6,7 +6,6 @@ package com.phasmidsoftware.parse import org.scalatest.flatspec import org.scalatest.matchers.should - import scala.util.Success class LineParserSpec extends flatspec.AnyFlatSpec with should.Matchers { diff --git a/src/test/scala/com/phasmidsoftware/parse/ParseableSpec.scala b/src/test/scala/com/phasmidsoftware/parse/ParseableSpec.scala index 7739446a..fc1f7062 100644 --- a/src/test/scala/com/phasmidsoftware/parse/ParseableSpec.scala +++ b/src/test/scala/com/phasmidsoftware/parse/ParseableSpec.scala @@ -6,7 +6,6 @@ package com.phasmidsoftware.parse import org.scalatest.flatspec import org.scalatest.matchers.should - import scala.util.Success class ParseableSpec extends flatspec.AnyFlatSpec with should.Matchers { diff --git a/src/test/scala/com/phasmidsoftware/parse/RawParsersSpec.scala b/src/test/scala/com/phasmidsoftware/parse/RawParsersSpec.scala index aa863264..5f728a83 100644 --- a/src/test/scala/com/phasmidsoftware/parse/RawParsersSpec.scala +++ b/src/test/scala/com/phasmidsoftware/parse/RawParsersSpec.scala @@ -7,7 +7,6 @@ package com.phasmidsoftware.parse import com.phasmidsoftware.table.{HeadedTable, RawTable, Table} import org.scalatest.flatspec import org.scalatest.matchers.should - import scala.util.{Success, Try} class RawParsersSpec extends flatspec.AnyFlatSpec with should.Matchers { @@ -19,7 +18,7 @@ class RawParsersSpec extends flatspec.AnyFlatSpec with should.Matchers { val rows = Seq( "color,director_name,num_critic_for_reviews,duration,director_facebook_likes,actor_3_facebook_likes,actor_2_name,actor_1_facebook_likes,gross,genres,actor_1_name,movie_title,num_voted_users,cast_total_facebook_likes,actor_3_name,facenumber_in_poster,plot_keywords,movie_imdb_link,num_user_for_reviews,language,country,content_rating,budget,title_year,actor_2_facebook_likes,imdb_score,aspect_ratio,movie_facebook_likes", - ",Doug Walker,,,131,,Rob Walker,131,,Documentary,Doug Walker,Star Wars: Episode VII - The Force Awakens  ,8,143,,0,,http://www.imdb.com/title/tt5289954/?ref_=fn_tt_tt_1,,,,,,,12,7.1,,0" + ",Doug Walker,,,131,,Rob Walker,131,,Documentary,Doug Walker,Star Wars: Episode VII - The Force Awakens  ,8,143,,0,,https://www.imdb.com/title/tt5289954/?ref_=fn_tt_tt_1,,,,,,,12,7.1,,0" ) val mty: Try[RawTable] = Table.parse(rows) diff --git a/src/test/scala/com/phasmidsoftware/parse/RowParserSpec.scala b/src/test/scala/com/phasmidsoftware/parse/RowParserSpec.scala index 90d3b61c..350ca229 100644 --- a/src/test/scala/com/phasmidsoftware/parse/RowParserSpec.scala +++ b/src/test/scala/com/phasmidsoftware/parse/RowParserSpec.scala @@ -7,7 +7,6 @@ package com.phasmidsoftware.parse import com.phasmidsoftware.table.Header import org.scalatest.flatspec import org.scalatest.matchers.should - import scala.util.matching.Regex import scala.util.{Success, Try} diff --git a/src/test/scala/com/phasmidsoftware/parse/StringsParserSpec.scala b/src/test/scala/com/phasmidsoftware/parse/StringsParserSpec.scala index b0f88067..7f155ac4 100644 --- a/src/test/scala/com/phasmidsoftware/parse/StringsParserSpec.scala +++ b/src/test/scala/com/phasmidsoftware/parse/StringsParserSpec.scala @@ -7,7 +7,6 @@ package com.phasmidsoftware.parse import com.phasmidsoftware.table.Header import org.scalatest.flatspec import org.scalatest.matchers.should - import scala.util.{Success, Try} class StringsParserSpec extends flatspec.AnyFlatSpec with should.Matchers { diff --git a/src/test/scala/com/phasmidsoftware/parse/TableParserHelperSpec.scala b/src/test/scala/com/phasmidsoftware/parse/TableParserHelperSpec.scala index ef38fb83..0f2763ac 100644 --- a/src/test/scala/com/phasmidsoftware/parse/TableParserHelperSpec.scala +++ b/src/test/scala/com/phasmidsoftware/parse/TableParserHelperSpec.scala @@ -8,13 +8,12 @@ import com.phasmidsoftware.render.{JsonTableRenderer, Renderer} import com.phasmidsoftware.table._ import org.scalatest.flatspec import org.scalatest.matchers.should -import spray.json.{DefaultJsonProtocol, RootJsonFormat, enrichAny} - import scala.util.{Success, Try} +import spray.json.{DefaultJsonProtocol, RootJsonFormat, enrichAny} /** - * This tests one aspect of TableParserHelper which cannot be conveniently tested in the same module as the other. - */ + * This tests one aspect of TableParserHelper which cannot be conveniently tested in the same module as the other. + */ class TableParserHelperSpec extends flatspec.AnyFlatSpec with should.Matchers { behavior of "TableParserHelper without a header row in the input file" @@ -27,14 +26,14 @@ class TableParserHelperSpec extends flatspec.AnyFlatSpec with should.Matchers { lazy val cellParser: CellParser[Player] = cellParser2(apply) /** - * Method to transform a Table[Player] into a Table[Partnership]. - * - * The requirements of the application are that the rows of the Player table are grouped by twos - * and each resulting entity (an array of length 2) is taken to form a Partnership. - * - * @param pt a Table[Player] - * @return a Table[Partnership] - */ + * Method to transform a Table[Player] into a Table[Partnership]. + * + * The requirements of the application are that the rows of the Player table are grouped by twos + * and each resulting entity (an array of length 2) is taken to form a Partnership. + * + * @param pt a Table[Player] + * @return a Table[Partnership] + */ def convertTable(pt: Table[Player]): Table[Partnership] = pt.processRows(xs => (xs grouped 2).toList).map(r => Partnership(r)) } @@ -52,17 +51,17 @@ class TableParserHelperSpec extends flatspec.AnyFlatSpec with should.Matchers { def size: Int = partners.length /** - * Method to output these Partnerships as a Json String. - * - * @return a String with some embedded newlines. - */ + * Method to output these Partnerships as a Json String. + * + * @return a String with some embedded newlines. + */ def prettyPrint: String = Partnerships.prettyPrint(this) } /** - * Companion object for Partnerships. - */ + * Companion object for Partnerships. + */ object Partnerships extends DefaultJsonProtocol { implicit val partnershipsFormat: RootJsonFormat[Partnerships] = jsonFormat1(apply) diff --git a/src/test/scala/com/phasmidsoftware/parse/TableParserSpec.scala b/src/test/scala/com/phasmidsoftware/parse/TableParserSpec.scala index 7845c83b..58e4b12c 100644 --- a/src/test/scala/com/phasmidsoftware/parse/TableParserSpec.scala +++ b/src/test/scala/com/phasmidsoftware/parse/TableParserSpec.scala @@ -10,7 +10,6 @@ import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat import org.scalatest.flatspec import org.scalatest.matchers.should - import scala.io.Codec import scala.util.matching.Regex import scala.util.parsing.combinator.JavaTokenParsers @@ -39,7 +38,7 @@ class TableParserSpec extends flatspec.AnyFlatSpec with should.Matchers { } //noinspection NotImplementedCode - override def parseHeader(w: String): Try[Header] = ??? + override def parseHeader(w: Seq[String]): Try[Header] = ??? } implicit object IntPairRowParser extends IntPairRowParser @@ -51,6 +50,8 @@ class TableParserSpec extends flatspec.AnyFlatSpec with should.Matchers { val maybeFixedHeader: Option[Header] = Some(Header.create("x", "y")) + val headerRowsToRead: Int = 0 + val rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] } @@ -103,6 +104,8 @@ class TableParserSpec extends flatspec.AnyFlatSpec with should.Matchers { val maybeFixedHeader: Option[Header] = None + val headerRowsToRead: Int = 1 + val rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] protected def builder(rows: Iterable[DailyRaptorReport], header: Header): Table[Row] = HeadedTable(rows, header) @@ -123,7 +126,7 @@ class TableParserSpec extends flatspec.AnyFlatSpec with should.Matchers { val rowParser = implicitly[RowParser[DailyRaptorReport, String]] val firstRow = headerRaptors val row = "09/16/2018\t" + partlyCloudy + "\tSE\t6-12\t0\t0\t0\t4\t19\t3\t30\t2\t0\t0\t2\t3308\t5\t0\t0\t0\t0\t27\t8\t1\t0\t1\t0\t3410" - val Success(header) = rowParser.parseHeader(firstRow) + val Success(header) = rowParser.parseHeader(Seq(firstRow)) val hawkCount: Try[DailyRaptorReport] = parser.parse((row, 0))(header) hawkCount should matchPattern { case Success(DailyRaptorReport(_, `partlyCloudy`, 3308, 5)) => } @@ -205,6 +208,8 @@ class TableParserSpec extends flatspec.AnyFlatSpec with should.Matchers { val maybeFixedHeader: Option[Header] = None + val headerRowsToRead: Int = 1 + val rowParser: RowParser[Row, Seq[String]] = implicitly[RowParser[Row, Seq[String]]] protected def builder(rows: Iterable[DailyRaptorReport], header: Header): Table[Row] = HeadedTable(rows, header) @@ -258,6 +263,8 @@ class TableParserSpec extends flatspec.AnyFlatSpec with should.Matchers { val maybeFixedHeader: Option[Header] = Some(Header.create(header: _*)) + val headerRowsToRead: Int = 0 + protected def builder(rows: Iterable[DailyRaptorReport], header: Header): Table[DailyRaptorReport] = HeadedTable(rows, header) val rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] @@ -307,6 +314,8 @@ class TableParserSpec extends flatspec.AnyFlatSpec with should.Matchers { val maybeFixedHeader: Option[Header] = None // Some(header) + val headerRowsToRead: Int = 1 + protected def builder(rows: Iterable[Row], header: Header): Table[Row] = HeadedTable(rows, header) override val forgiving: Boolean = false @@ -378,6 +387,8 @@ class TableParserSpec extends flatspec.AnyFlatSpec with should.Matchers { val maybeFixedHeader: Option[Header] = None // Some(header) + val headerRowsToRead: Int = 1 + override val forgiving: Boolean = true val rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] @@ -394,8 +405,8 @@ class TableParserSpec extends flatspec.AnyFlatSpec with should.Matchers { } /** - * The following tests relate to the application CsvToJSON - */ + * The following tests relate to the application CsvToJSON + */ behavior of "TableParserHelper" case class Player(first: String, last: String) { @@ -406,14 +417,14 @@ class TableParserSpec extends flatspec.AnyFlatSpec with should.Matchers { def cellParser: CellParser[Player] = cellParser2(apply) /** - * Method to transform a Table[Player] into a Table[Partnership]. - * - * The requirements of the application are that the rows of the Player table are grouped by twos - * and each resulting entity (an array of length 2) is taken to form a Partnership. - * - * @param pt a Table[Player] - * @return a Table[Partnership] - */ + * Method to transform a Table[Player] into a Table[Partnership]. + * + * The requirements of the application are that the rows of the Player table are grouped by twos + * and each resulting entity (an array of length 2) is taken to form a Partnership. + * + * @param pt a Table[Player] + * @return a Table[Partnership] + */ def convertTable(pt: Table[Player]): Table[Partnership] = pt.processRows(xs => (xs grouped 2).toList).map(r => Partnership(r)) } diff --git a/src/test/scala/com/phasmidsoftware/render/CsvRenderersSpec.scala b/src/test/scala/com/phasmidsoftware/render/CsvRenderersSpec.scala new file mode 100644 index 00000000..74418d92 --- /dev/null +++ b/src/test/scala/com/phasmidsoftware/render/CsvRenderersSpec.scala @@ -0,0 +1,298 @@ +package com.phasmidsoftware.render + +import com.phasmidsoftware.parse._ +import com.phasmidsoftware.table._ +import java.io.File +import org.joda.time.LocalDate +import org.joda.time.format.DateTimeFormat +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should +import scala.util.matching.Regex +import scala.util.parsing.combinator.JavaTokenParsers +import scala.util.{Failure, Success, Try} + +class CsvRenderersSpec extends AnyFlatSpec with should.Matchers { + + case class IntPair(a: Int, b: Int) { + def map(f: Int => Int): IntPair = IntPair(f(a), f(b)) + } + + object IntPair { + + object IntPairParser extends JavaTokenParsers { + lazy val pair: Parser[(Int, Int)] = wholeNumber ~ wholeNumber ^^ { case x ~ y => (x.toInt, y.toInt) } + } + + trait IntPairRowParser extends StringParser[IntPair] { + def parse(indexedString: (String, Int))(header: Header): Try[IntPair] = IntPairParser.parseAll(IntPairParser.pair, indexedString._1) match { + case IntPairParser.Success((x, y), _) => Success(IntPair(x, y)) + case _ => Failure(TableException(s"unable to parse ${indexedString._1}")) + } + + //noinspection NotImplementedCode + def parseHeader(w: Seq[String]): Try[Header] = ??? + } + + implicit object IntPairRowParser extends IntPairRowParser + + trait IntPairTableParser extends StringTableParser[Table[IntPair]] { + type Row = IntPair + + val maybeFixedHeader: Option[Header] = Some(Header.create("a", "b")) + + val headerRowsToRead: Int = 0 + + protected def builder(rows: Iterable[IntPair], header: Header): Table[IntPair] = HeadedTable(rows, Header[IntPair]()) + + val rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] + } + + implicit object IntPairTableParser extends IntPairTableParser + + } + + behavior of "CsvGenerators" + + it should "generate header for an Int" in { + import CsvGenerators._ + implicit val csvAttributes: CsvAttributes = CsvAttributes(", ") + implicitly[CsvGenerator[Int]].toColumnName(None, "x") shouldBe "x" + implicitly[CsvGenerator[Int]].toColumnName(Some("x"), "y") shouldBe "x.y" + } + + it should "generate header for an Option[Int]" in { + implicit val csvAttributes: CsvAttributes = CsvAttributes(", ") + implicit val optionIntGenerator: CsvGenerator[Option[Int]] = new CsvGenerators {}.optionGenerator + optionIntGenerator.toColumnName(None, "x") shouldBe "x" + implicitly[CsvGenerator[Option[Int]]].toColumnName(Some("x"), "y") shouldBe "x.y" + } + + behavior of "CsvRenderers" + + it should "render an Option[Int]" in { + import CsvRenderers._ + implicit val csvAttributes: CsvAttributes = CsvAttributes(", ") + implicit val optionIntRenderer: CsvRenderer[Option[Int]] = new CsvRenderers {}.optionRenderer + optionIntRenderer.render(Some(42)) shouldBe "42" + optionIntRenderer.render(None) shouldBe "" + } + + it should "render an 1-tuple" in { + import CsvRenderers._ + implicit val csvAttributes: CsvAttributes = CsvAttributes(", ") + case class Onesy(x: Int) + implicit val onesyCsvRenderer: CsvRenderer[Onesy] = new CsvRenderers {}.renderer1(Onesy) + onesyCsvRenderer.render(Onesy(42)) shouldBe "42" + } + + it should "render an IntPair" in { + import CsvRenderers._ + implicit val csvAttributes: CsvAttributes = CsvAttributes(", ") + implicit val intPairCsvRenderer: CsvRenderer[IntPair] = new CsvRenderers {}.renderer2(IntPair.apply) + val intPair = IntPair(42, 99) + intPairCsvRenderer.render(intPair) shouldBe "42, 99" + } + + behavior of "CsvTableStringRenderer" + + it should "render a table 1" in { + val csvRenderers = new CsvRenderers {} + import CsvRenderers._ + val csvGenerators = new CsvGenerators {} + import CsvGenerators._ + implicit val csvAttributes: CsvAttributes = CsvAttributes(", ") + implicit val intPairCsvRenderer: CsvRenderer[IntPair] = csvRenderers.renderer2(IntPair.apply) + implicit val intPairCsvGenerator: CsvProductGenerator[IntPair] = csvGenerators.generator2(IntPair.apply) + import IntPair._ + val iIty = Table.parseFile(new File("src/test/resources/com/phasmidsoftware/table/intPairs.csv")) + iIty should matchPattern { case Success(_) => } + CsvTableStringRenderer[IntPair]().render(iIty.get).toString shouldBe "a, b\n1, 2\n42, 99\n" + } + + case class Hawks(bw: Int, rt: Int) { + def map(f: Int => Int): Hawks = Hawks(f(bw), f(rt)) + } + + object Hawks { + + object HawksParser extends JavaTokenParsers { + lazy val pair: Parser[(Int, Int)] = wholeNumber ~ wholeNumber ^^ { case x ~ y => (x.toInt, y.toInt) } + } + + trait HawksRowParser extends StringParser[Hawks] { + def parse(indexedString: (String, Int))(header: Header): Try[Hawks] = HawksParser.parseAll(HawksParser.pair, indexedString._1) match { + case HawksParser.Success((x, y), _) => Success(Hawks(x, y)) + case _ => Failure(TableException(s"unable to parse ${indexedString._1}")) + } + + //noinspection NotImplementedCode + def parseHeader(w: Seq[String]): Try[Header] = ??? + } + + implicit object HawksRowParser extends HawksRowParser + + trait HawksTableParser extends StringTableParser[Table[Hawks]] { + type Row = Hawks + + val maybeFixedHeader: Option[Header] = Some(Header.create("a", "b")) + + val headerRowsToRead: Int = 0 + + protected def builder(rows: Iterable[Hawks], header: Header): Table[Hawks] = HeadedTable(rows, Header[Hawks]()) + + val rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] + } + + implicit object HawksTableParser extends HawksTableParser + } + + case class DailyRaptorReport(date: LocalDate, weather: String, hawks: Hawks) + + object DailyRaptorReport { + + object DailyRaptorReportParser extends CellParsers { + private val raptorReportDateFormatter = DateTimeFormat.forPattern("MM/dd/yyyy") + + def parseDate(w: String): LocalDate = LocalDate.parse(w, raptorReportDateFormatter) + + implicit val dateParser: CellParser[LocalDate] = cellParser(parseDate) + implicit val dailyRaptorReportColumnHelper: ColumnHelper[DailyRaptorReport] = columnHelper() + implicit val hawksCellParser: CellParser[Hawks] = cellParser2(Hawks.apply) + implicit val dailyRaptorReportParser: CellParser[DailyRaptorReport] = cellParser3(DailyRaptorReport.apply) + } + + import DailyRaptorReportParser._ + + trait DailyRaptorReportConfig extends DefaultRowConfig { + override val string: Regex = """[\w/\- ]+""".r + override val delimiter: Regex = """\t""".r + } + + implicit object DailyRaptorReportConfig extends DailyRaptorReportConfig + + implicit val parser: StandardRowParser[DailyRaptorReport] = StandardRowParser[DailyRaptorReport](LineParser.apply) + + trait DailyRaptorReportTableParser extends StringTableParser[Table[DailyRaptorReport]] { + type Row = DailyRaptorReport + + val maybeFixedHeader: Option[Header] = None + + val headerRowsToRead: Int = 1 + + val rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] + + protected def builder(rows: Iterable[DailyRaptorReport], header: Header): Table[Row] = HeadedTable(rows, header) + } + + implicit object DailyRaptorReportTableParser extends DailyRaptorReportTableParser + } + + it should "parse and output raptors from raptors.csv" in { + import DailyRaptorReport._ + + val rty: Try[Table[DailyRaptorReport]] = for (r <- Table.parseResource(classOf[TableParserSpec].getResource("/raptors.csv"))) yield r + rty should matchPattern { case Success(HeadedTable(_, _)) => } + val rt = rty.get + rt.rows.size shouldBe 13 + import CsvGenerators._ + import CsvRenderers._ + implicit val csvAttributes: CsvAttributes = CsvAttributes(", ") + implicit val intPairCsvRenderer: CsvRenderer[Hawks] = new CsvRenderers {}.renderer2(Hawks.apply) + implicit val intPairCsvGenerator: CsvProductGenerator[Hawks] = new CsvGenerators {}.generator2(Hawks.apply) + implicit val dateCsvRenderer: CsvRenderer[LocalDate] = new CsvRenderer[LocalDate] { + val csvAttributes: CsvAttributes = implicitly[CsvAttributes] + + def render(t: LocalDate, attrs: Map[String, String]): String = t.toString + } + implicit val dateCsvGenerator: CsvGenerator[LocalDate] = new BaseCsvGenerator[LocalDate] + implicit val DRRCsvRenderer: CsvRenderer[DailyRaptorReport] = new CsvRenderers {}.renderer3(DailyRaptorReport.apply) + implicit val DRRCsvGenerator: CsvProductGenerator[DailyRaptorReport] = new CsvGenerators {}.generator3(DailyRaptorReport.apply) + val w: String = rt.toCSV + w shouldBe + """date, weather, hawks.bw, hawks.rt + |2018-09-12, Dense Fog/Light Rain, 0, 0 + |2018-09-13, Fog/Overcast, 79, 0 + |2018-09-14, Drizzle/Fog/Overcast, 1, 0 + |2018-09-15, Overcast/ Mostly Cloudy, 1054, 0 + |2018-09-16, Partly Cloudy, 3308, 5 + |2018-09-17, Dense Fog/Light Rain, 0, 0 + |2018-09-18, Clear/Partly cloudy, 260, 0 + |2018-09-19, Overcast/Mostly cloudy/Partly cloudy/Clear, 821, 4 + |2018-09-20, Overcast/Fog, 36, 1 + |2018-09-21, Dense Fog/Overcast, 29, 0 + |2018-09-22, Partly cloudy/Mostly cloudy/Overcast, 455, 3 + |2018-09-23, Overcast, 470, 2 + |2018-09-24, Overcast/Mostly cloudy, 292, 2 + |""".stripMargin + } + + case class WeatherHawks(weather: String, hawks: Hawks) + + case class NestedRaptorReport(date: LocalDate, weatherHawks: WeatherHawks) + + object NestedRaptorReport { + + object NestedRaptorReportParser extends CellParsers { + private val raptorReportDateFormatter = DateTimeFormat.forPattern("MM/dd/yyyy") + + def parseDate(w: String): LocalDate = LocalDate.parse(w, raptorReportDateFormatter) + + implicit val dateParser: CellParser[LocalDate] = cellParser(parseDate) + implicit val dailyRaptorReportColumnHelper: ColumnHelper[NestedRaptorReport] = columnHelper() + implicit val hawksCellParser: CellParser[Hawks] = cellParser2(Hawks.apply) + implicit val weatherHawksCellParser: CellParser[WeatherHawks] = cellParser2(WeatherHawks.apply) + implicit val dailyRaptorReportParser: CellParser[NestedRaptorReport] = cellParser2(NestedRaptorReport.apply) + } + + import NestedRaptorReportParser._ + + trait NestedRaptorReportConfig extends DefaultRowConfig { + override val string: Regex = """[\w/\- ]+""".r + override val delimiter: Regex = """\t""".r + } + + implicit object NestedRaptorReportConfig extends NestedRaptorReportConfig + + implicit val parser: StandardRowParser[NestedRaptorReport] = StandardRowParser[NestedRaptorReport](LineParser.apply) + + trait NestedRaptorReportTableParser extends StringTableParser[Table[NestedRaptorReport]] { + type Row = NestedRaptorReport + + val maybeFixedHeader: Option[Header] = None + + val headerRowsToRead: Int = 1 + + val rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] + + protected def builder(rows: Iterable[NestedRaptorReport], header: Header): Table[Row] = HeadedTable(rows, header) + } + + implicit object NestedRaptorReportTableParser extends NestedRaptorReportTableParser + } + + + it should "parse and output raptors from raptors.csv with a more nested type" in { + import NestedRaptorReport._ + + val rty: Try[Table[NestedRaptorReport]] = for (r <- Table.parseResource(classOf[TableParserSpec].getResource("/raptors.csv"))) yield r + rty should matchPattern { case Success(HeadedTable(_, _)) => } + val rt = rty.get + rt.rows.size shouldBe 13 + import CsvGenerators._ + import CsvRenderers._ + implicit val csvAttributes: CsvAttributes = CsvAttributes(", ") + implicit val hawksCsvRenderer: CsvRenderer[Hawks] = new CsvRenderers {}.renderer2(Hawks.apply) + implicit val hawksCsvGenerator: CsvProductGenerator[Hawks] = new CsvGenerators {}.generator2(Hawks.apply) + implicit val weatherHawksCsvRenderer: CsvRenderer[WeatherHawks] = new CsvRenderers {}.renderer2(WeatherHawks.apply) + implicit val weatherHawksCsvGenerator: CsvProductGenerator[WeatherHawks] = new CsvGenerators {}.generator2(WeatherHawks.apply) + implicit val dateCsvRenderer: CsvRenderer[LocalDate] = new CsvRenderer[LocalDate] { + val csvAttributes: CsvAttributes = implicitly[CsvAttributes] + + def render(t: LocalDate, attrs: Map[String, String]): String = t.toString + } + implicit val dateCsvGenerator: CsvGenerator[LocalDate] = new BaseCsvGenerator[LocalDate] + implicit val DRRCsvRenderer: CsvRenderer[NestedRaptorReport] = new CsvRenderers {}.renderer2(NestedRaptorReport.apply) + implicit val DRRCsvGenerator: CsvProductGenerator[NestedRaptorReport] = new CsvGenerators {}.generator2(NestedRaptorReport.apply) + rt.take(1).toCSV shouldBe "date, weatherHawks.weather, weatherHawks.hawks.bw, weatherHawks.hawks.rt\n2018-09-12, Dense Fog/Light Rain, 0, 0\n" + } +} diff --git a/src/test/scala/com/phasmidsoftware/render/HierarchicalRendererSpec.scala b/src/test/scala/com/phasmidsoftware/render/HierarchicalRendererSpec.scala index 2c327921..b40b4e63 100644 --- a/src/test/scala/com/phasmidsoftware/render/HierarchicalRendererSpec.scala +++ b/src/test/scala/com/phasmidsoftware/render/HierarchicalRendererSpec.scala @@ -137,7 +137,7 @@ class HierarchicalRendererSpec extends flatspec.AnyFlatSpec with should.Matchers it should "render a table of sequenced Complexes in HTML with a header" in { import Complex2._ - val table = HeadedTable(Seq(Complex(0, 1), Complex(-1, 0)), Header(Seq("real", "imaginary"))) + val table = HeadedTable(Seq(Complex(0, 1), Complex(-1, 0)), Header(Seq(Seq("real", "imaginary")))) val h = table.renderHierarchicalSequenced("table", Map("border" -> "1")) h.toString shouldBe """ diff --git a/src/test/scala/com/phasmidsoftware/render/JsonTableRendererSpec.scala b/src/test/scala/com/phasmidsoftware/render/JsonTableRendererSpec.scala index 0472e7d6..f67a584b 100644 --- a/src/test/scala/com/phasmidsoftware/render/JsonTableRendererSpec.scala +++ b/src/test/scala/com/phasmidsoftware/render/JsonTableRendererSpec.scala @@ -4,9 +4,8 @@ import com.phasmidsoftware.parse.{CellParser, TableParserHelper} import com.phasmidsoftware.table.{HeadedTable, Header, Table, TableJsonFormat} import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should -import spray.json._ - import scala.util.{Success, Try} +import spray.json._ class JsonTableRendererSpec extends AnyFlatSpec with should.Matchers { @@ -32,14 +31,14 @@ class JsonTableRendererSpec extends AnyFlatSpec with should.Matchers { implicit val playerFormat: RootJsonFormat[Player] = jsonFormat2(apply) /** - * Method to transform a Table[Player] into a Table[Partnership]. - * - * The requirements of the application are that the rows of the Player table are grouped by twos - * and each resulting entity (an array of length 2) is taken to form a Partnership. - * - * @param pt a Table[Player] - * @return a Table[Partnership] - */ + * Method to transform a Table[Player] into a Table[Partnership]. + * + * The requirements of the application are that the rows of the Player table are grouped by twos + * and each resulting entity (an array of length 2) is taken to form a Partnership. + * + * @param pt a Table[Player] + * @return a Table[Partnership] + */ def convertTable(pt: Table[Player]): Table[Partnership] = pt.processRows(xs => (xs grouped 2).toList).map(r => Converters.convert(r)).replaceHeader(Some(Header.create("playerA", "playerB"))) } diff --git a/src/test/scala/com/phasmidsoftware/render/WritableSpec.scala b/src/test/scala/com/phasmidsoftware/render/WritableSpec.scala index f701f3e1..8d2eb6d8 100644 --- a/src/test/scala/com/phasmidsoftware/render/WritableSpec.scala +++ b/src/test/scala/com/phasmidsoftware/render/WritableSpec.scala @@ -4,6 +4,7 @@ package com.phasmidsoftware.render +import java.io.{File, FileWriter} import org.scalatest.flatspec import org.scalatest.matchers.should @@ -11,37 +12,27 @@ class WritableSpec extends flatspec.AnyFlatSpec with should.Matchers { behavior of "Writable" - implicit object StringBuilderWritable extends Writable[StringBuilder] { - def writeRaw(o: StringBuilder)(x: CharSequence): StringBuilder = o.append(x.toString) - - def unit: StringBuilder = new StringBuilder - - override def delimiter: CharSequence = "|" - } + private val sw = Writable.stringBuilderWritable("|") it should "write value" in { - val sw = implicitly[Writable[StringBuilder]] val o = sw.unit sw.writeValue(o)(1) o.toString shouldBe "1" } it should "write value containing delimiter" in { - val sw = implicitly[Writable[StringBuilder]] val o = sw.unit sw.writeValue(o)("a|b") o.toString shouldBe "\"a|b\"" } it should "write value containing quote" in { - val sw = implicitly[Writable[StringBuilder]] val o = sw.unit sw.writeValue(o)("""a"b""") o.toString shouldBe "\"a\"\"b\"" } it should "writeRowElements" in { - val sw = implicitly[Writable[StringBuilder]] val o = sw.unit sw.writeRowElements(o)(Seq(1, 2)) o.toString shouldBe "1|2" @@ -50,7 +41,6 @@ class WritableSpec extends flatspec.AnyFlatSpec with should.Matchers { case class Complex(r: Double, i: Double) it should "writeRow" in { - val sw = implicitly[Writable[StringBuilder]] val o = sw.unit sw.writeRow(o)(Complex(1, -1)) o.toString shouldBe @@ -58,4 +48,12 @@ class WritableSpec extends flatspec.AnyFlatSpec with should.Matchers { |""".stripMargin } + it should "writeRow to a File" in { + val file = new File("output.csv") + val fw: Writable[FileWriter] = Writable.fileWritable(file) + val o = fw.unit + fw.writeRow(o)(Complex(1, -1)) + fw.close(o) + } + } diff --git a/src/test/scala/com/phasmidsoftware/table/AirBNBSpec.scala b/src/test/scala/com/phasmidsoftware/table/AirBNBSpec.scala index 9cc9be2e..0ff6a6f4 100644 --- a/src/test/scala/com/phasmidsoftware/table/AirBNBSpec.scala +++ b/src/test/scala/com/phasmidsoftware/table/AirBNBSpec.scala @@ -4,7 +4,6 @@ import com.phasmidsoftware.parse.{RawTableParser, TableParser} import com.phasmidsoftware.util.FP.resource import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers - import scala.io.Source import scala.util._ @@ -13,10 +12,10 @@ class AirBNBSpec extends AnyFlatSpec with Matchers { behavior of "AirBNB table" /** - * NOTE: it is perfectly proper for there to be a number of parsing problems. - * These are application-specific and are not indicative of any bugs in the - * TableParser library itself. - */ + * NOTE: it is perfectly proper for there to be a number of parsing problems. + * These are application-specific and are not indicative of any bugs in the + * TableParser library itself. + */ it should "be ingested properly" in { val airBNBFile = "/airbnb2.csv" diff --git a/src/test/scala/com/phasmidsoftware/table/AnalysisSpec.scala b/src/test/scala/com/phasmidsoftware/table/AnalysisSpec.scala index 1054b62c..8059672e 100644 --- a/src/test/scala/com/phasmidsoftware/table/AnalysisSpec.scala +++ b/src/test/scala/com/phasmidsoftware/table/AnalysisSpec.scala @@ -4,7 +4,6 @@ import com.phasmidsoftware.parse.{RawTableParser, TableParser} import com.phasmidsoftware.util.FP.resource import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers - import scala.io.Source import scala.util._ @@ -13,10 +12,10 @@ class AnalysisSpec extends AnyFlatSpec with Matchers { behavior of "Analysis" /** - * NOTE: it is perfectly proper for there to be a number of parsing problems. - * These are application-specific and are not indicative of any bugs in the - * TableParser library itself. - */ + * NOTE: it is perfectly proper for there to be a number of parsing problems. + * These are application-specific and are not indicative of any bugs in the + * TableParser library itself. + */ it should "be correct for airbnb2.csv" in { val airBNBFile = "/airbnb2.csv" val parser = RawTableParser(TableParser.includeAll, None, forgiving = true).setMultiline(true) @@ -32,7 +31,6 @@ class AnalysisSpec extends AnyFlatSpec with Matchers { analysis.columnMap("bedrooms") should matchPattern { case Column("Int", false, _) => } analysis.columnMap("accommodates").toString shouldBe "Int (range: 1.0-10.0, mean: 2.783464566929134, stdDev: 1.7670324685210184)" analysis.columnMap("license").toString shouldBe "optional Int" - // println(analysis) } } diff --git a/src/test/scala/com/phasmidsoftware/table/Movie.scala b/src/test/scala/com/phasmidsoftware/table/Movie.scala index 4b76861a..c92b0b32 100644 --- a/src/test/scala/com/phasmidsoftware/table/Movie.scala +++ b/src/test/scala/com/phasmidsoftware/table/Movie.scala @@ -5,41 +5,39 @@ package com.phasmidsoftware.table import com.phasmidsoftware.parse._ - import scala.util.Try -import scala.util.matching.Regex /** - * This class represents a Movie from the IMDB data file on Kaggle. - * Although the limitation on 22 fields in a case class has partially gone away, it's still convenient to group the different attributes together into logical classes. - * - * Created by scalaprof on 9/12/16. - * - * Common questions in this assignment: - * 1. Where is main method? - * In most case, you don't need to run main method for assignments. - * Unit tests are provided to test your implementation. - * In this assignment, you will find the `object Movie extends App`, - * the `App` trait can be used to quickly turn objects into executable programs. - * You can read the official doc of Scala for more details. - * - * 2. How to understand the whole program in this assignment? - * I won't suggest you to understand the whole program in this assignment, - * there are some advanced features like `implicit` which hasn't been covered in class. - * You should be able to understand it before midterm. - * I will suggest you only focus on each TO BE IMPLEMENTED in the assignments. - * - */ + * This class represents a Movie from the IMDB data file on Kaggle. + * Although the limitation on 22 fields in a case class has partially gone away, it's still convenient to group the different attributes together into logical classes. + * + * Created by scalaprof on 9/12/16. + * + * Common questions in this assignment: + * 1. Where is main method? + * In most case, you don't need to run main method for assignments. + * Unit tests are provided to test your implementation. + * In this assignment, you will find the `object Movie extends App`, + * the `App` trait can be used to quickly turn objects into executable programs. + * You can read the official doc of Scala for more details. + * + * 2. How to understand the whole program in this assignment? + * I won't suggest you to understand the whole program in this assignment, + * there are some advanced features like `implicit` which hasn't been covered in class. + * You should be able to understand it before midterm. + * I will suggest you only focus on each TO BE IMPLEMENTED in the assignments. + * + */ case class Movie(title: String, format: Format, production: Production, reviews: Reviews, director: Principal, actor1: Principal, actor2: Principal, actor3: Option[Principal], genres: AttributeSet, plotKeywords: AttributeSet, imdb: String) /** - * The movie format (including language and duration). - * - * @param color whether filmed in color - * @param language the native language of the characters - * @param aspectRatio the aspect ratio of the film (optional) - * @param duration its length in minutes (optional) - */ + * The movie format (including language and duration). + * + * @param color whether filmed in color + * @param language the native language of the characters + * @param aspectRatio the aspect ratio of the film (optional) + * @param duration its length in minutes (optional) + */ case class Format(color: String, language: String, aspectRatio: Option[Double], duration: Option[Int]) { override def toString: String = { s"$color,$language,$aspectRatio,$duration" @@ -47,13 +45,13 @@ case class Format(color: String, language: String, aspectRatio: Option[Double], } /** - * The production: its country, year, and financials - * - * @param country country of origin - * @param budget (optional) production budget in US dollars - * @param gross (optional) gross earnings (?) - * @param titleYear the year the title was registered (?) - */ + * The production: its country, year, and financials + * + * @param country country of origin + * @param budget (optional) production budget in US dollars + * @param gross (optional) gross earnings (?) + * @param titleYear the year the title was registered (?) + */ case class Production(country: String, budget: Option[Int], gross: Option[Int], titleYear: Option[Int]) { def isKiwi: Boolean = this match { case Production("New Zealand", _, _, _) => true @@ -62,28 +60,28 @@ case class Production(country: String, budget: Option[Int], gross: Option[Int], } /** - * Information about various forms of review, including the content rating. - */ + * Information about various forms of review, including the content rating. + */ case class Reviews(imdbScore: Double, facebookLikes: Int, contentRating: Rating, numUsersReview: Option[Int], numUsersVoted: Int, numCriticReviews: Option[Int], totalFacebookLikes: Int) /** - * A cast or crew principal - * - * @param name name - * @param facebookLikes number of FaceBook likes - */ + * A cast or crew principal + * + * @param name name + * @param facebookLikes number of FaceBook likes + */ case class Principal(name: Name, facebookLikes: Int) { override def toString = s"$name ($facebookLikes likes)" } /** - * A name of a contributor to the production - * - * @param first first name - * @param middle middle name or initial - * @param last last name - * @param suffix suffix - */ + * A name of a contributor to the production + * + * @param first first name + * @param middle middle name or initial + * @param last last name + * @param suffix suffix + */ case class Name(first: String, middle: Option[String], last: String, suffix: Option[String]) { override def toString: String = { case class Result(r: StringBuffer) { @@ -104,10 +102,10 @@ case class Name(first: String, middle: Option[String], last: String, suffix: Opt } /** - * The US rating. - * NOTE: this definition does not cover all of the ratings in the IMDB movie dataset. - * That's OK--this is just an exemplar. - */ + * The US rating. + * NOTE: this definition does not cover all of the ratings in the IMDB movie dataset. + * That's OK--this is just an exemplar. + */ case class Rating(code: String, age: Option[Int]) { override def toString: String = code + (age match { case Some(x) => "-" + x @@ -117,20 +115,23 @@ case class Rating(code: String, age: Option[Int]) { object MovieParser extends CellParsers { - def camelCaseColumnNameMapper(w: String): String = w.replaceAll("([A-Z0-9])", "_$1") + /** + * Precede each upper case letter (or digit) with _. + */ + def camelToSnakeCaseColumnNameMapper(w: String): String = w.replaceAll("([A-Z0-9])", "_$1") - implicit val movieColumnHelper: ColumnHelper[Movie] = columnHelper(camelCaseColumnNameMapper _, + implicit val movieColumnHelper: ColumnHelper[Movie] = columnHelper(camelToSnakeCaseColumnNameMapper _, "title" -> "movie_title", "imdb" -> "movie_imdb_link") - implicit val reviewsColumnHelper: ColumnHelper[Reviews] = columnHelper(camelCaseColumnNameMapper _, + implicit val reviewsColumnHelper: ColumnHelper[Reviews] = columnHelper(camelToSnakeCaseColumnNameMapper _, "facebookLikes" -> "movie_facebook_likes", "numUsersReview" -> "num_user_for_reviews", "numUsersVoted" -> "num_voted_users", "numCriticReviews" -> "num_critic_for_reviews", "totalFacebookLikes" -> "cast_total_facebook_likes") - implicit val formatColumnHelper: ColumnHelper[Format] = columnHelper(camelCaseColumnNameMapper _) - implicit val productionColumnHelper: ColumnHelper[Production] = columnHelper(camelCaseColumnNameMapper _) - implicit val principalColumnHelper: ColumnHelper[Principal] = columnHelper(camelCaseColumnNameMapper _, Some("$x_$c")) + implicit val formatColumnHelper: ColumnHelper[Format] = columnHelper(camelToSnakeCaseColumnNameMapper _) + implicit val productionColumnHelper: ColumnHelper[Production] = columnHelper(camelToSnakeCaseColumnNameMapper _) + implicit val principalColumnHelper: ColumnHelper[Principal] = columnHelper(camelToSnakeCaseColumnNameMapper _, Some("$x_$c")) implicit val ratingParser: CellParser[Rating] = cellParser(Rating.apply: String => Rating) implicit val formatParser: CellParser[Format] = cellParser4(Format) implicit val productionParser: CellParser[Production] = cellParser4(Production) @@ -142,8 +143,6 @@ object MovieParser extends CellParsers { implicit val movieParser: CellParser[Movie] = cellParser11(Movie) implicit object MovieConfig extends DefaultRowConfig { - override val string: Regex = """[^,]*""".r - override val delimiter: Regex = """,""".r override val listEnclosure: String = "" } @@ -154,6 +153,8 @@ object MovieParser extends CellParsers { val maybeFixedHeader: Option[Header] = None + val headerRowsToRead: Int = 1 + override val forgiving: Boolean = true val rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] @@ -180,11 +181,11 @@ object Name { object Rating { /** - * Alternative apply method for the Rating class such that a single String is decoded - * - * @param s a String made up of a code, optionally followed by a dash and a number, e.g. "R" or "PG-13" - * @return a Rating - */ + * Alternative apply method for the Rating class such that a single String is decoded + * + * @param s a String made up of a code, optionally followed by a dash and a number, e.g. "R" or "PG-13" + * @return a Rating + */ def apply(s: String): Rating = s match { case rRating(code, _, null) => apply(code, None) diff --git a/src/test/scala/com/phasmidsoftware/table/MovieSpec.scala b/src/test/scala/com/phasmidsoftware/table/MovieSpec.scala index e08818dd..0fb1ded4 100644 --- a/src/test/scala/com/phasmidsoftware/table/MovieSpec.scala +++ b/src/test/scala/com/phasmidsoftware/table/MovieSpec.scala @@ -7,7 +7,6 @@ package com.phasmidsoftware.table import com.phasmidsoftware.parse.{CellParser, RowParser, StringTableParser} import org.scalatest.flatspec import org.scalatest.matchers.should - import scala.util._ //noinspection SpellCheckingInspection @@ -55,6 +54,8 @@ class MovieSpec extends flatspec.AnyFlatSpec with should.Matchers { val maybeFixedHeader: Option[Header] = None + val headerRowsToRead: Int = 1 + override val forgiving: Boolean = false val rowParser: RowParser[Row, String] = implicitly[RowParser[Row, String]] @@ -79,6 +80,8 @@ class MovieSpec extends flatspec.AnyFlatSpec with should.Matchers { val maybeFixedHeader: Option[Header] = None + val headerRowsToRead: Int = 1 + def builder(rows: Iterable[Row], header: Header): Table[Row] = HeadedTable(rows, header) override val forgiving: Boolean = false @@ -104,6 +107,8 @@ class MovieSpec extends flatspec.AnyFlatSpec with should.Matchers { val maybeFixedHeader: Option[Header] = None + val headerRowsToRead: Int = 1 + def builder(rows: Iterable[Row], header: Header): Table[Row] = HeadedTable(rows, header) override val forgiving: Boolean = false diff --git a/src/test/scala/com/phasmidsoftware/table/RowSpec.scala b/src/test/scala/com/phasmidsoftware/table/RowSpec.scala index 2e4b2ce9..a79a13f4 100644 --- a/src/test/scala/com/phasmidsoftware/table/RowSpec.scala +++ b/src/test/scala/com/phasmidsoftware/table/RowSpec.scala @@ -8,7 +8,6 @@ import com.phasmidsoftware.parse.ParserException import com.phasmidsoftware.util.FPException import org.scalatest.flatspec import org.scalatest.matchers.should - import scala.util.{Success, Try} class RowSpec extends flatspec.AnyFlatSpec with should.Matchers { @@ -37,9 +36,9 @@ class RowSpec extends flatspec.AnyFlatSpec with should.Matchers { } it should "fail apply(String) when appropriate" in { - val f: String => Try[String] = Row(Seq("1", "2", "Junk"), Header(Seq("A", "B", "c")), 0) + val f: String => Try[String] = Row(Seq("1", "2", "Junk"), Header(Seq(Seq("A", "B", "c"))), 0) an[FPException] should be thrownBy f("x").get - the[FPException] thrownBy f("x").get should have message "Header column x not found" + the[FPException] thrownBy f("x").get should have message "Header column 'x' not found" } } diff --git a/src/test/scala/com/phasmidsoftware/table/TableSpec.scala b/src/test/scala/com/phasmidsoftware/table/TableSpec.scala index e4e4f4a6..67c5c2b8 100644 --- a/src/test/scala/com/phasmidsoftware/table/TableSpec.scala +++ b/src/test/scala/com/phasmidsoftware/table/TableSpec.scala @@ -8,11 +8,10 @@ import com.phasmidsoftware.parse._ import com.phasmidsoftware.render._ import com.phasmidsoftware.util.FP.resource import com.phasmidsoftware.util.TryUsing +import java.io.{File, FileWriter, InputStream} +import java.net.URL import org.scalatest.flatspec import org.scalatest.matchers.should - -import java.io.{File, InputStream} -import java.net.URL import scala.io.Source import scala.util.parsing.combinator.JavaTokenParsers import scala.util.{Failure, Success, Try} @@ -36,7 +35,7 @@ class TableSpec extends flatspec.AnyFlatSpec with should.Matchers { } //noinspection NotImplementedCode - def parseHeader(w: String): Try[Header] = ??? + def parseHeader(w: Seq[String]): Try[Header] = ??? } implicit object IntPairRowParser extends IntPairRowParser @@ -46,6 +45,7 @@ class TableSpec extends flatspec.AnyFlatSpec with should.Matchers { val maybeFixedHeader: Option[Header] = Some(Header.create("a", "b")) + val headerRowsToRead: Int = 0 protected def builder(rows: Iterable[IntPair], header: Header): Table[IntPair] = HeadedTable(rows, Header[IntPair]()) @@ -199,7 +199,6 @@ class TableSpec extends flatspec.AnyFlatSpec with should.Matchers { def apply(x: String, a: String): HTML = apply(x, Some(a), Map.empty, Nil) def apply(x: String, hs: Seq[HTML]): HTML = apply(x, None, Map.empty, hs) - } object IntPairHTML extends HierarchicalRenderers { @@ -212,10 +211,9 @@ class TableSpec extends flatspec.AnyFlatSpec with should.Matchers { implicit val intPairRenderer: HierarchicalRenderer[IntPair] = renderer2("IntPair")(IntPair.apply) implicit val r: HierarchicalRenderer[Indexed[IntPair]] = indexedRenderer("", "th") - } - it should "render the parsed table to CSV" in { + it should "render the table to CSV" in { import IntPair._ val iIty: Try[Table[IntPair]] = Table.parse(Seq("1 2", "42 99")) iIty should matchPattern { case Success(_) => } @@ -230,12 +228,13 @@ class TableSpec extends flatspec.AnyFlatSpec with should.Matchers { implicit object DummyRenderer$$ extends Renderer[Table[IntPair], String] { def render(t: Table[IntPair], attrs: Map[String, String]): String = t match { - case t: RenderableTable[IntPair] => t.RenderToWritable(StringBuilderWritable).toString + case t: RenderableTable[IntPair] => t.renderToWritable(StringBuilderWritable).toString case _ => throw TableException("render problem") } } - val sy = iIty map { + + val sy: Try[String] = iIty map { case r: Table[IntPair] => implicitly[Renderer[Table[IntPair], String]].render(r) case _ => fail("cannot render table") } @@ -243,6 +242,81 @@ class TableSpec extends flatspec.AnyFlatSpec with should.Matchers { sy.get shouldBe "a|b\n1|2\n42|99\n" } + + it should "render the table to CSV using a Writable" in { + import IntPair._ + val iIty: Try[Table[IntPair]] = Table.parse(Seq("1 2", "42 99")) + iIty should matchPattern { case Success(_) => } + val file = new File("output.csv") + implicit val fw: Writable[FileWriter] = Writable.fileWritable(file) + + implicit object FileRenderer extends Renderer[Table[IntPair], FileWriter] { + def render(t: Table[IntPair], attrs: Map[String, String]): FileWriter = t match { + case pr: RenderableTable[IntPair] => pr.renderToWritable + case _ => throw TableException("render problem") + } + } + + // implicit object FileCsvRenderer extends CsvRenderer[Table[IntPair]] { + // def render(t: Table[IntPair], attrs: Map[String, String]): FileWriter = t match { + // case pr: RenderableTable[IntPair] => pr.renderToWritable + // case _ => throw TableException("render problem") + // } + // } + + val sy: Try[FileWriter] = iIty map { + case r: Table[IntPair] => + val z: Renderer[Table[IntPair], FileWriter] = implicitly[Renderer[Table[IntPair], FileWriter]] + z.render(r) + case _ => fail("cannot render table") + } + sy should matchPattern { case Success(_) => } + } + + it should "render another parsed table to CSV" in { + import IntPair._ + + implicit object IntPairCsvRenderer extends CsvRenderer[IntPair] { + val csvAttributes: CsvAttributes = CsvAttributes(", ") + + def render(t: IntPair, attrs: Map[String, String]): String = s"${t.a}${csvAttributes.delimiter}${t.b}" + } + + implicit object IntPairCsvGenerator extends CsvProductGenerator[IntPair] { + val csvAttributes: CsvAttributes = CsvAttributes(", ") + + def toColumnNames(po: Option[String], no: Option[String]): String = s"a${csvAttributes.delimiter}b" + } + + implicit val csvAttributes: CsvAttributes = IntPairCsvRenderer.csvAttributes + val iIty = Table.parseFile(new File("src/test/resources/com/phasmidsoftware/table/intPairs.csv")) + iIty should matchPattern { case Success(_) => } + val iIt = iIty.get + val ws = iIt.toCSV + ws shouldBe "a, b\n1, 2\n42, 99\n" + } + + it should "render another parsed table to CSV with delim, quote" in { + import IntPair._ + implicit val myCsvAttributes: CsvAttributes = CsvAttributes("|") + + implicit object IntPairCsvRenderer extends CsvRenderer[IntPair] { + val csvAttributes: CsvAttributes = myCsvAttributes + + def render(t: IntPair, attrs: Map[String, String]): String = s"${t.a}${csvAttributes.delimiter}${t.b}" + } + + implicit object IntPairCsvGenerator extends CsvProductGenerator[IntPair] { + val csvAttributes: CsvAttributes = myCsvAttributes + + def toColumnNames(wo: Option[String], no: Option[String]): String = s"a${csvAttributes.delimiter}b" + } + + val iIty = Table.parseFile(new File("src/test/resources/com/phasmidsoftware/table/intPairs.csv")) + iIty should matchPattern { case Success(_) => } + iIty.get.toCSV shouldBe "a|b\n1|2\n42|99\n" + } + // it should "render the parsed table with TreeWriter" in { // import IntPair._ // val iIty: Try[Table[IntPair]] = Table.parse(Seq("1 2", "42 99")) @@ -337,16 +411,44 @@ class TableSpec extends flatspec.AnyFlatSpec with should.Matchers { behavior of "sort" - it should "sort a Table" in { + it should "sort a Table and then select" in { import IntPair._ val iIty = Table.parse(Seq("1 2", "42 99", "1 3")) + iIty should matchPattern { case Success(_) => } + implicit object IntPairOrdering extends Ordering[IntPair] { def compare(x: IntPair, y: IntPair): Int = x.a.compareTo(y.a) match { case 0 => x.b.compareTo(y.b) case cf => cf } } - println(iIty map (_.sorted)) + val triedSorted = iIty map (_.sort) + triedSorted should matchPattern { case Success(_) => } + val sorted = triedSorted.get + val row1 = sorted.select(Range(2, 3)) + row1.size shouldBe 1 + row1.head shouldBe IntPair(1, 3) + } + + behavior of "select" + + it should "select from a Table" in { + import IntPair._ + val iIty = Table.parse(Seq("1 2", "42 99", "1 3")) + iIty should matchPattern { case Success(_) => } + val table = iIty.get + table.select(1).size shouldBe 1 + table.select(1).head shouldBe IntPair(1, 2) + table.select(2).head shouldBe IntPair(42, 99) + table.select(3).head shouldBe IntPair(1, 3) + val row1 = table.select(Range(3, 4)) + row1.size shouldBe 1 + row1.head shouldBe IntPair(1, 3) + val row2 = table.select(Range(2, 3)) + row2.size shouldBe 1 + row2.head shouldBe IntPair(42, 99) + val rows02 = table.select(Range(1, 4, 2)) + rows02.size shouldBe 2 } behavior of "parseResourceRaw" @@ -361,6 +463,23 @@ class TableSpec extends flatspec.AnyFlatSpec with should.Matchers { println(s"parseResourceRaw: successfully read ${h.size} columns") r.size shouldBe 4 r take 4 foreach println + case _ => fail("should succeed") } } + + behavior of "Header" + it should "do lookup" in { + val hdr = Header(Seq(Seq("a", "Hello Goodbye", "Team Number"))) + hdr.getIndex("a") shouldBe Success(0) + hdr.getIndex("Hello Goodbye") shouldBe Success(1) + hdr.getIndex("team number") shouldBe Success(2) + } + + behavior of "Table[Row]" + it should "work" in { + val hdr = Header(Seq(Seq("a", "b"))) + val row1 = Row(Seq("1", "2"), hdr, 1) + val table = Table(Seq(row1), Some(hdr)) + Table.toCSVRow(table) shouldBe "a,b\n1,2\n" + } } diff --git a/src/test/scala/com/phasmidsoftware/util/FPSpec.scala b/src/test/scala/com/phasmidsoftware/util/FPSpec.scala index 527077cb..6164a7ca 100644 --- a/src/test/scala/com/phasmidsoftware/util/FPSpec.scala +++ b/src/test/scala/com/phasmidsoftware/util/FPSpec.scala @@ -6,10 +6,9 @@ package com.phasmidsoftware.util import com.phasmidsoftware.table.TableSpec import com.phasmidsoftware.util.FP._ +import java.io.InputStream import org.scalatest.flatspec import org.scalatest.matchers.should - -import java.io.InputStream import scala.io.Source import scala.util.{Failure, Success, Try} @@ -19,7 +18,7 @@ class FPSpec extends flatspec.AnyFlatSpec with should.Matchers { it should "indexFound" in { indexFound("junk", 0) shouldBe Success(0) - indexFound("junk", -1) should matchPattern { case Failure(FPException("Header column junk not found", None)) => } + indexFound("junk", -1) should matchPattern { case Failure(FPException("Header column 'junk' not found", None)) => } } it should "sequence" in { diff --git a/src/test/scala/com/phasmidsoftware/worksheets/airbnb.sc b/src/test/scala/com/phasmidsoftware/worksheets/airbnb.sc index c2edfefa..74ec4e60 100644 --- a/src/test/scala/com/phasmidsoftware/worksheets/airbnb.sc +++ b/src/test/scala/com/phasmidsoftware/worksheets/airbnb.sc @@ -1,7 +1,6 @@ import com.phasmidsoftware.parse.{RawTableParser, TableParser} import com.phasmidsoftware.table._ import com.phasmidsoftware.util.FP.resource - import scala.io.Source import scala.util.Success diff --git a/src/test/scala/com/phasmidsoftware/worksheets/movie.sc b/src/test/scala/com/phasmidsoftware/worksheets/movie.sc index 01ea397b..0da007e0 100644 --- a/src/test/scala/com/phasmidsoftware/worksheets/movie.sc +++ b/src/test/scala/com/phasmidsoftware/worksheets/movie.sc @@ -2,7 +2,6 @@ import com.phasmidsoftware.parse.StringTableParser import com.phasmidsoftware.table.MovieParser.MovieTableParser import com.phasmidsoftware.table._ import com.phasmidsoftware.util.FP.resource - import scala.io.Source import scala.util.{Failure, Success, Try}