Skip to content

ENH: Add expandtabs ufunc for string & unicode dtypes - #25891

Merged
ngoldbaum merged 6 commits into
numpy:mainfrom
lysnikolaou:string-ufuncs-expandtabs
Feb 29, 2024
Merged

ngoldbaum merged 6 commits into
numpy:mainfrom
lysnikolaou:string-ufuncs-expandtabs

Conversation

@lysnikolaou

Copy link
Copy Markdown
Member

No description provided.

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure why the xfail is not working, but the code itself looks good - just two tiny comments.

Comment thread numpy/_core/src/umath/string_ufuncs.cpp Resolved
@lysnikolaou

Copy link
Copy Markdown
Member Author

Hmm, expandtabs seems to be part of a list of unary functions in test_stringdtype, but it's not really a unary function. I'm not sure what to do there. cc @ngoldbaum

if (ch == '\n' || ch == '\r') {
line_pos = 0;
}
}

@ngoldbaum ngoldbaum Feb 27, 2024 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess it depends on what the input looks like, but I suspect for common real-world uses that instead of writing each non-tab character out individually, it would be faster to keep track of the index of the character after the last non-tab character and instead buffer_memcpy between there and here whenever we encounter a tab or the end of the string.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With that, the code would become rather like replace, except with a more variable replacement string. Should be possible to basically copy the loop from there to implement the faster copying.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed, it'd most probably be faster. Let's merge this to get into NumPy 2.0 and I'll open a follow-up issue.

@ngoldbaum

Copy link
Copy Markdown
Member

but it's not really a unary function

Oops, that was me being lazy and not worrying about the tabsize keyword argument. Effectively it is unary in that test.

The real fix is just to implement this for stringdtype. Hoping to push an implementation to this PR this afternoon.

Don't worry about the other comment I left about the algorithm if you're not interested in trying an implementation, the suggestion I left there is an optimization we could come back to later.

PyArrayMethodObject *NPY_UNUSED(self),
PyArray_DTypeMeta *NPY_UNUSED(dtypes[3]),
PyArray_Descr *given_descrs[3],
PyArray_Descr *loop_descrs[3],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

FWIW I have these typed as e.g. loop_descrs[] in stringdtype so there's no need to make sure they're the correct length in the signature.

@ngoldbaum

ngoldbaum commented Feb 27, 2024 •

Copy link
Copy Markdown
Member

Another comment is that it would be nice to add tests with some unicode characters. The existing test you added didn't catch a bug in a WIP version of the last commit I just pushed with stringdtype that only showed up with some unicode characters in the string.

@ngoldbaum
ngoldbaum force-pushed the string-ufuncs-expandtabs branch from 3fb9820 to 53f5a6e Compare February 28, 2024 00:28
@lysnikolaou

Copy link
Copy Markdown
Member Author

@ngoldbaum I added a couple of tests with unicode, but one fails with StringDType. I looked a tiny bit into it, but wasn't able to completely understand what's going on.

@ngoldbaum

Copy link
Copy Markdown
Member

Sorry about that! See the last commit, line_pos is a character index but new_len is the number of bytes for UTF-8, so that was inconsistent.

Comment thread numpy/_core/src/umath/string_buffer.h Outdated
Comment on lines +1465 to +1468
if (new_len > PY_SSIZE_T_MAX - 1) {
npy_gil_error(PyExc_OverflowError, "new string is too long");
return -1;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@ngoldbaum Wouldn't this overflow if we have a string where there's a tab right before reaching PY_SSIZE_T_MAX? This might lead to an incr of more than one, which means that new_len will turn negative. Am I missing something?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for pointing that out! Is checking if new_len is equal to `Py_SSIZE_T_MAX or negative sufficient? I just pushed a fixup.

Constructing such a string on a 64 bit system isn't feasible so it's not straightforward to test this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That is okay, yes. Let's merge this!

@ngoldbaum

Copy link
Copy Markdown
Member

@mhvk already looked this over and Lysandros reviewed the code I pushed afterwards, so I feel comfortable merging. I'll hit the button assuming all the tests pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants