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

regexp: add S.replace #693

Merged
merged 1 commit into from Aug 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions .circleci/config.yml
Expand Up @@ -28,7 +28,7 @@ jobs:
nvm exec $1 npm test
}
case $CIRCLE_NODE_INDEX in
0) test_with_version 8 ;;
1) test_with_version 10 ;;
2) npm install && npm test ;;
0) test_with_version 10 ;;
1) npm install && npm test ;;
2) test_with_version 14 ;;
esac
47 changes: 47 additions & 0 deletions index.js
Expand Up @@ -4403,6 +4403,53 @@
impl: matchAll
};

//# replace :: (Array (Maybe String) -> String) -> RegExp -> String -> String
//.
//. Replaces occurrences of the given pattern within the given string
//. in accordance with the given replacement function, which receives an
//. array of captured values. Replaces all occurrences of the pattern if
//. its `g` flag is set; just the first occurrence otherwise.
//.
//. ```javascript
//. > S.replace (([$1]) => S.maybe ('') (S.toUpper) ($1)) (/(\w)/) ('foo')
//. 'Foo'
//.
//. > S.replace (([$1]) => S.maybe ('') (S.toUpper) ($1)) (/(\w)/g) ('foo')
//. 'FOO'
//.
//. > S.replace (S.show) (/(foo)(bar)?/) ('<>')
//. '<>'
//.
//. > S.replace (S.show) (/(foo)(bar)?/) ('<foo>')
//. '<[Just ("foo"), Nothing]>'
//.
//. > S.replace (S.show) (/(foo)(bar)?/) ('<foobar>')
//. '<[Just ("foo"), Just ("bar")]>'
//. ```
function replace(substitute) {
return function(pattern) {
return function(text) {
return text.replace (pattern, function() {
var groups = [];
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to know the length of this array upfront? Say, using var groups = new Array(arguments.length). If so, then that would get you slightly better performance at relatively low implementation and maintenance cost. You would have to alter groups.push statements to index assignment statements instead, eg: groups[idx] = (...).

Copy link
Member Author

Choose a reason for hiding this comment

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

Is it possible to know the length of this array upfront?

No, due to the existence of named capturing groups:

> '@sanctuary-js'.replace (/@([-\w]+)/, function() { return arguments.length; })
'4'

> '@sanctuary-js'.replace (/@(?<username>[-\w]+)/, function() { return arguments.length; })
'5'

Copy link
Member

Choose a reason for hiding this comment

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

Ah, so these named groups are skipped? Is that what the typeof check below is doing?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's right. I imagine groups is omitted when the pattern contains no named capturing groups to preserve backward compatibility. Providing the sensible empty value, {}, as an additional argument would break programs that assume that Array.prototype.slice.call(arguments, 1, -2) extracts (only) the captured values.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, wow. It took me a really long time to realize that what's happening there is: skip one argument, and keep taking arguments until a number is encountered. The numeric argument signifies the "end" of the capture group arguments. Wow. The signature of JavaScript's replace is crazier than I remember.

var group, idx = 1;
// eslint-disable-next-line no-plusplus
while (typeof (group = arguments[idx++]) !== 'number') {
groups.push (group == null ? Nothing : Just (group));
}
Avaq marked this conversation as resolved.
Show resolved Hide resolved
return substitute (groups);
});
};
};
}
_.replace = {
consts: {},
types: [$.Fn ($.Array ($.Maybe ($.String))) ($.String),
$.RegExp,
$.String,
$.String],
impl: replace
};

//. ### String

//# toUpper :: String -> String
Expand Down
2 changes: 2 additions & 0 deletions test/.eslintrc.json
@@ -1,9 +1,11 @@
{
"root": true,
"extends": ["../node_modules/sanctuary-style/eslint-es6.json"],
"parserOptions": {"ecmaVersion": 2018},
"env": {"node": true},
"globals": {"suite": false, "test": false},
"rules": {
"comma-dangle": ["error", {"arrays": "always-multiline", "objects": "always-multiline", "functions": "never"}],
"max-len": ["off"]
}
}
19 changes: 19 additions & 0 deletions test/replace.js
@@ -0,0 +1,19 @@
'use strict';

const S = require ('..');

const eq = require ('./internal/eq');


test ('replace', () => {

eq (S.show (S.replace)) ('replace :: (Array (Maybe String) -> String) -> RegExp -> String -> String');

eq (S.replace (([$1]) => S.maybe ('') (S.toUpper) ($1)) (/(\w)/) ('foo')) ('Foo');
eq (S.replace (([$1]) => S.maybe ('') (S.toUpper) ($1)) (/(\w)/g) ('foo')) ('FOO');
eq (S.replace (S.show) (/(foo)(bar)?/) ('<>')) ('<>');
eq (S.replace (S.show) (/(foo)(bar)?/) ('<foo>')) ('<[Just ("foo"), Nothing]>');
eq (S.replace (S.show) (/(foo)(bar)?/) ('<foobar>')) ('<[Just ("foo"), Just ("bar")]>');
eq (S.replace (S.show) (/@(?<username>[-\w]+)/) ('@sanctuary-js')) ('[Just ("sanctuary-js")]');

});