Sign in to follow this  

Registration function problem (Wall of text/code)

This topic is 2041 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hello, I have a problem making a function work correctly for my school project. At times, I feel the output I receive is very random, and most of the time it should not act like it does. I just don't see why it won't work, and now I have grown so frustrated, that I fear for my computer screen if I continue trying to solve this. So I would really like anyone here to help me see what the problem is. I attached all of the code, which has been developed using Codeblocks with GNU, but I'll try also posting the important things here.

I need every member of a vector object printed out in a text file like this in userlist.txt:
[CODE]
ID:1
created:Sun May 13 16:04:31 2012
name:admin
password:Admin1
security level:2
status:active
[/CODE]

Now, this particular user is created in main.cpp,

[CODE]
#include "preproc.h"
#include "dbmani.h"
#include "session.h"


int main()
{
system("mkdir messages");
ifstream inFile;
inFile.open("userlist.txt");

if(inFile.good())
{
// ADD DATE OF MODIFICATION
cout << "USERLIST FOUND, READING USERS.\n\n";
userstats tempBuffer;
int userCount = -1;
int overCount = 0;
string buffer;
while(getline(inFile, buffer))
{
if (0 == buffer.find("ID:"))
{
userCount++;
if (userCount > overCount)
{
userbase.users.push_back(tempBuffer);
overCount++;
}
buffer.erase(0, 3);
tempBuffer.ID = buffer;
}
else if (0 == buffer.find("created:"))
{
buffer.erase(0, 8);
tempBuffer.date = buffer;
}
else if (0 == buffer.find("name:"))
{
buffer.erase(0, 5);
tempBuffer.name = buffer;
}
else if (0 == buffer.find("password:"))
{
buffer.erase(0, 9);
tempBuffer.password = buffer;
}
else if (0 == buffer.find("status:"))
{
buffer.erase(0,7);
if (buffer == "active")
tempBuffer.active = true;
else if (buffer == "inactive")
tempBuffer.active = false;
}
}
if (userCount == 0)
{
userbase.users.push_back(tempBuffer);
}
inFile.close();
}
else
{
cout << "NO USERS FOUND, CREATING NEW LIST.\n";
inFile.close();
// Determine current date
time_t rawtime;
struct tm * timeinfo;
time (&rawtime );
timeinfo = localtime ( &rawtime );
string cdate = asctime(timeinfo);
userstats userBuffer = {"1", cdate, "admin", "Admin1", admin, 1};
userbase.users.push_back(userBuffer);
ofstream outFile("userlist.txt");

outFile << "created: " << cdate;
outFile << "mod_date: " << cdate << "\n\n";
outFile << "ID:" << userbase.users[0].ID << "\n";
outFile << "created:" << userbase.users[0].date;
outFile << "name:" << userbase.users[0].name << "\n";
outFile << "password:"<< userbase.users[0].password << "\n";
outFile << "security level:" << userbase.users[0].secLev << "\n";
outFile << "status:active\n\n";

outFile.close();
}
// CHECK DIR
char ch;
cout << "Choices are listed below\n";
cout << "1) login\t2) Register an Account\n"
<< "q) Quit.\n";
cout << "Select option: ";
bool breakFlag = false;
while (breakFlag == false && cin.get(ch))
{
cin.ignore();
// Could use getch to eliminate errors instead.
switch (ch)
{
case '1': int logID;
logID = userbase.login();
if (logID)
{
logID -= 1;
loggedin(logID);
}
cout << "\nChoices are listed below\n";
cout << "1) login\t2) Register an Account\n"
<< "q) Quit.\n";
cout << "Select option: ";
break;
case '2': userbase.regAcc();
cout << "\nChoices are listed below\n";
cout << "1) login\t2) Register an Account\n"
<< "q) Quit.\n";
cout << "Select option: ";
break;
case 'q': breakFlag = true;
cout << "\nEXITING PROGRAM\n";
break;
default : cout << "Wrong input.\n\n";
}

}
return 0;
}
[/CODE]

the "database" vector is inside a class, Userbase::userbase in dbmani.h,

