├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties.example ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── samples ├── GettingDependencyUsingGrab.groovy └── UsingAutoDetection.groovy ├── src └── com │ └── xlson │ └── groovycsv │ ├── AutoDetectHandler.groovy │ ├── CsvIterator.groovy │ ├── CsvParser.groovy │ └── PropertyMapper.groovy └── test └── com └── xlson └── groovycsv ├── AutoDetectHandlerSpec.groovy ├── ConfigurableColumnNamesSpec.groovy ├── CsvIteratorSpec.groovy ├── CsvParserSpec.groovy └── PropertyMapperSpec.groovy /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/* 2 | build/* 3 | lib/* 4 | _site 5 | gradle.properties 6 | .idea 7 | groovycsv.iml 8 | out/ 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at leo@xlson.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Leonard Gram 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GroovyCSV 2 | 3 | GroovyCSV is a library for Groovy which aims to make csv data 4 | easier (and more idiomatically Groovy) to work with. The library was inspired by @[goeh's](http://twitter.com/goeh) 5 | [ExcelBuilder](http://www.technipelago.se/blog/show/groovy-poi-excel) that lets you 6 | iterate over rows in the excel file using `eachLine` and access values 7 | using the column names. 8 | 9 | ## Features 10 | 11 | * Value-access by header name or position 12 | * Iteration using the ordinary collection methods (`findAll`, `collect` 13 | and so on) 14 | * Support for guessing separator and/or quote character 15 | * Support for reading csv without headers 16 | * Support for skipping initial lines of the csv 17 | 18 | ## Example 19 | 20 | The parse method returns an iterator over the rows in the csv. This 21 | means we can use any of the default groovy ways to iterate, in this 22 | example we see the for each loop in use. 23 | 24 | @Grab('com.xlson.groovycsv:groovycsv:1.3') 25 | import static com.xlson.groovycsv.CsvParser.parseCsv 26 | 27 | def csv = '''Name,Lastname 28 | Mark,Andersson 29 | Pete,Hansen''' 30 | 31 | def data = parseCsv(csv) 32 | for(line in data) { 33 | println "$line.Name $line.Lastname" 34 | } 35 | 36 | The parse method takes a String or a Reader as argument. 37 | 38 | **Output:** 39 | 40 | Mark Andersson 41 | Pete Hansen 42 | 43 | ## Getting GroovyCSV 44 | 45 | GroovyCSV is available through Maven Central. 46 | 47 | ### Maven & Ivy configuration 48 | 49 | #### Latest stable 50 | 51 | * *GroupId:* com.xlson.groovycsv 52 | * *ArtifactId:* groovycsv 53 | * *Version:* 1.3 54 | 55 | #### Latest snapshot 56 | 57 | * *Version:* 1.3-SNAPSHOT 58 | * *Repository:* https://oss.sonatype.org/content/groups/public/ 59 | 60 | ### Downloads 61 | 62 | *GroovyCSV 1.3* 63 | 64 | 65 | * [groovycsv-1.3.jar](http://repo1.maven.org/maven2/com/xlson/groovycsv/groovycsv/1.3/groovycsv-1.3.jar) 66 | * [groovycsv-1.3-javadoc.jar](http://repo1.maven.org/maven2/com/xlson/groovycsv/groovycsv/1.3/groovycsv-1.3-javadoc.jar) 67 | * [Javadoc Online](https://xlson.github.io/groovycsv/docs/1.3/javadoc/) 68 | 69 | ## Dependencies 70 | 71 | * [Groovy 1.8.x](http://groovy.codehaus.org) or later 72 | * [OpenCSV 4.x](http://opencsv.sourceforge.net/) 73 | 74 | 75 | Many thanks to everyone who's contributed to the project and everyone in the OpenCSV team for 76 | doing all the heavy lifting. 77 | 78 | ## Building 79 | 80 | GroovyCSV uses Gradle for building as is packaged with the gradle wrapper which will download and install gradle for you behind the scenes the first time you run it. 81 | 82 | **Build instruction** 83 | 84 | 1. Fetch the latest code: `git clone git://github.com/xlson/groovycsv.git` 85 | 2. (Optional) Run the tests using the gradle wrapper `./gradlew test` 86 | 4. Go to the project directory and run: `./gradlew jar` 87 | 88 | You will find the built jar in `./build/libs`. If you need any 89 | dependencies you can download them using `./gradlew downloadDeps`, they 90 | end up in the `lib` folder. 91 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | 6 | dependencies { 7 | classpath 'de.huxhorn.gradle:de.huxhorn.gradle.pgp-plugin:0.0.4' 8 | } 9 | } 10 | 11 | apply plugin: 'groovy' 12 | apply plugin: 'maven' 13 | apply plugin: 'signing' 14 | apply plugin: 'idea' 15 | 16 | repositories { 17 | mavenCentral() 18 | maven { url "http://m2repo.spockframework.org/snapshots" } 19 | } 20 | 21 | dependencies { 22 | compile 'com.opencsv:opencsv:4.0' 23 | compileOnly 'org.codehaus.groovy:groovy-all:1.8.8' 24 | testCompile 'org.spockframework:spock-core:1.1-groovy-2.4-rc-2' 25 | testCompile 'cglib:cglib-nodep:2.2' 26 | testCompile 'org.objenesis:objenesis:1.2' 27 | } 28 | 29 | version = '1.3' 30 | group = 'com.xlson.groovycsv' 31 | 32 | sourceSets { 33 | main { 34 | groovy { 35 | srcDir 'src' 36 | } 37 | } 38 | test { 39 | groovy { 40 | srcDir 'test' 41 | } 42 | } 43 | } 44 | 45 | task groovydocJar(type: Jar, dependsOn: groovydoc) { 46 | classifier = 'javadoc' 47 | from 'build/docs/groovydoc' 48 | } 49 | 50 | task sourcesJar(type: Jar) { 51 | from sourceSets.main.allSource 52 | classifier = 'sources' 53 | } 54 | 55 | artifacts { 56 | archives groovydocJar, sourcesJar 57 | } 58 | if(isSetupForDeployToMavenCentral()) { 59 | signing { 60 | sign configurations.archives 61 | } 62 | } 63 | 64 | task wrapper(type: Wrapper) { 65 | gradleVersion = '4.3.1' 66 | } 67 | 68 | task downloadDeps << { 69 | def libDir = file('lib') 70 | ant.delete(dir: libDir) 71 | copy { 72 | from configurations.testRuntime 73 | into libDir 74 | } 75 | } 76 | 77 | 78 | def isSetupForDeployToMavenCentral() { 79 | def requiredProperties = ['ossrhUsername', 80 | 'ossrhPassword', 81 | 'signing.keyId', 82 | 'signing.password', 83 | 'signing.secretKeyRingFile'] 84 | for(prop in requiredProperties) { 85 | if(!project.hasProperty(prop)) { 86 | return false 87 | } 88 | } 89 | return true 90 | } 91 | 92 | uploadArchives { 93 | repositories { 94 | mavenDeployer { 95 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 96 | 97 | if(isSetupForDeployToMavenCentral()) { 98 | repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { 99 | authentication(userName: ossrhUsername, password: ossrhPassword) 100 | } 101 | 102 | snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { 103 | authentication(userName: ossrhUsername, password: ossrhPassword) 104 | } 105 | } 106 | 107 | pom.project { 108 | name 'GroovyCSV' 109 | packaging 'jar' 110 | // optionally artifactId can be defined here 111 | description 'Library for parsing csv in Groovy' 112 | url 'http://github.com/xlson/groovycsv' 113 | inceptionYear '2010' 114 | 115 | scm { 116 | connection 'scm:git:git@github.com:xlson/groovycsv.git' 117 | developerConnection 'scm:git:git@github.com:xlson/groovycsv.git' 118 | url 'http://github.com/xlson/groovycsv' 119 | } 120 | 121 | licenses { 122 | license { 123 | name 'The Apache License, Version 2.0' 124 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 125 | } 126 | } 127 | 128 | developers { 129 | developer { 130 | id 'xlson' 131 | name 'Leonard Gram' 132 | email 'leo@xlson.com' 133 | url 'http://xlson.com/' 134 | timezone '+1' 135 | } 136 | } 137 | } 138 | 139 | pom.withXml { XmlProvider xmlProvider -> 140 | def xml = xmlProvider.asString() 141 | def pomXml = new XmlParser().parseText(xml.toString()) 142 | 143 | pomXml.version[0] + { packaging('jar') } 144 | 145 | def newXml = new StringWriter() 146 | def printer = new XmlNodePrinter(new PrintWriter(newXml)) 147 | printer.preserveWhitespace = true 148 | printer.print(pomXml) 149 | xml.setLength(0) 150 | xml.append(newXml.toString()) 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /gradle.properties.example: -------------------------------------------------------------------------------- 1 | // This must be configured if you wanna deploy to the Nexus OSS Public Repo 2 | signing.keyId= 3 | signing.password= 4 | signing.secretKeyRingFile= 5 | 6 | ossrhUsername= 7 | ossrhPassword= 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xlson/groovycsv/f6d25f8418391258e684aac1084eb939280abb47/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Nov 06 22:26:46 CET 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.3.1-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /samples/GettingDependencyUsingGrab.groovy: -------------------------------------------------------------------------------- 1 | @Grab('com.xlson.groovycsv:groovycsv:0.2') 2 | import com.xlson.groovycsv.CsvParser 3 | 4 | def csv = '''Name,Lastname 5 | Mark,Andersson 6 | Pete,Hansen''' 7 | 8 | def data = new CsvParser().parse(csv) 9 | for(line in data) { 10 | println "$line.Name $line.Lastname" 11 | } 12 | -------------------------------------------------------------------------------- /samples/UsingAutoDetection.groovy: -------------------------------------------------------------------------------- 1 | import com.xlson.groovycsv.CsvParser 2 | 3 | def csv = '''Name:Lastname 4 | Mark:Andersson 5 | Pete:Hansen''' 6 | 7 | def data = new CsvParser().parse(csv, autoDetect:true) 8 | for(line in data) { 9 | println "$line.Name $line.Lastname" 10 | } 11 | -------------------------------------------------------------------------------- /src/com/xlson/groovycsv/AutoDetectHandler.groovy: -------------------------------------------------------------------------------- 1 | package com.xlson.groovycsv 2 | 3 | class AutoDetectHandler { 4 | 5 | List autoDetectSeparators = [",", ";", ":", "|"] 6 | 7 | List autoDetectQuoteChars = ['"', "'", "%"] 8 | 9 | String linesToInspect 10 | 11 | 12 | String autoDetectQuoteChar() { 13 | return mostFrequentChar(linesToInspect, autoDetectQuoteChars) 14 | } 15 | 16 | String autoDetectSeparator() { 17 | return mostFrequentChar(linesToInspect, autoDetectSeparators) 18 | } 19 | 20 | /** 21 | * Find the most frequent character in a string among a list of characters. 22 | * Falls back on the first character in the list if no character is found. 23 | * 24 | * @param sequence The string to search. 25 | * @param characters The list of characters to search. 26 | * @return The most frequent character. 27 | */ 28 | private mostFrequentChar(String sequence, List characters) { 29 | characters.max { sequence.count(it) } 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/com/xlson/groovycsv/CsvIterator.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Leonard Gram 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.xlson.groovycsv 18 | 19 | import com.opencsv.CSVReader 20 | 21 | /** 22 | * Iterates over the csv data in a non-synchronized way. 23 | * 24 | * @author Leonard Gram 25 | * @since 0.1 26 | */ 27 | class CsvIterator implements Iterator { 28 | 29 | private def columns 30 | 31 | private CSVReader csvReader 32 | 33 | private def readValue 34 | 35 | private Boolean closed = false 36 | 37 | def CsvIterator(def columnNames, CSVReader csvReader) { 38 | this.columns = [:] 39 | columnNames.eachWithIndex { name, i -> 40 | columns."$name" = i 41 | } 42 | 43 | this.csvReader = csvReader 44 | } 45 | 46 | /** 47 | * Closes the underlying reader object. Could be useful if one would 48 | * not like to read all of the csv into memory. 49 | * 50 | * @throws IllegalStateException if the underlying dataset is already closed. 51 | */ 52 | void close() { 53 | throwsExceptionIfClosed() 54 | 55 | closed = true 56 | csvReader.close() 57 | } 58 | 59 | /** 60 | * Checks if there is more data available. Will close the underlying dataset 61 | * if there isn't any more data. 62 | * 63 | * @return true if there is more data in the iterator 64 | */ 65 | boolean hasNext() { 66 | if (isClosed()) { 67 | return false 68 | } else if (nextValueIsRead()) { 69 | return true 70 | } else { 71 | readValue = getNextValue() 72 | if (readValue == null) { 73 | close() 74 | } 75 | return readValue != null 76 | } 77 | 78 | } 79 | 80 | /** 81 | * Checks if the underlying reader is closed. 82 | * 83 | * @return true if the underlying reader is closed 84 | */ 85 | boolean isClosed() { 86 | closed 87 | } 88 | 89 | private boolean nextValueIsRead() { 90 | readValue as boolean 91 | } 92 | 93 | private def getNextValue() { 94 | if (nextValueIsRead()) { 95 | def value = readValue 96 | readValue = null 97 | return value 98 | } else { 99 | def nextValue 100 | for (; ;) { 101 | nextValue = csvReader.readNext() 102 | if (nextValue == null) { 103 | break 104 | } else if (isEmptyLine(nextValue)) { 105 | // Continues the loop and reads another value 106 | } else { 107 | break 108 | } 109 | } 110 | return nextValue 111 | } 112 | } 113 | 114 | private boolean isEmptyLine(String[] nextValue) { 115 | nextValue.size() == 1 && nextValue.first().isEmpty() 116 | } 117 | 118 | /** 119 | * Gets the next row in the csv file. 120 | * 121 | * @return an instance of PropertyMapper 122 | */ 123 | def next() { 124 | throwsExceptionIfClosed() 125 | 126 | new PropertyMapper(columns: columns, values: nextValue) 127 | } 128 | 129 | private def throwsExceptionIfClosed() { 130 | if (isClosed()) { 131 | throw new IllegalStateException("The connection the underlying dataset has already been closed.") 132 | } 133 | } 134 | 135 | /** 136 | * remove is not supported in CsvIterator. 137 | * 138 | * @throws UnsupportedOperationException when called 139 | */ 140 | void remove() { 141 | throw new UnsupportedOperationException() 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/com/xlson/groovycsv/CsvParser.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Leonard Gram 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.xlson.groovycsv 18 | 19 | import com.opencsv.CSVReader 20 | 21 | /** 22 | * Helper class used to parse information from csv files using the column names 23 | * in the first line. Currently it only supports csv files where the first line 24 | * contains the column names. 25 | *

26 | * Usage: 27 | *

 28 |  * def csv = '''Name,Lastname
 29 |  * Mark,Andersson
 30 |  * Pete,Hansen'''
 31 |  *
 32 |  * def data = new CsvParser().parse(csv)
 33 |  * for(line in data) {*   println "$line.Name $line.Lastname"
 34 |  *}
35 | * 36 | * 37 | * @author Leonard Gram 38 | * @since 0.1 39 | */ 40 | class CsvParser { 41 | 42 | /** 43 | * Number of characters used to provide to autodetection (in case auto 44 | * detection is used. 45 | */ 46 | Integer autoDetectCharNumber = 1000 47 | 48 | /** 49 | * Parses a string as csv in the same way as CsvParser.parse(...). 50 | * 51 | * @param args 52 | * @param csv 53 | * @return 54 | */ 55 | static Iterator parseCsv(Map args = [:], String csv) { 56 | new CsvParser().parse(args, csv) 57 | } 58 | 59 | /** 60 | * Parses a reader as csv in the same way as CsvParser.parse(...). 61 | * 62 | * @param args 63 | * @param csv 64 | * @return 65 | */ 66 | static Iterator parseCsv(Map args = [:], Reader reader) { 67 | new CsvParser().parse(args, reader) 68 | } 69 | 70 | /** 71 | * Parses the csv supplied using the reader. See parse(Reader reader) for 72 | * more information about usage. 73 | * 74 | * @param args configurable parameters 75 | * @param csv the csv to parse 76 | * @return an instance of com.xlson.groovycsv.CsvIterator 77 | */ 78 | Iterator parse(Map args = [:], String csv) { 79 | parse(args, new StringReader(csv)) 80 | } 81 | 82 | /** 83 | * Parses the supplied csv and returns a CsvIterator that can be 84 | * use to access the data. The first line of the csv will be used 85 | * as column-headers. Named paramenters can be used to configure the 86 | * parsing, see the class documentation for more more information on 87 | * usage. There's also support for autodetecting the quote and separator 88 | * characters. 89 | *

90 | * Arguments for configuration: 91 | *

  • separator: configures the separator character to use (default: ,) 92 | *
  • quoteChar: configures the quote character to use (default: ") 93 | *
  • escapeChar: configures the escape character for the separator and quoteChar (default:\) 94 | *
  • autoDetect: sets up autodetect that will honor other configurations you've done (default: false) 95 | *
  • columnNames: set custom column names instead of using the first line 96 | *
  • readFirstLine: reads the first line as csv instead of using it as headers 97 | *
  • trimWhitespaceFromColumnNames: trims leading and trailing whitespace for column names when parsing them (default: false) 98 | *
  • skipLines: skips the specified number of lines at the beginning (default: 0) 99 | * 100 | *

    101 | * Usage: 102 | *

    103 |      * def csv = '''Fruit-Quantity
    104 |      * Apple-2
    105 |      * Pear-5'''
    106 |      *
    107 |      * def data = new CsvParser().parse(csv, separator: '-')
    108 |      *
    109 |      * // Print all fruits that have a quantity higher than 3
    110 |      * data.findAll{ (it.Quantity as int) > 3 }.each{ println it }* 
    111 | *

    112 | * @param reader the csv to parse 113 | * @param args the configuration arguments 114 | * @return an instance of com.xlson.groovycsv.CsvIterator 115 | */ 116 | Iterator parse(Map args = [:], Reader reader) { 117 | def csvReader = createCSVReader(args, reader) 118 | def columnNames = parseColumnNames(args, csvReader) 119 | new CsvIterator(columnNames, csvReader) 120 | } 121 | 122 | private def parseColumnNames(Map args, CSVReader csvReader) { 123 | def columnNames 124 | 125 | if (!args.readFirstLine) { 126 | columnNames = csvReader.readNext() 127 | } 128 | 129 | if (args.columnNames) { 130 | columnNames = args.columnNames 131 | } 132 | 133 | if (args.trimWhitespaceFromColumnNames) { 134 | columnNames = columnNames.collect { it.toString().trim() } 135 | } 136 | return columnNames 137 | } 138 | 139 | private CSVReader createCSVReader(Map args = [:], Reader reader) { 140 | Character separator 141 | Character quoteChar 142 | Character escapeChar = args.escapeChar 143 | Integer skipLines = args.skipLines ?: 0 144 | 145 | if (args.autoDetect == true) { 146 | reader = new PushbackReader(reader, autoDetectCharNumber) 147 | doAutoDetection(args, reader) 148 | separator = args.separator 149 | quoteChar = args.quoteChar 150 | } else { 151 | separator = args.separator ?: ',' 152 | quoteChar = args.quoteChar ?: '"' 153 | } 154 | 155 | if (escapeChar != null) { 156 | return new CSVReader(reader, separator, quoteChar, escapeChar, skipLines) 157 | } else { 158 | return new CSVReader(reader, separator, quoteChar, skipLines) 159 | } 160 | } 161 | 162 | /** 163 | * Performs automatic detection of separator and quote character. 164 | * 165 | * It will search arguments for values 'auto' in separator and quoteChar. It 166 | * will return a new version of the arguments modified with the values that 167 | * were found. 168 | * 169 | * If nothing is detected, the values are removed from the args. 170 | * 171 | * Note that 172 | * 173 | * @param args the configuration arguments. 174 | * @param text the CSV as a String. 175 | * @return modified args with detected. 176 | */ 177 | private Map doAutoDetection(Map args, PushbackReader reader) { 178 | def buf = new char[autoDetectCharNumber] 179 | def charsRead = reader.read(buf) 180 | def linesToInspect = new String(buf) 181 | reader.unread(buf, 0, charsRead) 182 | 183 | 184 | def autoDetector = new AutoDetectHandler(linesToInspect: linesToInspect) 185 | if (args.autoDetectQuoteChars) { 186 | autoDetector.autoDetectQuoteChars = args.autoDetectQuoteChars 187 | } 188 | if (args.autoDetectSeparators) { 189 | autoDetector.autoDetectSeparators = args.autoDetectSeparators 190 | } 191 | if (!args.separator) { 192 | def detectedSeparator = autoDetector.autoDetectSeparator() 193 | if (detectedSeparator) args.separator = detectedSeparator 194 | else args.remove("separator") 195 | } 196 | if (!args.quoteChar) { 197 | def detectedQuoteChar = autoDetector.autoDetectQuoteChar() 198 | if (detectedQuoteChar) args.quoteChar = detectedQuoteChar 199 | else args.remove("quoteChar") 200 | } 201 | return args 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /src/com/xlson/groovycsv/PropertyMapper.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Leonard Gram 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.xlson.groovycsv 18 | 19 | import groovy.transform.EqualsAndHashCode 20 | 21 | /** 22 | * Maps between column names and values in a list. Uses propertyMissing 23 | * to allow for named access. 24 | * 25 | * @author Leonard Gram 26 | * @since 0.1 27 | */ 28 | @EqualsAndHashCode 29 | class PropertyMapper { 30 | 31 | /** 32 | * A list of values for one csv line. 33 | */ 34 | def values 35 | 36 | /** 37 | * The columns of the csv. 38 | */ 39 | def columns = [:] 40 | 41 | /** 42 | * Maps properties to values. 43 | * 44 | * @param name the name of the property 45 | * @return the value as a String 46 | * @throws MissingPropertyException where the values-list doesn't contain enough data. 47 | */ 48 | def propertyMissing(String name) { 49 | def index = columns[name] 50 | if (index != null) { 51 | if(values?.size() <= index) { 52 | return null 53 | } 54 | values[index] 55 | } else { 56 | throw new MissingPropertyException(name) 57 | } 58 | } 59 | 60 | /** 61 | * Allows values to be obtained using their position 62 | * 63 | * @param index 64 | * @return the value at that position 65 | */ 66 | def getAt(Integer index) { 67 | values[index] 68 | } 69 | 70 | String toString() { 71 | columns.collect { key, index -> "$key: ${values[index]}" }.join(', ') 72 | } 73 | 74 | Map toMap() { 75 | def sortedKeys = columns.keySet().sort { columns[it] } 76 | [sortedKeys, values].transpose().collectEntries() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/com/xlson/groovycsv/AutoDetectHandlerSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.xlson.groovycsv 2 | 3 | import spock.lang.Specification; 4 | 5 | class AutoDetectHandlerSpec extends Specification { 6 | 7 | 8 | def "should auto detect quote character"() { 9 | setup: 10 | def adh = new AutoDetectHandler(linesToInspect: csvData) 11 | 12 | expect: 13 | adh.autoDetectQuoteChar() == quoteChar 14 | 15 | where: 16 | csvData | quoteChar 17 | testDataWithColumnNamesAnd3Rows | '"' 18 | testDataWithColumnNamesAnd2Rows | '"' 19 | csvUsingDoubleQuoteAsQuoteChar | '"' 20 | csvUsingPercentageAsQuoteChar | "%" 21 | 22 | } 23 | 24 | def "should auto detect separator"() { 25 | setup: 26 | def adh = new AutoDetectHandler(linesToInspect: csvData) 27 | 28 | expect: 29 | adh.autoDetectSeparator() == separator 30 | 31 | where: 32 | csvData | separator 33 | testDataWithColumnNamesAnd3Rows | "," 34 | testDataWithColumnNamesAnd2Rows | "," 35 | csvUsingDoubleQuoteAsQuoteChar | ',' 36 | csvWithColonAsSeparator | ":" 37 | } 38 | 39 | 40 | def getTestDataWithColumnNamesAnd3Rows() { 41 | '''Name,Lastname,Age,Email 42 | Mark,Hamilton,45,mark@hamilton.com 43 | Bosse,Bildoktorn,50,bildoktorn@tv4.se 44 | Peps,Persson,65,peps.persson@hotmail.com''' 45 | } 46 | 47 | def getTestDataWithColumnNamesAnd2Rows() { 48 | '''Letter,Word,Number 49 | a,paris,5 50 | h,drink,60''' 51 | } 52 | 53 | def getCsvWithColonAsSeparator() { 54 | '''Fruit:Count 55 | Apple:5 56 | Pear:10 57 | Kiwi:200''' 58 | } 59 | 60 | def getCsvUsingDoubleQuoteAsQuoteChar() { 61 | '''Typo,Desc 62 | 123,"text ,and more"''' 63 | } 64 | 65 | def getCsvUsingPercentageAsQuoteChar() { 66 | '''Typo,Desc 67 | 1123,%bla, ha%''' 68 | 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /test/com/xlson/groovycsv/ConfigurableColumnNamesSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.xlson.groovycsv 2 | 3 | import com.opencsv.CSVReader 4 | import spock.lang.Specification 5 | 6 | class ConfigurableColumnNamesSpec extends Specification { 7 | 8 | def csvWithoutCoulmnNames = """field1,field2,field3, 9 | Joe,Doe,18 10 | Jane,Doe,24""" 11 | 12 | def "Replaces existing column names with new ones"() { 13 | setup: 14 | def persons = new CsvParser().parse(csvWithoutCoulmnNames, columnNames: ['name', 'lastname', 'age']) 15 | 16 | when: 17 | def joe = persons.next() 18 | 19 | then: 20 | joe.name == 'Joe' 21 | joe.lastname == 'Doe' 22 | joe.age == '18' 23 | } 24 | 25 | def "Read all lines of csv content using custom column as column names"() { 26 | setup: 27 | def persons = new CsvParser().parse(csvWithoutCoulmnNames, readFirstLine: true, columnNames: ['name', 'lastname', 'age']) 28 | 29 | when: 30 | def names = persons*.name 31 | 32 | then: 33 | names == ['field1', 'Joe', 'Jane'] 34 | } 35 | 36 | def "Parses columns from the first line by default"() { 37 | setup: 38 | def reader = Mock(CSVReader) 39 | 40 | when: 41 | def columnNames = new CsvParser().parseColumnNames([:], reader) 42 | 43 | then: 44 | reader.readNext() >> ['a', 'b', 'c'] 45 | columnNames == ['a', 'b', 'c'] 46 | } 47 | 48 | def "Throws away the first line by default when using custom column names."() { 49 | setup: 50 | def reader = Mock(CSVReader) 51 | 52 | when: 53 | def customColumnNames = ['1', '2', '3'] 54 | def columnNames = new CsvParser().parseColumnNames([columnNames: customColumnNames], reader) 55 | 56 | then: 57 | 1 * reader.readNext() 58 | columnNames == customColumnNames 59 | } 60 | 61 | def "Does not read the first line as header when readFirstLine is specified."() { 62 | setup: 63 | def reader = Mock(CSVReader) 64 | 65 | when: 66 | def customColumnNames = ['1', '2', '3'] 67 | def columnNames = new CsvParser().parseColumnNames([columnNames : customColumnNames, 68 | readFirstLine: true], 69 | reader) 70 | then: 71 | 0 * reader.readNext() 72 | columnNames == customColumnNames 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /test/com/xlson/groovycsv/CsvIteratorSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.xlson.groovycsv 2 | 3 | import com.opencsv.CSVReader 4 | import spock.lang.Specification 5 | 6 | class CsvIteratorSpec extends Specification { 7 | 8 | def getCsvData() { 9 | def csv = """a,b,c 10 | 1,2,3 11 | 4,5,6""" 12 | def csvReader = new CSVReader(new StringReader(csv)) 13 | def columnNames = csvReader.readNext() 14 | return [columnNames, csvReader] 15 | } 16 | 17 | def "CsvIterator iterates correctly over the CSVReader"() { 18 | setup: 19 | def (colNames, csvReader) = csvData 20 | def iter = new CsvIterator(colNames, csvReader) 21 | 22 | expect: 23 | iter.hasNext() 24 | iter.next().a == '1' 25 | iter.hasNext() 26 | iter.next().c == '6' 27 | !iter.hasNext() 28 | } 29 | 30 | def "CsvIterator should close the underlying CSVReader instance when reaching the end of the file."() { 31 | setup: 32 | CSVReader csvReader = Mock(CSVReader) 33 | def iter = new CsvIterator(["a", "b"], csvReader) 34 | csvReader.readNext() >>> [["1", "2"], ["3", "4"], null] 35 | 36 | when: 37 | iter.next() 38 | 39 | then: 40 | iter.hasNext() 41 | !iter.isClosed() 42 | 0 * csvReader.close() 43 | 44 | when: 45 | iter.next() 46 | iter.hasNext() 47 | 48 | then: 49 | iter.isClosed() 50 | 1 * csvReader.close() 51 | } 52 | 53 | def "CsvIterator isClosed after a full iteration."() { 54 | setup: 55 | def csvIterator = new CsvIterator(*csvData) 56 | 57 | when: 58 | csvIterator.each {} 59 | 60 | then: 61 | csvIterator.isClosed() 62 | 63 | when: 64 | csvIterator.next() 65 | 66 | then: 67 | thrown(IllegalStateException) 68 | } 69 | 70 | def "close can be called on the CsvIterator to close the connection to the reader."() { 71 | setup: 72 | def (colNames, csvReader) = csvData 73 | def iter = new CsvIterator(colNames, csvReader) 74 | 75 | when: 76 | iter.next() 77 | iter.close() 78 | 79 | then: 80 | iter.isClosed() 81 | 82 | when: 83 | iter.next() 84 | 85 | then: 86 | thrown(IllegalStateException) 87 | 88 | } 89 | 90 | def "CsvIterator.hasNext() returns false when the underlying reader instance is closed."() { 91 | setup: 'Create an instance of CsvIterator consisting of 2 rows.' 92 | def csvIterator = new CsvIterator(*csvData) 93 | 94 | when: 'Iterates over the iterator until hasNext() is false' 95 | csvIterator.each {} 96 | 97 | then: 'hasNext() should return false.' 98 | !csvIterator.hasNext() 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /test/com/xlson/groovycsv/CsvParserSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.xlson.groovycsv 2 | 3 | import com.opencsv.CSVReader 4 | import spock.lang.* 5 | 6 | class CsvParserSpec extends Specification { 7 | def getTestDataWithColumnNamesAnd3Rows() { 8 | '''Name,Lastname,Age,Email 9 | Mark,Hamilton,45,mark@hamilton.com 10 | Bosse,Bildoktorn,50,bildoktorn@tv4.se 11 | Peps,Persson,65,peps.persson@hotmail.com''' 12 | } 13 | 14 | def getTestDataWithColumnNamesAnd2Rows() { 15 | '''Letter,Word,Number 16 | a,paris,5 17 | h,drink,60''' 18 | } 19 | 20 | def getCsvWithColonAsSeparator() { 21 | '''Fruit:Count 22 | Apple:5 23 | Pear:10 24 | Kiwi:200''' 25 | } 26 | 27 | def getTestDataWithQuotedComma() { 28 | '''a,b 29 | -,abc-,4 30 | abc,-4-''' 31 | } 32 | 33 | def "Iterating over the parsed csv values are available by column name."() { 34 | setup: 35 | def data = new CsvParser().parse(getTestDataWithColumnNamesAnd3Rows()) 36 | 37 | expect: 38 | data*."$columnName" == values 39 | 40 | where: 41 | columnName | values 42 | "Name" | ['Mark', 'Bosse', "Peps"] 43 | "Lastname" | ['Hamilton', 'Bildoktorn', 'Persson'] 44 | 'Age' | ['45', '50', '65'] 45 | "Email" | ['mark@hamilton.com', 'bildoktorn@tv4.se', 'peps.persson@hotmail.com'] 46 | } 47 | 48 | def "Functional collection methods are available on parsed object."() { 49 | setup: 50 | def data = new CsvParser().parse(getTestDataWithColumnNamesAnd3Rows()) 51 | 52 | expect: 53 | data.findAll { (it.Age as int) > 46 }.size() == 2 54 | } 55 | 56 | def "readAll should never be called on the CSVReader instance used to parse the csv."() { 57 | setup: 58 | CSVReader csvReader = Mock(CSVReader) 59 | def partiallyMockedCsvParser = new CsvParser() 60 | partiallyMockedCsvParser.metaClass.createCSVReader = { Reader reader -> 61 | csvReader 62 | } 63 | 64 | when: "csv is parsed and looped through" 65 | def data = partiallyMockedCsvParser.parse(getTestDataWithColumnNamesAnd2Rows()) 66 | for (d in data) { 67 | } 68 | 69 | then: "readAll() should not be called." 70 | 0 * csvReader.readAll() 71 | } 72 | 73 | def "Parse supports a custom separator."() { 74 | setup: 75 | def data = new CsvParser().parse(csvWithColonAsSeparator, separator: ':') 76 | 77 | expect: 78 | data*."$columnName" == values 79 | 80 | where: 81 | columnName | values 82 | "Fruit" | ['Apple', 'Pear', 'Kiwi'] 83 | "Count" | ["5", "10", "200"] 84 | } 85 | 86 | def getCsvUsingDoubleQuoteAsQuoteChar() { 87 | '''Typo,Desc 88 | 123,"text ,and more"''' 89 | } 90 | 91 | def getCsvUsingPercentageAsQuoteChar() { 92 | '''Typo,Desc 93 | 1123,%bla, ha%''' 94 | 95 | } 96 | 97 | def "Parse supports custom quote character."() { 98 | when: 99 | def csv = new CsvParser().parse(csvData, quoteChar: quoteChar) 100 | 101 | then: 102 | csv*."$columnName" == values 103 | 104 | where: 105 | csvData | quoteChar | values | columnName 106 | csvUsingDoubleQuoteAsQuoteChar | '"' | ['text ,and more'] | "Desc" 107 | csvUsingPercentageAsQuoteChar | "%" | ['bla, ha'] | "Desc" 108 | } 109 | 110 | def "Parse supports custom escape char."() { 111 | setup: 112 | def csvData = '''Test,It 113 | 1,"this is \"a quote\""''' 114 | def csv = new CsvParser().parse(csvData, escapeChar: "\\") 115 | 116 | expect: 117 | csv*.It == ['this is "a quote"'] 118 | 119 | } 120 | 121 | def "Parser quietly ignores one empty line."() { 122 | setup: 123 | def csvData = '''Fruit,Country 124 | 125 | Apple,Sweden 126 | ''' 127 | def csv = new CsvParser().parse(csvData) 128 | 129 | expect: 130 | for (line in csv) { 131 | line.Fruit == 'Apple' 132 | line.Country == 'Sweden' 133 | } 134 | } 135 | 136 | def "Parser quitely ignores multiple empty lines."() { 137 | setup: 138 | def csvData = '''Color,Day 139 | Red,Monday 140 | 141 | 142 | 143 | Black,Friday''' 144 | 145 | def csv = new CsvParser().parse(csvData) 146 | 147 | expect: 148 | def firstLine = csv.next() 149 | firstLine.Color == 'Red' 150 | firstLine.Day == 'Monday' 151 | 152 | def secondLine = csv.next() 153 | secondLine.Color == 'Black' 154 | secondLine.Day == 'Friday' 155 | csv.hasNext() == false 156 | } 157 | 158 | def "Parser quitely ignores multiple empty linessadadas."() { 159 | setup: 160 | def csvData = '''Color,Day 161 | 162 | 163 | 164 | Red,Monday 165 | 166 | 167 | 168 | Black,Friday 169 | 170 | 171 | ''' 172 | 173 | def csv = new CsvParser().parse(csvData) 174 | 175 | expect: 176 | for (line in csv) { 177 | println line 178 | } 179 | } 180 | 181 | def "Parse supports java.io.Reader as input."() { 182 | when: 183 | def csv = new CsvParser().parse(new StringReader(testDataWithColumnNamesAnd2Rows)) 184 | 185 | then: 186 | csv*.Number == ['5', '60'] 187 | } 188 | 189 | def "CsvParser can auto detect separator and quote character"() { 190 | when: "a CSV file is parsed with auto detection" 191 | def csv = new CsvParser().parse(autoDetect: true, csvData) 192 | 193 | then: "it should return the correct columns" 194 | csv*."$property" == values 195 | 196 | where: 197 | csvData | property | values 198 | testDataWithColumnNamesAnd3Rows | "Age" | ["45", "50", "65"] 199 | csvWithColonAsSeparator | "Count" | ["5", "10", "200"] 200 | csvUsingDoubleQuoteAsQuoteChar | "Desc" | ["text ,and more"] 201 | csvUsingDoubleQuoteAsQuoteChar | "Typo" | ["123"] 202 | testDataWithColumnNamesAnd2Rows | "Word" | ["paris", "drink"] 203 | testDataWithColumnNamesAnd3Rows | "Email" | ["mark@hamilton.com", "bildoktorn@tv4.se", "peps.persson@hotmail.com"] 204 | } 205 | 206 | def "should allow to override auto detection"() { 207 | when: "autoDetect is active and a separator is provided" 208 | def csv = new CsvParser().parse(autoDetect: true, separator: ',', csvWithColonAsSeparator) 209 | 210 | then: "the separator provided is used" 211 | csv*."Fruit:Count" == ["Apple:5", "Pear:10", "Kiwi:200"] 212 | } 213 | 214 | def "The separator should be allowed in the csv data if its quoted"() { 215 | when: 216 | def csv = new CsvParser().parse(quoteChar: '-', testDataWithQuotedComma) 217 | 218 | then: 219 | csv*.a == [',abc', 'abc'] 220 | } 221 | 222 | def "Values in the csv can be obtained by using the index of the column."() { 223 | when: 224 | def csv = new CsvParser().parse(testDataWithColumnNamesAnd2Rows) 225 | def line = csv.next() 226 | 227 | then: 228 | line[0] == 'a' 229 | line[1] == 'paris' 230 | line[2] == '5' 231 | } 232 | 233 | def csvWithoutHeaders = 'Joe,Doe,19' 234 | 235 | def "Parsing csv without headers using the position of the values."() { 236 | when: 237 | def csv = new CsvParser().parse(readFirstLine: true, csvWithoutHeaders) 238 | def line = csv.next() 239 | 240 | then: 241 | line[0] == 'Joe' 242 | line[1] == 'Doe' 243 | line[2] == '19' 244 | 245 | } 246 | 247 | def "CsvParser.parseCsv can be used statically."() { 248 | when: 249 | def csv = CsvParser.parseCsv(csvData) 250 | 251 | then: 252 | csv*.Letter == letterValues 253 | 254 | where: 255 | csvData | letterValues 256 | testDataWithColumnNamesAnd2Rows | ['a', 'h'] 257 | new StringReader(testDataWithColumnNamesAnd2Rows) | ['a', 'h'] 258 | } 259 | 260 | def getTestDataWithWhitespaceSurroundingColumnNames() { 261 | """ Name , Lastname,Country 262 | Leonard,Gram,Sweden""" 263 | } 264 | 265 | def "CsvParser can strip leading and trailing whitespaces on column names."() { 266 | when: 267 | def csv = CsvParser.parseCsv(testDataWithWhitespaceSurroundingColumnNames, trimWhitespaceFromColumnNames: true) 268 | 269 | then: 270 | for(line in csv) { 271 | line.Name == 'Leonard' 272 | line.Lastname == 'Gram' 273 | line.Country == 'Sweden' 274 | } 275 | } 276 | 277 | def "CsvParser can skip lines at the beginning of the file."() { 278 | 279 | def csvWithSkipLines = '''skipped line1 280 | skipped line2 281 | Letter,Word,Number 282 | a,paris,5 283 | h,drink,60''' 284 | when: 285 | def csv = new CsvParser().parse(skipLines: 2, csvWithSkipLines) 286 | def line = csv.next() 287 | 288 | then: 289 | line["Letter"] == "a" 290 | line["Word"] == "paris" 291 | line["Number"] == "5" 292 | } 293 | 294 | def "CsvParser and different cardinality lines"() { 295 | given: 296 | def csvWithDifferentCardinality = '''header1,header2,header3 297 | val1,val2 298 | val1,val2,val3''' 299 | 300 | when: 301 | def csv = new CsvParser().parse(csvWithDifferentCardinality) 302 | def line = csv.next() 303 | 304 | then: 305 | line["header3"] == null 306 | 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /test/com/xlson/groovycsv/PropertyMapperSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.xlson.groovycsv 2 | 3 | import spock.lang.Specification 4 | 5 | class PropertyMapperSpec extends Specification { 6 | 7 | def getTestDataWithDuplicatedLines() { 8 | '''a,b 9 | 1,2 10 | 1,2 11 | 3,4''' 12 | } 13 | 14 | def "PropertyMapper has a toMap() that returns a map representation."() { 15 | setup: 16 | def pm = new PropertyMapper(values: values, columns: columns) 17 | 18 | expect: 19 | pm.toMap() == toMapRepresentation 20 | 21 | where: 22 | columns | values | toMapRepresentation 23 | ['a': 0, 'b': 1, 'c': 2] | ['1', '2', '3'] | ['a': '1', 'b': '2', 'c': '3'] 24 | ['Name': 0, 'Age': 1] | ['Mark', '56'] | ['Name': 'Mark', 'Age': '56'] 25 | } 26 | 27 | def "PropertyMapper has a toString() that returns all the data in it's columns."() { 28 | setup: 29 | def pm = new PropertyMapper(values: values, columns: columns) 30 | 31 | expect: 32 | pm.toString() == toStringRepresentation 33 | 34 | where: 35 | columns | values | toStringRepresentation 36 | ['a': 0, 'b': 1, 'c': 2] | ['1', '2', '3'] | "a: 1, b: 2, c: 3" 37 | ['Name': 0, 'Age': 1] | ['Mark', '56'] | "Name: Mark, Age: 56" 38 | } 39 | 40 | } 41 | --------------------------------------------------------------------------------