Skip to content

Commit

Permalink
#545: added "concatenate" & "replace" string column builders
Browse files Browse the repository at this point in the history
  • Loading branch information
aschonfeld committed Aug 6, 2021
1 parent a19ed63 commit ac2d803
Show file tree
Hide file tree
Showing 12 changed files with 723 additions and 101 deletions.
58 changes: 58 additions & 0 deletions dtale/column_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def __init__(self, data_id, column_type, name, cfg):
self.builder = NumericColumnBuilder(name, cfg)
elif column_type == "string":
self.builder = StringColumnBuilder(name, cfg)
elif column_type == "concatenate":
self.builder = ConcatenateColumnBuilder(name, cfg)
elif column_type == "replace":
self.builder = ReplaceColumnBuilder(name, cfg)
elif column_type == "datetime":
self.builder = DatetimeColumnBuilder(name, cfg)
elif column_type == "bins":
Expand Down Expand Up @@ -143,6 +147,60 @@ def build_code(self):
).format(name=self.name, data_str=data_str)


class ConcatenateColumnBuilder(object):
def __init__(self, name, cfg):
self.name = name
self.cfg = cfg

def build_column(self, data):
left, right = (self.cfg.get(p) for p in ["left", "right"])
left = data[left["col"]].astype("str") if "col" in left else left["val"]
right = data[right["col"]].astype("str") if "col" in right else right["val"]
return left + right

def build_code(self):
left, right = (self.cfg.get(p) for p in ["left", "right"])
return "df.loc[:, '{name}'] = {left} + {right}".format(
name=self.name,
left="df['{}'].astype('str')".format(left["col"])
if "col" in left
else left["val"],
right="df['{}'].astype('str')".format(right["col"])
if "col" in right
else right["val"],
)


class ReplaceColumnBuilder(object):
def __init__(self, name, cfg):
self.name = name
self.cfg = cfg

def build_column(self, data):
col, search, replacement, case, regex = (
self.cfg.get(p)
for p in ["col", "search", "replacement", "caseSensitive", "regex"]
)
return pd.Series(
data[col].str.replace(search, replacement, case=case, regex=regex),
index=data.index,
name=self.name,
)

def build_code(self):
col, search, replacement, case, regex = (
self.cfg.get(p)
for p in ["col", "search", "replacement", "caseSensitive", "regex"]
)
return "data['{col}'].str.replace('{search}', '{replacement}', case={case}, regex={regex})".format(
col=col,
search=search,
replacement=replacement,
case="True" if case else "False",
regex="True" if regex else "False",
)


FREQ_MAPPING = dict(month="M", quarter="Q", year="Y")


Expand Down
131 changes: 131 additions & 0 deletions static/__tests__/dtale/create/concatenate-test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { mount } from "enzyme";
import React from "react";
import { Provider } from "react-redux";
import Select from "react-select";

import { expect, it } from "@jest/globals";

import DimensionsHelper from "../../DimensionsHelper";
import mockPopsicle from "../../MockPopsicle";
import reduxUtils from "../../redux-test-utils";

import {
buildInnerHTML,
clickMainMenuButton,
mockChartJS,
mockT as t,
tick,
tickUpdate,
withGlobalJquery,
} from "../../test-utils";

import { clickBuilder } from "./create-test-utils";

describe("DataViewer tests", () => {
let result, CreateColumn;

function findConcatenateInputs(r) {
const CreateConcatenate = require("../../../popups/create/CreateConcatenate").default;
return r.find(CreateConcatenate).first();
}

function findLeftInputs(r) {
return findConcatenateInputs(r).find("div.form-group").first();
}

const simulateClick = async r => {
r.simulate("click");
await tick();
};

const dimensions = new DimensionsHelper({
offsetWidth: 500,
offsetHeight: 500,
innerWidth: 1205,
innerHeight: 775,
});

beforeAll(() => {
dimensions.beforeAll();
const mockBuildLibs = withGlobalJquery(() =>
mockPopsicle.mock(url => {
const { urlFetcher } = require("../../redux-test-utils").default;
return urlFetcher(url);
})
);

mockChartJS();

jest.mock("popsicle", () => mockBuildLibs);
});

beforeEach(async () => {
const { DataViewer } = require("../../../dtale/DataViewer");
CreateColumn = require("../../../popups/create/CreateColumn").ReactCreateColumn;

const store = reduxUtils.createDtaleStore();
buildInnerHTML({ settings: "" }, store);
result = mount(
<Provider store={store}>
<DataViewer />
</Provider>,
{ attachTo: document.getElementById("content") }
);

await tick();
clickMainMenuButton(result, "Dataframe Functions");
await tickUpdate(result);
clickBuilder(result, "Concatenate");
});

afterEach(() => {
result.unmount();
});

afterAll(dimensions.afterAll);

it("DataViewer: build concatenate column", async () => {
const CreateConcatenate = require("../../../popups/create/CreateConcatenate").default;
expect(result.find(CreateConcatenate).length).toBe(1);
result
.find(CreateColumn)
.find("div.form-group")
.first()
.find("input")
.first()
.simulate("change", { target: { value: "numeric_col" } });
await simulateClick(findLeftInputs(result).find("button").first());
await simulateClick(findLeftInputs(result).find("button").last());
await simulateClick(findLeftInputs(result).find("button").first());
findLeftInputs(result).find(Select).first().instance().onChange({ value: "col1" });
await tick();
findConcatenateInputs(result)
.find("div.form-group")
.last()
.find(Select)
.first()
.instance()
.onChange({ value: "col2" });
expect(result.find(CreateColumn).instance().state.cfg).toEqual({
left: { col: "col1", type: "col" },
right: { col: "col2", type: "col" },
});
result.find("div.modal-footer").first().find("button").first().simulate("click");
await tickUpdate(result);
expect(result.find(CreateColumn)).toHaveLength(0);
});

it("DataViewer: build concatenate cfg validation", () => {
const { validateConcatenateCfg } = require("../../../popups/create/CreateConcatenate");
const cfg = {};
cfg.left = { type: "col", col: null };
expect(validateConcatenateCfg(t, cfg)).toBe("Left side is missing a column selection!");
cfg.left = { type: "val", val: null };
expect(validateConcatenateCfg(t, cfg)).toBe("Left side is missing a static value!");
cfg.left.val = "x";
cfg.right = { type: "col", col: null };
expect(validateConcatenateCfg(t, cfg)).toBe("Right side is missing a column selection!");
cfg.right = { type: "val", val: null };
expect(validateConcatenateCfg(t, cfg)).toBe("Right side is missing a static value!");
});
});
127 changes: 127 additions & 0 deletions static/__tests__/dtale/create/replace-test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { mount } from "enzyme";
import React from "react";
import { Provider } from "react-redux";
import Select from "react-select";