[CODE]
#ifndef DBMANI_H_INCLUDED
#define DBMANI_H_INCLUDED
enum securityLevel {user = 0, moderator, admin};
struct userstats
{
string ID;
string date;
string name;
string password; // Password minimum: One capital letter and number included
securityLevel secLev; // 0/normal user (Costumer, can only interact through messages)
// 1/moderator (Can delete level 0 users and see list of users)
// 2/admin (Same as moderator, but right to delete moderators aswell, defines root password) - INERASABLE
bool active;
};
class Userbase
{
private:
string rootPass;
public:
Userbase();
void setPas();
void regAcc();
int login();
void writedb();
// void resetdb(string uP);
vector<userstats> users;
};
Userbase::Userbase()
{
rootPass = "Admin1";
}
// Objective: Change rootpassword. Possibly connect admin/root passwords -
// But having constructor-problems.
//STATUS: Functional
void Userbase::setPas()
{
cout << "Enter current root password: ";
string pass;
getline(cin, pass);
if (pass == Userbase::rootPass)
{
cout << "Select new rootPass: ";
getline(cin, pass);
Userbase::rootPass = pass;
cout << "Password is now \"" << Userbase::rootPass << "\" until system exit.\n\n";
}
else
cout << "Password incorrect.";
}

// Objective: To add member to vector database and call writedb
// Status: Functional
enum RegFlags {
notUnique, tooShort, tooLong, inappropriate
};
void Userbase::regAcc()
{
cout << "\nCREATING NEW USER\n";
string username, password;
cout << "Enter desired username: ";
getline(cin, username);
cout << "Enter desired password: ";
getline(cin,password);
const unsigned int dbsize = Userbase::users.size();
// Do background check to make sure username isn't taken
vector<RegFlags> regFlags;
for (unsigned int i = 0; i < dbsize; i++)
{
if (username == Userbase::users[i].name)
regFlags.push_back(notUnique);
}
if (password.size() < 6)
regFlags.push_back(tooShort);
if (password.size() > 15)
regFlags.push_back(tooLong);
if (regFlags.size() > 0)
{
cout << "\tREGISTRATION ERRORS: \n";
for (unsigned int j = 0; j < regFlags.size(); j++)
{
switch (regFlags[j])
{
case notUnique : cout << "\t" << j + 1 << ". Your desired username, "
<< username << ", has already been taken\n";
continue;
case tooShort : cout << "\t" << j + 1 << ". Your password is not long enough (six to 15 characters)\n";
break;

case tooLong : cout << "\t" << j + 1 << ". Your password is too long (six to 15 characters\n";
break;

case inappropriate: cout << "\t" << j + 1 << ". Your password is inappropiate.\n";
break;
}
}
cout << "\nREGISTRATION FAILED\n";
}
else
{
// Conversion of string value to integer value
int i_regID = dbsize + 1;
string str_regID;
std::stringstream out;
out << i_regID;
str_regID = out.str();
cout << str_regID;
// Date of creation
time_t rawtime;
struct tm * timeinfo;
time (&rawtime );
timeinfo = localtime ( &rawtime );
string cdate = asctime(timeinfo);
userstats regBuffer = {str_regID, cdate, username, password, user,1};
Userbase::users.push_back(regBuffer);
Userbase::writedb();
cout << "\nREGISTRATION COMPLETE\n";
}

}

// Objective: Required login to view functions
// Status: Functional
int Userbase::login()
{
cout << "\nLOGGING IN\n";
string username, password;
cout << "\tEnter username: ";
getline(cin, username);
cout << "\tEnter password: ";
getline(cin, password);
int loggedIn = 0; // 0 equals not logged in.
const int dbsize = Userbase::users.size();
for (int i = 0; i < dbsize; i++)
{
if (username == Userbase::users[i].name)
{
if (password == Userbase::users[i].password && Userbase::users[i].active == 1)
{
loggedIn = i+1;
}
}
}
if (loggedIn > 0 )
{
cout << "\n\nLOGIN SUCCESFUL\n";
cout << "Logged in as " << Userbase::users[loggedIn-1].name << ".\n";
return loggedIn;
}
else
{
cout << "Username/password incorrect.\n";
return loggedIn;
}
}

