Commit ecf6563965cf3a734a59dfba74cd29977efbe9d6
initial commit
romuloalves committed on 10/23/2021, 12:31:09 AMFiles changed
.editorconfig | ||
---|---|---|
@@ -1,0 +1,14 @@ | ||
1 … | + | |
2 … | +[*] | |
3 … | +indent_style = space | |
4 … | +indent_size = 2 | |
5 … | +end_of_line = lf | |
6 … | +charset = utf-8 | |
7 … | +trim_trailing_whitespace = true | |
8 … | +insert_final_newline = true | |
9 … | + | |
10 … | +[*.json] | |
11 … | +insert_final_newline = ignore | |
12 … | + | |
13 … | +[*.md] | |
14 … | +trim_trailing_whitespace = false |
README.md | ||
---|---|---|
@@ -1,0 +1,76 @@ | ||
1 … | +# money-block | |
2 … | + | |
3 … | +> Take notes of your earnings and expenses and process them over money-block to calculate everything for you | |
4 … | + | |
5 … | +## Symbols | |
6 … | + | |
7 … | +`$`: balance - Auto generated, shows your general balance | |
8 … | + | |
9 … | +`|`: group - Represents an account or card, like savings or some credit card | |
10 … | + | |
11 … | +`|-` other group - This represents groups as `|`, except that they won't be added to the general balance | |
12 … | + | |
13 … | +`+`: earning - Any more your received | |
14 … | + | |
15 … | +`-`: expense - Money you wasted | |
16 … | + | |
17 … | +`=`: total - Auto generated, balance of a group | |
18 … | + | |
19 … | +## Examples | |
20 … | + | |
21 … | +### With one group | |
22 … | + | |
23 … | +If you write this: | |
24 … | +``` | |
25 … | +| savings account | |
26 … | ++ 123 first deposit | |
27 … | ++ 111 second deposit | |
28 … | +``` | |
29 … | + | |
30 … | +`money-block` would return: | |
31 … | +``` | |
32 … | +$ 234 | |
33 … | + | |
34 … | +| savings account | |
35 … | ++ 123 first deposit | |
36 … | ++ 111 second deposit | |
37 … | += 234 | |
38 … | + | |
39 … | +``` | |
40 … | + | |
41 … | +### How to use | |
42 … | + | |
43 … | +```javascript | |
44 … | +const moneyBlockProcess = require('money-block'); | |
45 … | + | |
46 … | +const myMoneyNotes = ` | |
47 … | +| savings account | |
48 … | ++ 123 first deposit | |
49 … | ++ 111 second deposit | |
50 … | +`; | |
51 … | + | |
52 … | +console.log(moneyBlockProcess(myMoneyNotes)); | |
53 … | +/* OUTPUT: | |
54 … | +$ 234 | |
55 … | + | |
56 … | +| savings account | |
57 … | ++ 123 first deposit | |
58 … | ++ 111 second deposit | |
59 … | += 234 | |
60 … | + | |
61 … | +*/ | |
62 … | +``` | |
63 … | + | |
64 … | +## Testing | |
65 … | + | |
66 … | +### With npm | |
67 … | +``` | |
68 … | +npm install | |
69 … | +npm test | |
70 … | +``` | |
71 … | + | |
72 … | +### With yarn | |
73 … | +``` | |
74 … | +yarn install | |
75 … | +yarn test | |
76 … | +``` |
package-lock.json | ||
---|---|---|
The diff is too large to show. Use a local git client to view these changes. Old file size: 0 bytes New file size: 225370 bytes |
package.json | ||
---|---|---|
@@ -1,0 +1,15 @@ | ||
1 … | +{ | |
2 … | + "name": "money-block", | |
3 … | + "version": "1.0.0", | |
4 … | + "description": "", | |
5 … | + "main": "index.js", | |
6 … | + "scripts": { | |
7 … | + "test": "ava" | |
8 … | + }, | |
9 … | + "keywords": [], | |
10 … | + "author": "", | |
11 … | + "license": "ISC", | |
12 … | + "devDependencies": { | |
13 … | + "ava": "^3.15.0" | |
14 … | + } | |
15 … | +} |
src/domain/block.js | ||
---|---|---|
@@ -1,0 +1,72 @@ | ||
1 … | +const { blockIdentifiers, blockTypes } = require('./types'); | |
2 … | +const { ignoreGroupInBalance, readGroup } = require('./group'); | |
3 … | +const { readRecord } = require('./record'); | |
4 … | +const { writeNumber } = require('./number'); | |
5 … | + | |
6 … | +const GROUP_END_TYPE = 'group-end'; | |
7 … | +const IGNORED = 'ignored'; | |
8 … | + | |
9 … | +function getBlockType(line) { | |
10 … | + const lineFirstChar = line[0]; | |
11 … | + | |
12 … | + switch (lineFirstChar) { | |
13 … | + case blockIdentifiers.GROUP: | |
14 … | + return blockTypes.GROUP; | |
15 … | + case blockIdentifiers.EARNING: | |
16 … | + return blockTypes.EARNING; | |
17 … | + case blockIdentifiers.EXPENSE: | |
18 … | + return blockTypes.EXPENSE; | |
19 … | + | |
20 … | + case '': | |
21 … | + case ' ': | |
22 … | + case undefined: | |
23 … | + return GROUP_END_TYPE; | |
24 … | + | |
25 … | + default: | |
26 … | + return IGNORED; | |
27 … | + } | |
28 … | +} | |
29 … | + | |
30 … | +module.exports.readBlock = function readBlock(data) { | |
31 … | + const lines = data.split('\n'); | |
32 … | + const groupsArr = []; | |
33 … | + let currentGroup = null; | |
34 … | + | |
35 … | + for (let index = 0; index < lines.length; index++) { | |
36 … | + const line = lines[index].trim(); | |
37 … | + const blockType = getBlockType(line); | |
38 … | + | |
39 … | + switch (blockType) { | |
40 … | + case blockTypes.GROUP: // Create a group | |
41 … | + currentGroup = readGroup(line); | |
42 … | + continue; | |
43 … | + case blockTypes.EARNING: | |
44 … | + case blockTypes.EXPENSE: // Add record to current group | |
45 … | + currentGroup.records.push(readRecord(line)); | |
46 … | + continue; | |
47 … | + case GROUP_END_TYPE: // Push group to groups array | |
48 … | + if (currentGroup) groupsArr.push({ ...currentGroup }); | |
49 … | + currentGroup = null; | |
50 … | + continue; | |
51 … | + } | |
52 … | + } | |
53 … | + | |
54 … | + return groupsArr; | |
55 … | +}; | |
56 … | + | |
57 … | +module.exports.writeBlock = function writeBlock(balance, groupsArr) { | |
58 … | + function writeGroups(groups) { | |
59 … | + return groups.reduce((text, group, groupIndex) => { | |
60 … | + return `${text}${groupIndex > 0 ? ` | |
61 … | +` : ''}${group.toString()} | |
62 … | +${group.records.reduce((recordsText, record, recordIndex) => `${recordsText}${recordIndex > 0 ? ` | |
63 … | +` : ''}${record.toString()}`, '')} | |
64 … | +${blockIdentifiers.TOTAL} ${writeNumber(group.total)} | |
65 … | +`; | |
66 … | + }, ''); | |
67 … | + } | |
68 … | + | |
69 … | + return `${blockIdentifiers.BALANCE} ${writeNumber(balance)} | |
70 … | + | |
71 … | +${writeGroups(groupsArr)}`; | |
72 … | +}; |
src/domain/group.js | ||
---|---|---|
@@ -1,0 +1,25 @@ | ||
1 … | +const { blockTypes, blockIdentifiers } = require('./types'); | |
2 … | + | |
3 … | +const IGNORE_GROUP_IN_BALANCE = '-'; | |
4 … | + | |
5 … | +function ignoreGroupInBalance(groupLine) { | |
6 … | + if (groupLine[0] !== blockIdentifiers.GROUP) { | |
7 … | + throw Error('Line is not a group: ' + groupLine); | |
8 … | + } | |
9 … | + | |
10 … | + return groupLine[1] === IGNORE_GROUP_IN_BALANCE; | |
11 … | +} | |
12 … | +module.exports.ignoreGroupInBalance = ignoreGroupInBalance; | |
13 … | + | |
14 … | +module.exports.readGroup = function readGroup(line) { | |
15 … | + const groupName = line.split(' ').slice(1).join(' '); | |
16 … | + const shouldIgnoreGroupInBalance = ignoreGroupInBalance(line); | |
17 … | + | |
18 … | + return { | |
19 … | + name: groupName, | |
20 … | + ignoreInBalance: shouldIgnoreGroupInBalance, | |
21 … | + records: [], | |
22 … | + total: 0, | |
23 … | + toString: () => `${blockIdentifiers.GROUP}${shouldIgnoreGroupInBalance ? IGNORE_GROUP_IN_BALANCE : ''} ${groupName}` | |
24 … | + }; | |
25 … | +}; |
src/domain/number.js | ||
---|---|---|
@@ -1,0 +1,3 @@ | ||
1 … | +const isInteger = n => Number(n) === n && n % 1 === 0; | |
2 … | + | |
3 … | +module.exports.writeNumber = n => isInteger(n) ? n : Number.parseFloat(n).toFixed(2); |
src/domain/record.js | ||
---|---|---|
@@ -1,0 +1,25 @@ | ||
1 … | +const { blockTypes, blockIdentifiers } = require('./types'); | |
2 … | +const { writeNumber } = require('./number'); | |
3 … | + | |
4 … | +module.exports.readRecord = function readRecord(line) { | |
5 … | + const [symbol, value, ...nameArr] = line.split(' '); | |
6 … | + | |
7 … | + if (![blockIdentifiers.EARNING, blockIdentifiers.EXPENSE].includes(symbol)) { | |
8 … | + throw Error(`Record not in right format, expected first chat to be '+' or '-': ${line}`); | |
9 … | + } | |
10 … | + | |
11 … | + if (isNaN(value)) { | |
12 … | + throw Error('Wrong monetary value to record: ' + line); | |
13 … | + } | |
14 … | + | |
15 … | + const isEarning = symbol === '+'; | |
16 … | + const num = Number(value); | |
17 … | + const name = nameArr.join(' '); | |
18 … | + | |
19 … | + return { | |
20 … | + name, | |
21 … | + symbol, | |
22 … | + value: isEarning ? num : -num, | |
23 … | + toString: () => `${symbol} ${writeNumber(num)} ${name}`, | |
24 … | + }; | |
25 … | +}; |
src/domain/types.js | ||
---|---|---|
@@ -1,0 +1,15 @@ | ||
1 … | +const blockTypes = { | |
2 … | + GROUP: 'group', | |
3 … | + EARNING: 'earning', | |
4 … | + EXPENSE: 'expense', | |
5 … | +}; | |
6 … | +module.exports.blockTypes = blockTypes; | |
7 … | + | |
8 … | +const blockIdentifiers = { | |
9 … | + BALANCE: '$', | |
10 … | + GROUP: '|', | |
11 … | + EARNING: '+', | |
12 … | + EXPENSE: '-', | |
13 … | + TOTAL: '=', | |
14 … | +}; | |
15 … | +module.exports.blockIdentifiers = blockIdentifiers; |
src/helpers/block.js | ||
---|---|---|
@@ -1,0 +1,36 @@ | ||
1 … | +const IGNORE_GROUP_IN_BALANCE = '-'; | |
2 … | + | |
3 … | +function getValue(line) { | |
4 … | + const blockType = getBlockType(line); | |
5 … | + if (![blockTypes.EARNING, blockTypes.EXPENSE].includes(blockType)) { | |
6 … | + throw Error('Line does not have value format: ' + line); | |
7 … | + } | |
8 … | + | |
9 … | + const parts = line.trim().split(' '); | |
10 … | + | |
11 … | + if (parts.length < 2) { | |
12 … | + throw Error('Line not in right format: ' + line); | |
13 … | + } | |
14 … | + | |
15 … | + const value = parts[1]; | |
16 … | + | |
17 … | + if (isNaN(value)) { | |
18 … | + throw Error('Wrong value to line: ' + line); | |
19 … | + } | |
20 … | + | |
21 … | + const num = Number(value); | |
22 … | + | |
23 … | + return blockType === blockTypes.EARNING ? num : num * -1; | |
24 … | +} | |
25 … | +module.exports.getValue = getValue; | |
26 … | + | |
27 … | +function ignoreGroupInBalance(groupLine) { | |
28 … | + const blockType = getBlockType(groupLine); | |
29 … | + | |
30 … | + if (blockType !== blockTypes.GROUP) { | |
31 … | + throw Error('Line is not a group'); | |
32 … | + } | |
33 … | + | |
34 … | + return groupLine[1] === IGNORE_GROUP_IN_BALANCE; | |
35 … | +} | |
36 … | +module.exports.ignoreGroupInBalance = ignoreGroupInBalance; |
src/index.js | ||
---|---|---|
@@ -1,0 +1,23 @@ | ||
1 … | +const { readBlock, writeBlock } = require('./domain/block'); | |
2 … | + | |
3 … | +/* | |
4 … | + * data {string} - Data to be parsed | |
5 … | + */ | |
6 … | +module.exports = function process(lines) { | |
7 … | + const groupsArr = readBlock(lines); | |
8 … | + | |
9 … | + // Calculate total of each group | |
10 … | + const groupsWithTotals = groupsArr.map(g => ({ | |
11 … | + ...g, | |
12 … | + total: g.records.reduce((total, record) => total + record.value, g.total), | |
13 … | + })); | |
14 … | + | |
15 … | + // Calculate balance | |
16 … | + const balance = groupsWithTotals.reduce((balance, g) => { | |
17 … | + if (g.ignoreInBalance) return balance; | |
18 … | + | |
19 … | + return balance + g.total; | |
20 … | + }, 0); | |
21 … | + | |
22 … | + return writeBlock(balance, groupsWithTotals); | |
23 … | +}; |
tests/__mocks__/four-groups-with-balance-ignore-group.txt | ||
---|---|---|
@@ -1,0 +1,21 @@ | ||
1 … | +$ 12402867.05 | |
2 … | + | |
3 … | +| savings account | |
4 … | ++ 123 first deposit | |
5 … | ++ 111 second deposit | |
6 … | +- 29.48 wasting money | |
7 … | ++ 400 saved a lot!!! | |
8 … | + | |
9 … | +| investing | |
10 … | ++ 1240 first deposit | |
11 … | ++ 02.40 second deposit | |
12 … | ++ 4.13 investing hard! | |
13 … | + | |
14 … | +| savings account 2 | |
15 … | ++ 1000 first | |
16 … | +- 40 don't know | |
17 … | ++ 55.55 more deposited | |
18 … | += 1015.55 | |
19 … | + | |
20 … | +|- investing hard | |
21 … | ++ 12400000.45 lot of money!! |
tests/__mocks__/four-groups-with-balance.txt | ||
---|---|---|
@@ -1,0 +1,21 @@ | ||
1 … | +$ 1234.44 | |
2 … | + | |
3 … | +| savings account | |
4 … | ++ 123 first deposit | |
5 … | ++ 111 second deposit | |
6 … | +- 29.48 wasting money | |
7 … | ++ 400 saved a lot!!! | |
8 … | + | |
9 … | +| investing | |
10 … | ++ 1240 first deposit | |
11 … | ++ 02.40 second deposit | |
12 … | ++ 4.13 investing hard! | |
13 … | + | |
14 … | +| savings account 2 | |
15 … | ++ 1000 first | |
16 … | +- 40 don't know | |
17 … | ++ 55.55 more deposited | |
18 … | += 1015.55 | |
19 … | + | |
20 … | +| investing hard | |
21 … | ++ 12400000.45 lot of money!! |
tests/__mocks__/four-groups-with-total.txt | ||
---|---|---|
@@ -1,0 +1,21 @@ | ||
1 … | +$ | |
2 … | + | |
3 … | +| savings account | |
4 … | ++ 123 first deposit | |
5 … | ++ 111 second deposit | |
6 … | +- 29.48 wasting money | |
7 … | ++ 400 saved a lot!!! | |
8 … | + | |
9 … | +| investing | |
10 … | ++ 1240 first deposit | |
11 … | ++ 02.40 second deposit | |
12 … | ++ 4.13 investing hard! | |
13 … | + | |
14 … | +| savings account 2 | |
15 … | ++ 1000 first | |
16 … | +- 40 don't know | |
17 … | ++ 55.55 more deposited | |
18 … | += 1015.55 | |
19 … | + | |
20 … | +| investing hard | |
21 … | ++ 12400000.45 lot of money!! |
tests/__mocks__/four-groups.txt | ||
---|---|---|
@@ -1,0 +1,20 @@ | ||
1 … | +$ | |
2 … | + | |
3 … | +| savings account | |
4 … | ++ 123 first deposit | |
5 … | ++ 111 second deposit | |
6 … | +- 29.48 wasting money | |
7 … | ++ 400 saved a lot!!! | |
8 … | + | |
9 … | +| investing | |
10 … | ++ 1240 first deposit | |
11 … | ++ 02.40 second deposit | |
12 … | ++ 4.13 investing hard! | |
13 … | + | |
14 … | +| savings account 2 | |
15 … | ++ 1000 first | |
16 … | +- 40 don't know | |
17 … | ++ 55.55 more deposited | |
18 … | + | |
19 … | +| investing hard | |
20 … | ++ 12400000.45 lot of money!! |
tests/__mocks__/single-group.txt | ||
---|---|---|
@@ -1,0 +1,5 @@ | ||
1 … | +$ | |
2 … | + | |
3 … | +| savings account | |
4 … | ++ 123 first deposit | |
5 … | ++ 111 second deposit |
tests/__mocks__/two-groups.txt | ||
---|---|---|
@@ -1,0 +1,10 @@ | ||
1 … | +$ | |
2 … | + | |
3 … | +| savings account | |
4 … | ++ 123 first deposit | |
5 … | ++ 111 second deposit | |
6 … | + | |
7 … | +| investing | |
8 … | ++ 1240 first deposit | |
9 … | ++ 02.40 second deposit | |
10 … | ++ 4.13 investing hard! |
tests/helpers/mock.js | ||
---|---|---|
@@ -1,0 +1,7 @@ | ||
1 … | +const { readFileSync } = require('fs'); | |
2 … | +const { join } = require('path'); | |
3 … | + | |
4 … | +module.exports.readMock = function readMock(mockFilename) { | |
5 … | + const filePath = join(__dirname, '..', '__mocks__', mockFilename); | |
6 … | + return readFileSync(filePath, { encoding: 'utf-8' }); | |
7 … | +}; |
tests/index.test.js | ||
---|---|---|
@@ -1,0 +1,172 @@ | ||
1 … | +const test = require('ava'); | |
2 … | + | |
3 … | +const { readMock } = require('./helpers/mock'); | |
4 … | +const moneyBlockProcess = require('../src/index'); | |
5 … | + | |
6 … | +test('single group', function(t) { | |
7 … | + const data = readMock('single-group.txt'); | |
8 … | + const expected = `$ 234 | |
9 … | + | |
10 … | +| savings account | |
11 … | ++ 123 first deposit | |
12 … | ++ 111 second deposit | |
13 … | += 234 | |
14 … | +`; | |
15 … | + | |
16 … | + const response = moneyBlockProcess(data); | |
17 … | + | |
18 … | + t.is(response, expected); | |
19 … | +}); | |
20 … | + | |
21 … | +test('two groups', function(t) { | |
22 … | + const data = readMock('two-groups.txt'); | |
23 … | + const expected = `$ 1480.53 | |
24 … | + | |
25 … | +| savings account | |
26 … | ++ 123 first deposit | |
27 … | ++ 111 second deposit | |
28 … | += 234 | |
29 … | + | |
30 … | +| investing | |
31 … | ++ 1240 first deposit | |
32 … | ++ 2.40 second deposit | |
33 … | ++ 4.13 investing hard! | |
34 … | += 1246.53 | |
35 … | +`; | |
36 … | + | |
37 … | + const response = moneyBlockProcess(data); | |
38 … | + | |
39 … | + t.is(response, expected); | |
40 … | +}); | |
41 … | + | |
42 … | +test('four groups', function(t) { | |
43 … | + const data = readMock('four-groups.txt'); | |
44 … | + const expected = `$ 12402867.05 | |
45 … | + | |
46 … | +| savings account | |
47 … | ++ 123 first deposit | |
48 … | ++ 111 second deposit | |
49 … | +- 29.48 wasting money | |
50 … | ++ 400 saved a lot!!! | |
51 … | += 604.52 | |
52 … | + | |
53 … | +| investing | |
54 … | ++ 1240 first deposit | |
55 … | ++ 2.40 second deposit | |
56 … | ++ 4.13 investing hard! | |
57 … | += 1246.53 | |
58 … | + | |
59 … | +| savings account 2 | |
60 … | ++ 1000 first | |
61 … | +- 40 don't know | |
62 … | ++ 55.55 more deposited | |
63 … | += 1015.55 | |
64 … | + | |
65 … | +| investing hard | |
66 … | ++ 12400000.45 lot of money!! | |
67 … | += 12400000.45 | |
68 … | +`; | |
69 … | + | |
70 … | + const response = moneyBlockProcess(data); | |
71 … | + | |
72 … | + t.is(response, expected); | |
73 … | +}); | |
74 … | + | |
75 … | +test('four groups with total', function(t) { | |
76 … | + const data = readMock('four-groups-with-total.txt'); | |
77 … | + const expected = `$ 12402867.05 | |
78 … | + | |
79 … | +| savings account | |
80 … | ++ 123 first deposit | |
81 … | ++ 111 second deposit | |
82 … | +- 29.48 wasting money | |
83 … | ++ 400 saved a lot!!! | |
84 … | += 604.52 | |
85 … | + | |
86 … | +| investing | |
87 … | ++ 1240 first deposit | |
88 … | ++ 2.40 second deposit | |
89 … | ++ 4.13 investing hard! | |
90 … | += 1246.53 | |
91 … | + | |
92 … | +| savings account 2 | |
93 … | ++ 1000 first | |
94 … | +- 40 don't know | |
95 … | ++ 55.55 more deposited | |
96 … | += 1015.55 | |
97 … | + | |
98 … | +| investing hard | |
99 … | ++ 12400000.45 lot of money!! | |
100 … | += 12400000.45 | |
101 … | +`; | |
102 … | + | |
103 … | + const response = moneyBlockProcess(data); | |
104 … | + | |
105 … | + t.is(response, expected); | |
106 … | +}); | |
107 … | + | |
108 … | +test('four groups with balance', function(t) { | |
109 … | + const data = readMock('four-groups-with-balance.txt'); | |
110 … | + const expected = `$ 12402867.05 | |
111 … | + | |
112 … | +| savings account | |
113 … | ++ 123 first deposit | |
114 … | ++ 111 second deposit | |
115 … | +- 29.48 wasting money | |
116 … | ++ 400 saved a lot!!! | |
117 … | += 604.52 | |
118 … | + | |
119 … | +| investing | |
120 … | ++ 1240 first deposit | |
121 … | ++ 2.40 second deposit | |
122 … | ++ 4.13 investing hard! | |
123 … | += 1246.53 | |
124 … | + | |
125 … | +| savings account 2 | |
126 … | ++ 1000 first | |
127 … | +- 40 don't know | |
128 … | ++ 55.55 more deposited | |
129 … | += 1015.55 | |
130 … | + | |
131 … | +| investing hard | |
132 … | ++ 12400000.45 lot of money!! | |
133 … | += 12400000.45 | |
134 … | +`; | |
135 … | + | |
136 … | + const response = moneyBlockProcess(data); | |
137 … | + | |
138 … | + t.is(response, expected); | |
139 … | +}); | |
140 … | + | |
141 … | +test('four groups with balance ignoring last group', function(t) { | |
142 … | + const data = readMock('four-groups-with-balance-ignore-group.txt'); | |
143 … | + const expected = `$ 2866.60 | |
144 … | + | |
145 … | +| savings account | |
146 … | ++ 123 first deposit | |
147 … | ++ 111 second deposit | |
148 … | +- 29.48 wasting money | |
149 … | ++ 400 saved a lot!!! | |
150 … | += 604.52 | |
151 … | + | |
152 … | +| investing | |
153 … | ++ 1240 first deposit | |
154 … | ++ 2.40 second deposit | |
155 … | ++ 4.13 investing hard! | |
156 … | += 1246.53 | |
157 … | + | |
158 … | +| savings account 2 | |
159 … | ++ 1000 first | |
160 … | +- 40 don't know | |
161 … | ++ 55.55 more deposited | |
162 … | += 1015.55 | |
163 … | + | |
164 … | +|- investing hard | |
165 … | ++ 12400000.45 lot of money!! | |
166 … | += 12400000.45 | |
167 … | +`; | |
168 … | + | |
169 … | + const response = moneyBlockProcess(data); | |
170 … | + | |
171 … | + t.is(response, expected); | |
172 … | +}); |
Built with git-ssb-web