import { expect, it } from "@jest/globals";

import DimensionsHelper from "../../DimensionsHelper";
import mockPopsicle from "../../MockPopsicle";
import reduxUtils from "../../redux-test-utils";

import {
buildInnerHTML,
clickMainMenuButton,
mockChartJS,
mockT as t,
tick,
tickUpdate,
withGlobalJquery,
} from "../../test-utils";

import { clickBuilder } from "./create-test-utils";

function submit(res) {
res.find("div.modal-footer").first().find("button").first().simulate("click");
}

describe("DataViewer tests", () => {
let result, CreateColumn, CreateReplace;
const dimensions = new DimensionsHelper({
offsetWidth: 500,
offsetHeight: 500,
innerWidth: 1205,
innerHeight: 775,
});

beforeAll(() => {
dimensions.beforeAll();
const mockBuildLibs = withGlobalJquery(() =>
mockPopsicle.mock(url => {
const { urlFetcher } = require("../../redux-test-utils").default;
return urlFetcher(url);
})
);

mockChartJS();
jest.mock("popsicle", () => mockBuildLibs);
});

beforeEach(async () => {
CreateColumn = require("../../../popups/create/CreateColumn").ReactCreateColumn;
CreateReplace = require("../../../popups/create/CreateReplace").default;
const { DataViewer } = require("../../../dtale/DataViewer");

const store = reduxUtils.createDtaleStore();
buildInnerHTML({ settings: "" }, store);
result = mount(
<Provider store={store}>
<DataViewer />
</Provider>,
{ attachTo: document.getElementById("content") }
);
await tick();
clickMainMenuButton(result, "Dataframe Functions");
await tickUpdate(result);
clickBuilder(result, "Replace");
});

afterEach(() => {
result.unmount();
});

afterAll(dimensions.afterAll);

it("DataViewer: build replace column", async () => {
expect(result.find(CreateReplace).length).toBe(1);
result.find(CreateReplace).find(Select).first().instance().onChange({ value: "col1" });
result
.find(CreateReplace)
.find("div.form-group")
.at(1)
.find("input")
.simulate("change", { target: { value: "foo" } });
result
.find(CreateReplace)
.find("div.form-group")
.at(2)
.find("input")
.simulate("change", { target: { value: "bar" } });
result.find(CreateReplace).find("i.ico-check-box-outline-blank").first().simulate("click");
result.find(CreateReplace).find("i.ico-check-box-outline-blank").first().simulate("click");
result.update();
submit(result);
await tick();
expect(result.find(CreateColumn).instance().state.cfg).toEqual({
col: "col1",
search: "foo",
replacement: "bar",
caseSensitive: true,
regex: true,
});
expect(result.find(CreateColumn).instance().state.name).toBe("col1_replace");
});

it("DataViewer: build replace cfg validation", () => {
const { validateReplaceCfg } = require("../../../popups/create/CreateReplace");
expect(validateReplaceCfg(t, {})).toBe("Missing a column selection!");
expect(
validateReplaceCfg(t, {
col: "col1",
})
).toBe("You must enter a substring to search for!");
expect(
validateReplaceCfg(t, {
col: "col1",
search: "foo",
})
).toBe("You must enter a replacement!");
expect(
validateReplaceCfg(t, {
col: "col1",
search: "foo",
replacement: "bar",
})
).toBeNull();
});
});
Loading

0 comments on commit ac2d803

Please sign in to comment.