// Objective: Write vector database to .txt database
// Status: Functional
void Userbase::writedb()
{
unsigned int dbsize = Userbase::users.size();
ifstream inFile("userlist.txt");
string cdate_orig;
getline(inFile, cdate_orig);
cdate_orig.erase(0,9);

inFile.close();
ofstream outFile("userlist.txt", std::ios::trunc);
time_t rawtime;
struct tm * timeinfo;
time (&rawtime );
timeinfo = localtime ( &rawtime );
string cdate = asctime(timeinfo);
outFile << "created: " << cdate_orig << "\n";
outFile << "mod_date: " << cdate << "\n\n";
for (unsigned int i = 0; i < dbsize; i++)
{
time (&rawtime );
timeinfo = localtime ( &rawtime );
string cdate = asctime(timeinfo);

outFile << "ID:" << Userbase::users[i].ID << "\n";
outFile << "created:" << Userbase::users[i].date;
outFile << "name:" << Userbase::users[i].name << "\n";
outFile << "password:"<< Userbase::users[i].password << "\n";
if (Userbase::users[i].secLev == user)
outFile << "security level:0" << "\n";
else if (Userbase::users[i].secLev == moderator)
outFile << "security level:1" << "\n";
else if (Userbase::users[i].secLev == admin)
outFile << "security level:2" << "\n";
if (Userbase::users[i].active == true)
{
outFile << "status:active\n\n";
}
else if (Userbase::users[i].active == false)
{
outFile << "status:inactive\n\n";
}
}
outFile.close();
}
Userbase userbase;
#endif // DBMANI_H_INCLUDED
[/CODE]


I don't think session.h is needed, but it also contains some errors, which I think is attributed to secLev.

[CODE]
#ifndef SESSION_H_INCLUDED
#define SESSION_H_INCLUDED
void listUsers(int logID);
void loggedin(int logID)
{
char ch;
bool breakFlag = false;
if (userbase.users[logID].secLev == user)
{
cout << "Member choices are listed below\n";
cout << "1) Check messages\t2) Write a message"
<< "\n3) See users\nq) Log out.\n";
cout << "Choice: ";
}
else if (userbase.users[logID].secLev == moderator || userbase.users[logID].secLev == admin)
{
cout << "Member choices are listed below\n";
cout << "1) Check messages\t2) Write a message"
<< "\n3) See users\nq) Log out.\n";
cout << "Choice: ";
}
while (breakFlag == false && cin.get(ch))
{
cin.ignore();
// Could use getch to eliminate errors instead.
if (userbase.users[logID].secLev == user)
{
switch (ch)
{
case '1':
break;
case '2':
break;
case '3': listUsers(logID);
break;
case 'q': breakFlag = true;
break;
default : cout << "Wrong input.\n\n";
}
cout << "\n\nMember choices are listed below\n";
cout << "1) Check messages\t2) Write a message"
<< "\n3) See users\nq) Log out.\n";
cout << "Choice: ";
}
else if (userbase.users[logID].secLev == moderator || userbase.users[logID].secLev == admin)
{
switch (ch)
{
case '1':
break;
case '2':
break;
case '3': listUsers(logID);
break;
case 'q': breakFlag = true;
break;
default : cout << "Wrong input.\n\n";
}
cout << "\n\nMember choices are listed below\n";
cout << "1) Check messages\t2) Write a message"
<< "\n3) See users\nq) Log out.\n";
cout << "Choice: ";
}
cout << "Select option: ";
}
cout << "\n\nLOGGING OUT\n\n";
}
void listUsers(int logID)
{
if (userbase.users[logID].secLev == user)
{
cout << "NAME\t\tRIGHTS\n";
for (int i = 0; i < userbase.users.size(); i++)
{
cout << userbase.users[i].name << "\t\t";
if (userbase.users[i].secLev == user)
cout << "User\n";
else if(userbase.users[i].secLev == moderator)
cout << "Moderator\n";
else if(userbase.users[i].secLev == admin)
cout << "Admin\n";
}
}
else if (userbase.users[logID].secLev == moderator || userbase.users[logID].secLev == admin)
{
cout << "ID\tNAME\t\tRIGHTS\n";
for (int i = 0; i < userbase.users.size(); i++)
{
cout << userbase.users[i].ID << "\t";
cout << userbase.users[i].name << "\t\t";
if (userbase.users[i].secLev == user)
cout << "User\n";
else if(userbase.users[i].secLev == moderator)
cout << "Moderator\n";
else if(userbase.users[i].secLev == admin)
cout << "Admin\n";
}
}
}

#endif // SESSION_H_INCLUDED
[/CODE]


