Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type casting to CSV fields #82

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open

Conversation

Libertie
Copy link

@Libertie Libertie commented Oct 7, 2021

#81

Copy link
Contributor

@James-Burgess James-Burgess left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fast work!
I've thrown in a few comments.
And will be stealing that object reduce trick from you. 🔥

src/components/VueCsvImport.vue Outdated Show resolved Hide resolved

VueCsvImportData.value = map(newCsv, (row) => {
let newRow = {};
forEach(VueCsvImportData.map, (column, field) => {
set(newRow, field, get(row, column));
set(newRow, field, typeMap[field](get(row, column)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be a good idea to throw in some kind of error handing to this?
If I had a string in the field Number('abc') -> NaN

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing. We can push a message to VueCsvImportData.errors (e.g. "Invalid data type in row X column Y.").

In terms of fallback, we could use the original string value when a field is otherwise invalid. Would it be overkill to offer a strictTypeCasting option for situations when we actually want to convert invalid fields to null?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushing to errors and falling back to the original sounds like a good plan 👍
strict option does sound like overkill. imo, I would offload something like this to the server.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is implemented in the newest commit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 looking good! will have a proper look in the AM 👍

README.md Outdated Show resolved Hide resolved
@Libertie
Copy link
Author

Libertie commented Oct 8, 2021

Oh, dang. Just realized that boolean type casting isn't working because Boolean("false") is true. 🙃 I'll have fix that.

Copy link
Contributor

@James-Burgess James-Burgess left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really looking nice!
Cant see anything blocking this from going in.
It would be nice to see more documentation about the types supported rather than just "primitive types", Possibly a new section about what is supported and some more examples?

Comment on lines 112 to 113
case 'false': case 'no': case '0': case 'null': case '': return false;
case 'true': case 'yes': case '1': return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fieldVal.toLowerCase().trim() === 'false' ? false : !!string Could be an option too but I do like the idea of being able to pass these other values. "on/off" also comes to mind.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have time to push a documentation update tomorrow and I'll add on/off to the boolean switch statement.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, just pushed a few more commits to handle on/off booleans, improve error handling, and better document the new functionality. Wow. This really turned into a big thing. 😅

@Libertie
Copy link
Author

Did a little more testing and ran into a bug that I need to fix. Basically, if you map a field and then unmap it, an error gets thrown incorrectly. This is because the field remains in the VueCsvImportData.map object, but is given a value of null. The null value breaks the new typeCast function.

A solution is to simply exclude fields with a column value of null when updating VueCsvImportData.value in VueCsvImport.vue. I'll push a fix.

let newRow = {};
forEach(VueCsvImportData.map, (column, field) => {
set(newRow, field, get(row, column));
forEach(pickBy(VueCsvImportData.map, (v) => Number.isInteger(v)), (column, field) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This nested logic is pretty hairy.
Am I correct in assuming you are trying to do this?

VueCsvImportData
  .filter((v) => Number.isInteger(v))
  .map(([column, field]) => { try... })

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's not quite it. Here is that code fully commented, with the bit you highlighted split out for clarity:

VueCsvImportData.value = map(newCsv, (row, index) => {
    // Start a new row
    let newRow = {};

    // We only want to include mapped fields, so we'll exclude anything
    // in VueCsvImportData.map that isn't mapped to a column number
    let currentlyMapped = pickBy(VueCsvImportData.map, (v) => Number.isInteger(v));

    // Loop through the mapped fields and build up a new row with each field
    forEach(currentlyMapped, (column, field) => {
        // Get the field value
        let fieldVal = get(row, column);

        // Try casting the field value
        try { 
            fieldVal = typeCast(field, fieldVal);

        // If that generates an error, add a message to the VueCsvImportData.errors array
        } catch(err) {
            VueCsvImportData.errors.push(
                defaultLanguage.errors.invalidFieldType
                    .replace(/\${row}/g, index + (VueCsvImportData.fileHasHeaders ? 1 : 2))
                    .replace(/\${col}/g, column + 1)
            );

        // Either way, add it to the row
        } finally {
            set(newRow, field, fieldVal);
        }
    });

    // Return the completed row!
    return newRow;
});

Filtering VueCsvImportData.map is necessary because if you selected a column for a field, but then later unselected it, the field remains in the map object but is assigned a null value. In order to loop through the map and build up the new row, we have to exclude these unmapped fields. Otherwise they will generate errors.

I'm happy to add some or all of the commenting above to my code. And, of course, if you seen opportunities to improve it please let me know!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok that makes sense now! Nice catch 🥇

Could you just update the method to moved the nested curreltyMapped into what it looks like in your comment above. ie:

Suggested change
forEach(pickBy(VueCsvImportData.map, (v) => Number.isInteger(v)), (column, field) => {
// We only want to include mapped fields because deselected fields remain in the map object
// so we'll exclude anything in VueCsvImportData.map that isn't mapped to a column number
const currentlyMapped = pickBy(VueCsvImportData.map, (v) => Number.isInteger(v));
forEach(currentlyMapped, (column, field) => {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. And it looks like we can actually get a slight performance boost by moving that variable assignment out of the map function. I'll push a commit.

Copy link
Contributor

@James-Burgess James-Burgess left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really excited to see this merged in! 🔥

@Libertie
Copy link
Author

Libertie commented Nov 6, 2021

Really excited to see this merged in! fire

@Libertie Libertie closed this Nov 6, 2021
@Libertie Libertie reopened this Nov 6, 2021
@sonarcloud
Copy link

sonarcloud bot commented Nov 6, 2021

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

No Coverage information No Coverage information
No Duplication information No Duplication information

@Libertie
Copy link
Author

Libertie commented Nov 6, 2021

Oops. I was just looking back at this and accidentally closed the request. (I reopened it.)

@Libertie
Copy link
Author

@jgile, any chance of getting this PR merged soon?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants