Working with CSV files
Posted
#1
(In Topic #375)
Regular

Each value is seperated from the next via a comma, except the last value in a line. Multiline CSV files normally contain ordered data, where each line represents a data record and each value is the data in a field. The first line may contain field headers.
I must admit I've almost always written code from scratch to deal with CSVs. I seem to find it relaxing working with string commands like Mid, Instr & Replace.
However, I really don't need to do this every time because there is a CsvFile class hidden away in gb.Util. I've only just bothered to look at it, so I thought I'd share my notes.
In a new project I started by creating a text file and adding some CSV data. I then declared an object based on the CsvFile class and added my code to a Button_Click();
Code (gambas)
- Inc iRecords 'count the number of records
- aFields = myCSVfile.Fields 'get the field headers (i.e. "name", "rank" & "serialNo")
As you can see, you only need a few lines of code to 'decode' the CSV file and make the data easily accessible.
Then I wrote some nonsense to display data;
Code (gambas)
- 'display some data from the CSV file
- Me.Text = "Dad's Army Roll Call: " & "Field count: " & aFields.Count & ", Record count: " & iRecords
If your delimited file doesn't use commas, no problem!
You can specify the delimiter when you instantiate the object:-
I've also attached an oven-ready project, including the CSV file, to save you some time.
Posted
Enthusiast

At the risk of insulting the teacher
I was informed a few days ago to use &/ instead of & "/" &
http://gambaswiki.org/wiki/lang/stringop?nh
Posted
Regular

Talking of File handling, take a look at the File class in gb.Util. You can get the contents of a file like this:-grayghost4 said
… you must have been looking over my shoulder watching my struggle with file handling…
…and if you edit it, you can save it like this:-
Code (gambas)
Really simple! Much easier than the old way.
…I was informed a few days ago to use &/ instead of & "/" &
Don't you worry, I have a very thick skin!
Neither way is right or wrong. I often prefer "wordy" to "terse" code. Long live:-
Code (gambas)
- Let integerWithAVeryLongName = integerWithAVeryLongName + 1
So just go with what you are happy with.
Posted
Enthusiast

It took a while to learn to implement it, but has made loading a group of boxes on the form a snap.
I think I am starting to learn a little OOP .
it seam that rich text is not supported here, I tried to use <small> but it did not work.
I have discovered that you don't have to count the records as you read them
the Property mycsvfile.line give the last read line So
Code (gambas)
Posted
Regular

I am a huge fan of text files and string manipulation.
Most of the work done by DirLister, is string manipulation in fact.
I'm thinking of how to write a book on the solutions applied in DirLister.
There is a lot of code and lots of solutions that can be Copy/Pasted. Well, almost "Copy/Paste".
Why I need the ideas outlined here?
I'm planning to move to a different level with DirLister. Build a statistics module to show me aggregated data on the time spent for the various operations, the amount of data processed and finally, the amount of time consumed. Maybe, finally, convert it into some currency to show the money spared on storage media spendings with DirLister.
Unfortunately, there still is a long way to go to get there…
Greetings!
“The only thing necessary for the triumph of evil is for good men to do nothing.”― Edmund Burke;
It's easy to die for an idea. It is way harder to LIVE for your idea! (Me)
It's easy to die for an idea. It is way harder to LIVE for your idea! (Me)
Posted
Guru

Posted
Regular

I guess this is the only efficient approach. Now, we can cycle through the items and sort them, according to the needs, as [sData] is suitable for any kind of loop, thus we can apply multiple filtering conditions, up to where we want to get.cogier said
Probably the filtering code might get a little fat, but this does the job!
“The only thing necessary for the triumph of evil is for good men to do nothing.”― Edmund Burke;
It's easy to die for an idea. It is way harder to LIVE for your idea! (Me)
It's easy to die for an idea. It is way harder to LIVE for your idea! (Me)
Posted
Regular

When I read (myCSVfile.Read() ) the CSV file the first line with headers are completely in lower case although the fist letter of the word is a uppercase letter. The DB refuses to update the data as the field names are case sensitive in sqlite3. Any ideas?
Code
For Each sFile In Dir(DirChooser1.SelectedPath, "*.exp").Sort()
I = 0
Debug sFile & " Len: " & Stat(DirChooser1.SelectedPath &/ sFile).Size
myCSVfile = New CsvFile(DirChooser1.SelectedPath &/ sFile)
Do Until myCSVfile.Eof
colRecords[myCSVfile.Line] = myCSVfile.Read() 'read every line into a collection
Inc iRecords 'count the number of records
Loop
aFields = myCSVfile.Fields 'get the field headers (i.e. "name", "rank" & "serialNo"
Debug "Colcounts: " & colRecords.Count
For ii = 2 To colRecords.Count + 1 ' go through every line
sSQL = "SELECT " & aFields[0] & " FROM Lenses WHERE " & aFields[0] & " = " & colRecords[CStr(2)][aFields[0]]
Debug "SQL: " & sSQL
$Rec = DBS.$Con.Exec(sSQL)
If $Rec.Available Then ' change/edit record
$Query = " ID = &1"
$Rec = DBS.$Con.Edit("Lenses", $Query, colRecords[CStr(2)][aFields[0]])
Else 'new record
$Rec = DBS.$Con.Create("Lenses")
Endif
For i = 0 To myCSVfile.Fields.count - 1 ' go through the whole data set within the line
Debug "Field: " & aFields[i] & " Content: " & colRecords[CStr(ii)][aFields[i]]
If (aFields[i] <> "DateCreated") Or aFields[i] <> "DateChanged" Then
$Rec[aFields[i]] = colRecords[CStr(ii)][aFields[i]]
Endif
Next
Next
Posted
Enthusiast

Posted
Regular

Posted
Regular

01McAc said
…Too late to change it…
…then I think you will just have to access the headers like this:-
…then carry on using the CSV code.
For File.Load you will need gb.Utils
Posted
Regular

Posted
Regular

the original CSV file looks like this, text is always in double quotes:
ID,ID_Lens,Purchase_date,Purchase_price,Purchase_serial_no,Purchase_from,Purchase_lens_colour,Purchase_lens_condition,Purchase__comment,DateCreated,DateChanged
14,100,2019-04-25,"700€",159357,"Fotoshop Berlin",Black,Rosetta,,"2021-02-25 10:41:37","2021-02-25 10:42:51"
When I read the lines with ..
..there are obvious no more double quotes in colRecords[myCSVfile.Line]. Can I get a line of the CSV file with the original double qoutes?
Posted
Regular

01McAc said
…Can I get a line of the CSV file with the original double qoutes?
I haven't looked into this in any detail, but I don't think you can preserve quotes ( " ) using the CsvFile class. If you simply read the file using the File.Load method, you could work through the data looking for quotes, but its starting to become a Horrible Hack rather than a nicely designed program.
If I were you I would take a second look at the database, and assess how much work would be required to correct the problems there. How did you populate it with quoted strings? Did you first create the data in a spreadsheet, then export as a quoted string csv file?
My other suggestion is that you take a look at the Quote() method. Maybe if you know which fields are quoted, you could re-quote them before sending them back to the database.…but that also sounds like a pretty dreadful idea to me.
Posted
Regular

thanks for your time and effort. A short follow up to describe the intention: the simple idea behind is to export new and changed data from tables into a csv file using sqlite3 DB syntax beginning from a specific date because a new version of my little project Lens Register has been released.
My code generates the following shell script …
Code (gambas)
- cmdLine = "sqlite3 -header -csv \"" & myDBLocation & "\" \"SELECT * FROM " & sElement & " WHERE DateChanged > '"
- Shell cmdLine
After installing a new release of a Lens Register release which includes the standard database users can easily import their own data they added to the database since the last release. But this turned out it is not so simple :shock:
sqlite provides an (not standard SQL) UPSERT syntax. An UPSERT is an ordinary INSERT statement that is followed by the special ON CONFLICT clause. It inserts a record and in case of a PK violation it updates the record. Exactly what I need but the fields and values needs to be assigned one by one, depending on values (integer or string) they need quotes, e.g.:
INSERT INTO Lenses (id, focallength, lensmaker_id)
VALUES(7, '50mm', 3)
ON CONFLICT(id)
DO UPDATE SET
focallength=excluded.focallength
lensmaker_id=excluded.age;
I presume that I can do a lookup to the table (Lenses), check field names and types (string or integer) and save them in a local array. In case of field type string I manually add quotes to the UPSERT statement.
Posted
Expert

KeyTable is the name of the target table in the database
AG.DateFormatFiles = "YYYYMMDD"
The rest should be fairly obvious.
Code (gambas)
Cheers - Quin.
I code therefore I am
I code therefore I am
Posted
Regular

If someone is interested in the details: my use case is
- the database is released with a default set of lens records as part of the application. Data baseline is let's say 1-1-2021 but could be any other date
- user adds new and personal data about lenses, details etc. while using the app
- a new version of the software Lens Register has been released (by me) with the default database (see #1) - means: no personal data in there
- the user exports the new and personal data since 1-1-2021 up to today into csv files (form based in the app)
- the user installs the new release at home (including the default database)
- the user imports the CSV-files (insert if the record does not exist; update if exists) in order to get back his added personal data about lenses etc.
Re #6 I need a so called upsert clause in sqlite. It looks like this:
INSERT INTO Lenses(
ID,MakerId,MountId,MaxAperture,MinAperture,FocalLength,LensType,LensLabel,Order_No,Production_era,LensVariants,Number_of_lenses_groups,MFD,Diaphragm_setting_type,Angle_of_view,Filter_type,Accessories,Materials,Dimensions_diameter,Weight,Inscriptions,Designer,Information,DateCreated,DateChanged,Aperture,Smallest_object_field,Largest_reproduction,Position_of_entrance_pupil,Scales,Viewfinder,Length_to_bayonet_flange,Largest_diameter,Tech_Data,Focusing_range,Lens_mount_description,Maximum_magnification,Compatibility,Bayonet,Dimension,Special_editions,Known_as,Lens_Coding,FocalLength_sort,Focus_throw,Type_of_focus_ring,R_lens_spec)
VALUES(104,6,1,'1.4','16.0','35mm',NULL,'35mm ZEISS Distagon T* 1.4/35 ZM',NULL,NULL,NULL,'10 | 7','0,61 m (24.01") - ∞',NULL,'Angular field** (diag. horiz. vert.): 62° | 53° | 37°','Filter thread:
M49 x 0.75',NULL,NULL,'43 mm (1.69") / Diameter max.
63 mm (2.49") / Diameter of focusing ring:
60 mm (2.39")','381 g (13.4 oz)',NULL,NULL,'Coverage at close range (MOD): 609 x 406 mm (24.00 x 16.00")
Flange focal distance: 28 mm (1.09")','2021-02-03 13:52:46','2021-02-24 15:48:04','f/1.4 – f/16',NULL,NULL,'63 mm (2.48")',NULL,NULL,NULL,NULL,'https://www.zeiss.com/consumer-products/int/photography/zm/distagon-1435-zm.html#accessories','0,70 m (27.56") - ∞',NULL,NULL,'Full Frame',NULL,'Length (with lens caps): 87 mm (3.30") / Length (without lens caps):
65 mm (2.57")',NULL,'35mm Distagon, The Fat',NULL,35,'Rotation angle of focusing ring: 90°',NULL,0)
ON CONFLICT (ID) DO UPDATE SET
MakerId = 6,
MountId = 1,
MaxAperture = '1.4',
MinAperture = '16.0',
FocalLength = '35mm',
LensType = NULL,
LensLabel = '35mm ZEISS Distagon T* 1.4/35 ZM',
Order_No = NULL,
Production_era = NULL,
LensVariants = NULL,
Number_of_lenses_groups = '10 | 7',
MFD = '0,61 m (24.01") - ∞',
Diaphragm_setting_type = NULL,
Angle_of_view = 'Angular field** (diag. horiz. vert.): 62° | 53° | 37°',
Filter_type = 'Filter thread:
M49 x 0.75',
Accessories = NULL,
Materials = NULL,
Dimensions_diameter = '43 mm (1.69") / Diameter max.
63 mm (2.49") / Diameter of focusing ring:
60 mm (2.39")',
Weight = '381 g (13.4 oz)',
Inscriptions = NULL,
Designer = NULL,
Information = 'Coverage at close range (MOD): 609 x 406 mm (24.00 x 16.00")
Flange focal distance: 28 mm (1.09")',
DateCreated = '2021-02-03 13:52:46',
DateChanged = '2021-02-24 15:48:04',
Aperture = 'f/1.4 – f/16',
Smallest_object_field = NULL,
Largest_reproduction = NULL,
Position_of_entrance_pupil = '63 mm (2.48")',
Scales = NULL,
Viewfinder = NULL,
Length_to_bayonet_flange = NULL,
Largest_diameter = NULL,
Tech_Data = 'https://www.zeiss.com/consumer-products/int/photography/zm/distagon-1435-zm.html#accessories',
Focusing_range = '0,70 m (27.56") - ∞',
Lens_mount_description = NULL,
Maximum_magnification = NULL,
Compatibility = 'Full Frame',
Bayonet = NULL,
Dimension = 'Length (with lens caps): 87 mm (3.30") / Length (without lens caps):
65 mm (2.57")',
Special_editions = NULL,
Known_as = '35mm Distagon, The Fat',
Lens_Coding = NULL,
FocalLength_sort = 35,
Focus_throw = 'Rotation angle of focusing ring: 90°',
Type_of_focus_ring = NULL,
R_lens_spec = 0
WHERE ID = 104
My Sub ImportCSVFile which generates the upsert clause above looks quite nasty but it works.
Code (gambas)
- sTablename = sFilenamepart[1]
- getFields(sTablename)
- i = 0
- iRecords = 0
- Inc iRecords 'count the number of records for later usage
- sUpdate = ""
- sUpsert = "INSERT INTO " & sTablename & "(" & gb.Lf & sHeader & ") " & gb.Lf
- sUpsert &= "VALUES("
- sUpsert &= "NULL" & ","
- sUpdate &= aFields[i] & " = "
- sUpdate &= "NULL" & "," & gb.Lf
- sUpsert &= "ON CONFLICT (ID) DO UPDATE SET" & gb.Lf
- sUpsert &= sUpdate & gb.Lf & sWhere
- Next 'colRecords.Count + 1
- Endif ' File Size <> 0
- DBS.$Con.Commit
Posted
Regular

This the basics of what I need. Thanks in advance for any assistance.
Posted
Expert

Must have a connection to your database open ($Con in the example below)
Code (gambas)
Cheers - Quin.
I code therefore I am
I code therefore I am
Posted
Regular

1 guest and 0 members have just viewed this.