Now, the ERROR is, that when I choose to register a new person, it will rewrite the database completely wrong, and it will look like this, if I register user "asdf",

[CODE]
ID:1
created:Sun May 13 16:10:26 2012name:admin
password:Admin1
status:active
ID:2
created:Sun May 13 16:16:29 2012
name:asdf
password:asdfasdf
security level:0
status:active
[/CODE]

To make it even worse, if I register a third account, it will replace the second user, asdf, when it should in fact make a third account. It will also bug out, when I check if the desired username is unique, when creating a new account, and not find if the name has already taken. For instance, my program created ID:1 user, as always. Then I copied ID:1 user manually and replaced "ID:1" with "ID:2" and "name:admin" with "name:admina", like this,

[CODE]
created: Sun May 13 16:22:44 2012
mod_date: Sun May 13 16:22:44 2012


ID:1
created:Sun May 13 16:22:44 2012
name:admin
password:Admin1
security level:2
status:active

ID:2
created:Sun May 13 16:22:44 2012
name:admina
password:Admin1
security level:2
status:active
[/CODE]

If I try to create another "admin", it will find out that the username is not unique, if I try to create an account with the name "admina", it will not find "admina" among the users, and therfore it will complete the registration. The new userlist.txt will then look like this,

[CODE]
created: Sun May 13 16:22:44 2012
mod_date: Sun May 13 16:27:35 2012

ID:1
created:Sun May 13 16:22:44 2012name:admin
password:Admin1
status:active
ID:2
created:Sun May 13 16:27:35 2012
name:admina
password:asdfasdf
security level:0
status:active
[/CODE]

I don't think the regAcc() will read/write correct when I use writedb(), and errors only happen when I register a new user. Maybe some kind of newline bugs the reading?

Preproc.h looks like this, if curious,
[CODE]
#ifndef PREPROC_H_INCLUDED
#define PREPROC_H_INCLUDED
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <time.h>
#include <cstdlib>
#include <sstream>
using std::cout;
using std::cin;
using std::ifstream;
using std::ofstream;
using std::vector;
using std::string;

#endif // PREPROC_H_INCLUDED
[/CODE]



By the way, the "status:Functional" may not be true if you see it in a comment above a function. I would be very grateful, and give out many internets, if anyone could point out the possible faults, since I'm relatively brain dead at the moment, and I can't really understand it.

Regards,
Boooke

Share this post


Link to post
Share on other sites
Oops, sorry for double post, but I seem to have forgotten to attach the source. It was created as a Code::Blocks project. I doesn't seem like you can attach a file when editting an older post.

Regards,
Boooke


EDIT:
I'm sure its an error (One of them at least) in how the program reads the .txt into the program. If I create a new database by opening the program without any userlist.txt, it will work fine until I will create a new account. Or, if I close the program and then open it again. This will make the program read the userlist.txt instead of creating it anew if the userlist.txt exist. This will bug out, when I log in. Edited by Boooke

Share this post


Link to post
Share on other sites
I think your file reading code is at fault. It could be clearer and more robust. For example, what do you think happens if the file looks like this:
[code]
created:Sun May 13 16:22:44 2012
mod_date:Sun May 13 16:22:44 2012
ID:1
ID:2
ID:3
...
[/code]
One solution is to try to atomically add users when all the data about one has been read. Your code also seems to ignore the final user as the list gets larger. Users are only added when an "ID" block is encountered, and the odd special case where there have been no users added.

Here is a quick example I threw together. The error reporting is mixed into the file reading, which isn't ideal, but it might illustrate another way:
[code]
#include <string>
#include <vector>
#include <fstream>
#include <iostream>
#include <cassert>

enum SecurityLevel
{
SEC_USER = 0,
SEC_MODERATOR,
SEC_ADMIN
};

enum UserStatus
{
STATUS_INACTIVE = 0,
STATUS_ACTIVE,
};

struct User
{
std::string ID;
std::string date;
std::string name;
std::string password;
SecurityLevel securityLevel;
UserStatus status;
};

typedef std::vector<User> UserBase;

bool readAttribute(std::istream &input, const std::string &name, std::string &string)
{
std::string line;
while(std::getline(input, line))
{
if(!line.empty())
{
std::string prefix = name + ":";
if(0 == line.find(prefix))
{
string = line.substr(prefix.size());
return true;
}
else
{
// Invalid property
std::cerr << "Invalid property " << name << " (got: " << line << ")\n";
return false;
}
}
}
std::cerr << "Property " << name << " not found\n";
return false;
}

bool readAttribute(std::istream &input, const std::string &name, UserStatus &userStatus)
{
std::string line;
if(!readAttribute(input, name, line))
{
return false;
}

if(line == "active")
{
userStatus = STATUS_ACTIVE;
return true;
}
else if(line == "inactive")
{
userStatus = STATUS_INACTIVE;
return true;
}

std::cerr << "Invalid user status: " << line << '\n';
return false;
}

bool readAttribute(std::istream &input, const std::string &name, SecurityLevel &securityLevel)
{
std::string line;
if(!readAttribute(input, name, line))
{
return false;
}

if(line == "user")
{
securityLevel = SEC_USER;
return true;
}
else if(line == "moderator")
{
securityLevel = SEC_MODERATOR;
return true;
}
else if(line == "admin")
{
securityLevel = SEC_ADMIN;
return true;
}

std::cerr << "Invalid security level: " << line << '\n';
return false;
}

bool loadUser(std::istream &input, User &user)
{
if(!readAttribute(input, "ID", user.ID))
{
std::cerr << "Failed to read ID attribute\n";
return false;
}

if(!readAttribute(input, "created", user.date))
{
std::cerr << "Failed to read created attribute\n";
return false;
}

if(!readAttribute(input, "name", user.name))
{
std::cerr << "Failed to read name attribute\n";
return false;
}

if(!readAttribute(input, "password", user.password))
{
std::cerr << "Failed to read password attribute\n";
return false;
}

if(!readAttribute(input, "status", user.status))
{
std::cerr << "Failed to read status attribute\n";
return false;
}

if(!readAttribute(input, "security level", user.securityLevel))
{
std::cerr << "Failed to read security level attribute\n";
return false;
}

return true;
}

bool loadUserFile(const std::string &filename, UserBase &users)
{
std::ifstream input(filename);
if(!input)
{
std::cerr << "Error loading file\n";
return false;
}

// Simple local state machine
enum LoadState
{
STATE_CREATED,
STATE_MODIFIED,
STATE_USER,
STATE_ERROR,
STATE_END
};

LoadState state = STATE_CREATED;
while(!(state == STATE_END || state == STATE_ERROR))
{
// Skip blank lines
while(input && input.peek() == '\n')
{
input.ignore(1, '\n');
}

// Bail out at end of file
if(input.eof())
{
state = STATE_END;
}

switch(state)
{
case STATE_END:
std::cout << "Reached end of file\n";
break;
case STATE_CREATED:
{
std::string date;
if(readAttribute(input, "created", date))
{
state = STATE_MODIFIED;
std::cout << "Detected created date: " << date << '\n';
}
else
{
state = STATE_ERROR;
std::cerr << "Failed to find created date\n";
}
}
break;
case STATE_MODIFIED:
{
std::string date;
if(readAttribute(input, "mod_date", date))
{
state = STATE_USER;
std::cout << "Detected modified date: " << date << '\n';
}
else
{
state = STATE_ERROR;
std::cerr << "Failed to find modified date\n";
}
}
break;
case STATE_USER:
{
User user;
if(loadUser(input, user))
{
std::cout << "Detected user with ID " << user.ID << '\n';
users.push_back(user);
}
else
{
state = STATE_ERROR;
std::cerr << "Failed to load user index " << users.size() << '\n';
}
}
break;
default:
assert(false && "unhandled state");
break;
}
}
if(state != STATE_END)
{
std::cerr << "Failed to load users (state is " << state << ")\n";
}
return state == STATE_END;
}

void populateUsers(UserBase &users)
{
// TODO: add default users
assert(false && "unimplemented");
}

void writeAttribute(std::ostream &out, const std::string &name, const std::string &string)
{
out << name << ':' << string << '\n';
}

void writeAttribute(std::ostream &out, const std::string &name, UserStatus userStatus)
{
writeAttribute(out, name, userStatus == STATUS_ACTIVE ? "active" : "inactive");
}

void writeAttribute(std::ostream &out, const std::string &name, SecurityLevel securityLevel)
{
std::string string;
switch(securityLevel)
{
case SEC_USER:
string = "user";
break;
case SEC_MODERATOR:
string = "moderator";
break;
case SEC_ADMIN:
string = "admin";
break;
default:
assert(false && "unimplemented");
string = "unknown";
break;
}
writeAttribute(out, name, string);
}

void writeUser(std::ostream &out, const User &user)
{
writeAttribute(out, "ID", user.ID);
writeAttribute(out, "created", user.date);
writeAttribute(out, "name", user.name);
writeAttribute(out, "password", user.password);
writeAttribute(out, "security level", user.securityLevel);
writeAttribute(out, "status", user.status);
}

void writeUsers(std::ostream &out, const UserBase &users)
{
for(UserBase::const_iterator i = users.begin() ; i != users.end() ; ++i)
{
writeUser(out, *i);
}
out << '\n';
}

void writeUserFile(const std::string &filename, const UserBase &users)
{
// TODO:
assert(false && "unimplemented");
}

int main()
{
const std::string filename = "users.txt";

UserBase users;
if(loadUserFile(filename, users))
{
// Just write the users out now, for debug purposes.
std::string delim = " * * * ";
std::cout << users.size() << " users\n";
std::cout << delim << '\n';
for(size_t i = 0 ; i < users.size() ; ++i)
{
writeUser(std::cout, users[i]);
}
std::cout << delim << '\n';
}
else
{
std::cerr << "Failed to load users...\n";
/*
// TODO:
users = UserBase();
populateUsers(users);
writeUserFile(filename, users);
*/
}

// ...
}
[/code]
It isn't exhaustive, but it contains a few ideas that might help you. It would be nice to support dates as full types, given how you are using them.

The code might have errors in it, I didn't have time to test it much. However, it appears to correctly load the following file:
[code]
created:Sun May 13 16:22:44 2012
mod_date:Sun May 13 16:22:44 2012


ID:1
created:Sun May 13 16:21:44 2012
name:rip-off
password:lessersecret


status:active

security level:moderator

ID:2
created:Sun May 13 16:22:44 2012
name:Booke
password:supersecret
status:active
security level:admin

ID:4
created:Sun May 13 16:24:44 2012
name:Anonymous Coward
password:password1!11


status:inactive
security level:user
[/code]

Note that your code duplicates user writing - you have a separate code path for handling the "file missing / corrupted". This code should share parts of writedb(), if possible.

Share this post


Link to post
Share on other sites
Thank you for the reply. I will read through it when schools done for today. I understand the fool-proofnes of your code, and that was what I was aiming for first when I started programming my project, but i am running out of time, and functionality equals fulfilling the requirements of my project. I realized one of the errors, when the code overwrites the second user when registrating, might be that in my code the elements of the vector (If I read userlist.txt on startup) will be removed since I used push_back inside of the while loop. I did not realize that, but then the problem would also be preserving the elements of the vector somehow. But i already thought it would be preserved if I created the class of the vector outside of a function, which I did (last lines of dbmani.h)?

Share this post


Link to post
Share on other sites
Hey again. I really like your code, and I think I understand it. However, there are some few alterations I have done, because my project requires me to do do. I need to have the users vector inside of a class. Probably, this class should contain your functions aswell.

The const string filename, you use to contain the name of the text file to be read, will complain. I klugded this by just writing somthing like ifstream input("name_of_file.txt").

I implemented your code into mine at first, I will try just running yours alone later, when I'm near the code again. In fact, I would actually prefer rewriting my code based on yours, if you don't mind.

Share this post


Link to post
Share on other sites
You should not use my code in a school assignment. It was meant for illustration only (though I actually didn't catch that this was an assignment. It has such an obviously different style that your teacher will wonder where you got it.

Instead, see if you can adapt your code to take some of the principles involved. You're under pressure for time, but it can be done. The main point of my code is to make the process of parsing a user explicit. This means your code knows at what point it is in the process, and it knows when it has a full user it can add to the database. Using a state machine (explicit in loadUserFile, implicit in loadUser) is a simple way to achieve this.

Share this post


Link to post
Share on other sites
True. Copying is actually allowed. Anyone else but me and two friends do this, since our teacher has been unable to teach anyone anything, and we are the only ones with experience from before. But I do prefer making it myself, yes, and I will. Your code has certainly helped me understand, and it inspired me to include some functions for the extraction of data. Thank you very much!

Share this post


Link to post
Share on other sites

This topic is 2041 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this